From 32a69f09e9026ced3146aabe96d6e96f8e028776 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 11 Aug 2026 15:33:46 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20pos=20network=20checkport=20nmap=20engi?= =?UTF-8?q?ne=20overhaul=20=E2=80=94=202-pass=20scan,=20--versions=20probe?= =?UTF-8?q?,=20udp/v6=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENT_TODO.md | 4 + DOC/AGENT_Context_Project.md | 10 +- bin/pos-network-checkport | 503 +++++++++++++++++++++++++++++++++-- completions/pos.bash | 1 + 4 files changed, 487 insertions(+), 31 deletions(-) diff --git a/AGENT_TODO.md b/AGENT_TODO.md index 508f921..057549a 100644 --- a/AGENT_TODO.md +++ b/AGENT_TODO.md @@ -16,6 +16,10 @@ summary (newest last). ## Done +- **2026-08-11** — `pos network checkport` nmap overhaul: two-pass engine — pass 1 = fast `-Pn -T4 --max-retries 1` scan of only the asked ports (was: all 65535) with per-port state + nmap service names; pass 2 (`--versions`, opt-in) = `-sV --version-light` on open ports only (generous host-timeout — version probing a silent service otherwise made nmap skip the host entirely), fallback fast banner probe for open TCP with no version info; TCP fast path ~2s for 3 ports. Unprivileged UDP now falls back to the nc engine (Debian nmap `-sU` requires root and quit outright); IPv6 hosts get `-6`; `no output`/filtered states set rc=1; `--timeout` scales nmap host-timeouts. New `--versions` flag in `# POS_FLAGS:` (completions regenerated) + usage text; port-metadata fallback retained. `make gen && make check` green. + +- **2026-08-11** — `pos communication matrix sender login` error reporting: captures HTTP status + Matrix `errcode`/`error` from the JSON body (temp file, not stdout) instead of a generic "wrong credentials?" message — distinguishes unreachable homeserver from rejected credentials; auto-prepends `@` when `--user` is bare (e.g. `--user alice:example.org` → `@alice:example.org`). + - **2026-08-11** — New `share` category — `usb` and `nfs` moved out of `pos usb` / `pos system` into `pos share`: `pos share usb server` (was `pos-usb-server`), `pos share nfs server` + `pos share nfs client` (were `pos-system-nfs-*`). Renamed the three tools (`bin/pos-share-*`), updated `# POS:` headers/usage strings, `INTERACTIVE_CMDS` (`usb-server` → `share-usb-server`), `bin/pos` usage() EXAMPLES, and the notify-scope comment in `pos-system-backup`. Docs: new `DOC/howto/share.md` (USB + NFS consolidated; `howto/usb.md` deleted, NFS sections stripped from `howto/system.md`), POS.md `### share` section (replaces `### usb`, nfs rows moved out of `### system`), HOWTO/README indices, AGENT_Context hand-written spots, root README, DEV.md `INTERACTIVE_CMDS` example, AGENTS.md categories. Category is the home for future `smb`. `make gen && make check` green; `/usr/local/bin` refreshed. - **2026-08-09** — Matrix/Synapse `communication` tools — `pos communication matrix sender` + `listener`, completing the second notify platform `lib/notify.sh` was designed for (`NOTIFY_PLATFORM=telegram,matrix` fan-out; the sender implements the `send [--markdown]` contract via `notify_sender_name()`'s default key→tool mapping, no lib changes). **Sender** (`bin/pos-communication-matrix-sender`): `send [--markdown] [--room ]` PUTs `m.room.message` (`m.text`) to the client-server API v3 — room ids/aliases URL-encoded (`#pos:example.org` → `%23pos%3A…`), unique per-message txn id, `--markdown` sends `org.matrix.custom.html` via a best-effort markdown→HTML converter (bold/italic/code/fences/strike/links/headers/lists, escapes HTML, never fails the send); `login --user <@id>` (masked password prompt → `m.login.password` → saves `access_token`+`user_id`); `test`. Config scope `matrix` (`~/.config/linux_post_install/matrix.env`, `MATRIX_HOMESERVER`/`MATRIX_ACCESS_TOKEN`/`MATRIX_USER_ID`/`MATRIX_ROOM_ID`, secret masked) registered via `# POS_CONFIG:` → `pos config matrix` + tab-completion scope. **Listener** (`bin/pos-communication-matrix-listener`): systemd **user** daemon (`pos-matrix-listener.service`) long-polling `/sync` (30s timeout, per-sync `since` token, compact filter dropping presence/account_data/device noise, `m.room.message` only); reacts to `MATRIX_USER_ID`'s own messages (resolved via `/account/whoami` if unset), `MATRIX_ROOM_ID` restricts to one room; `/` and `!` both resolve; replies threaded `m.in_reply_to`; `@quiet` no-reply marker; `/cmd::desc=…` map descriptions; `ai …` bridge (`pos ai gemini ask`, per-room session `matrix-`, `ai /reset` clears, markdown stripped); interactive editor (`--status`/`--enable`/`--disable`/`--run`), 60s command timeout, exit-code prefix, ~3800-char truncation. `communication-matrix-listener` added to `INTERACTIVE_CMDS` (stdin editor + forever-loop daemon). Docs: POS.md rows + "in detail" sections + ai bridge note, howto/communication.md rewritten Matrix sections, HOWTO.md index + config table + platform note, `bin/pos` usage EXAMPLES; `make gen && make check` green. Verified against a mock homeserver: send plain/markdown/`--room`/test request shape (URL-encoding, Bearer auth, JSON body), login token save, listener owner-filter + `/status` reply + `/help` + `@quiet` silence + non-zero exit reply + interactive editor add. — state-based threshold rule monitors (eventer). Each line of `~/.config/linux_post_install/event.env` is an independent rule: `["" if ] ` (op `> < >= <= == !=`, unit suffix ok `60c`/`80%`). The check command is run on every pass and its **first numeric output** compared float-safe; operator detected as the rightmost `op threshold` pair so checks containing their own `>`/`<` (awk, redirection) parse fine. Alerts once on false→true plus one recovery message on true→false (no repeats while a condition holds); per-rule state in `~/.local/share/linux_post_install/eventer/state/` keyed by rule-line hash (editing a rule resets its state). Subcommands: `run` (timer entrypoint), `config` (interactive add/remove/edit with validation by test-running the check), `list` (rules + live values), `enable [interval]` (systemd **user** timer `pos-event-trigger.timer` + oneshot service; `5m…weekly` or `OnCalendar=…`; graceful warnings when no user systemd manager, `loginctl enable-linger` attempt), `disable`, `status`. `--dry-run` honors the DEV.md dry-run convention. Alerts via `lib/notify.sh` (Telegram default; other platforms via `NOTIFY_PLATFORM`). New: `bin/pos-system-event-trigger`, `lib/eventer-lib.sh`, `config/event.env` template (installed no-clobber by postinstall), `lib/eventer-lib.sh` installed by install.sh, `system-event-trigger` added to `INTERACTIVE_CMDS`, usage EXAMPLES row. Docs: POS.md system row, HOWTO.md index row, howto/event-trigger.md; `make gen && make check` green; functional tests covered trigger/recovery/no-repeat, float + unit parsing, editor add/remove/edit + validation + dry-run, timer enable/disable/status (graceful), dispatcher routing. diff --git a/DOC/AGENT_Context_Project.md b/DOC/AGENT_Context_Project.md index 7451982..f53d08c 100644 --- a/DOC/AGENT_Context_Project.md +++ b/DOC/AGENT_Context_Project.md @@ -75,7 +75,7 @@ Linux_post_install/ │ ├── pos-entertainment-status # Show enabled plugins and scheduler state │ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp) │ ├── pos-media-mp4 # Download video as MP4 (smart/interactive format select) -│ ├── pos-network-checkport # Check TCP port connectivity +│ ├── pos-network-checkport # Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view │ ├── pos-network-hotspot # Wi-Fi hotspot via create_ap + wihotspot-gui │ ├── pos-network-ip # Show interfaces, routes, public IP + location │ ├── pos-network-scan # Parallel ping sweep of CIDR @@ -273,7 +273,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst | entertainment | status | `pos-entertainment-status` | Show enabled plugins and scheduler state | | media | mp3 | `pos-media-mp3` | Download audio as MP3 (yt-dlp) | | media | mp4 | `pos-media-mp4` | Download video as MP4 (smart/interactive format select) | -| network | checkport | `pos-network-checkport` | Check TCP port connectivity | +| network | checkport | `pos-network-checkport` | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view | | network | hotspot | `pos-network-hotspot` | Wi-Fi hotspot via create_ap + wihotspot-gui | | network | ip | `pos-network-ip` | Show interfaces, routes, public IP + location | | network | scan | `pos-network-scan` | Parallel ping sweep of CIDR | @@ -569,7 +569,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `bin/pos` | 286 | CLI dispatcher with smart arg matching + logging + category help | | `bin/pos-ai-gemini` | 311 | Chat with Google Gemini (ask, chat, models, sessions) | | `bin/pos-communication-matrix-listener` | 565 | Matrix listener: map /command → bash, run them on room messages | -| `bin/pos-communication-matrix-sender` | 214 | Send messages to a Matrix room via the client-server API (send, test, login) | +| `bin/pos-communication-matrix-sender` | 224 | Send messages to a Matrix room via the client-server API (send, test, login) | | `bin/pos-communication-telegram-listener` | 563 | Telegram bot listener: map /command → bash, run them on chat messages | | `bin/pos-communication-telegram-sender` | 221 | Send Telegram messages/files/links/stickers via Bot API (send, test) | | `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) | @@ -584,7 +584,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `bin/pos-entertainment-status` | 49 | Show enabled plugins and scheduler state | | `bin/pos-media-mp3` | 80 | Download audio as MP3 (yt-dlp) | | `bin/pos-media-mp4` | 126 | Download video as MP4 (smart/interactive format select) | -| `bin/pos-network-checkport` | 45 | Check TCP port connectivity | +| `bin/pos-network-checkport` | 496 | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view | | `bin/pos-network-hotspot` | 93 | Wi-Fi hotspot via create_ap + wihotspot-gui | | `bin/pos-network-ip` | 69 | Show interfaces, routes, public IP + location | | `bin/pos-network-scan` | 271 | Parallel ping sweep of CIDR | @@ -597,7 +597,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `bin/pos-system-firewall` | 291 | Interactive UFW management | | `bin/pos-system-health` | 209 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL | | `bin/pos-tree` | 112 | Show the pos CLI command tree: categories, commands, and subcommands | -| `completions/pos.bash` | 288 | Dynamic bash completion | +| `completions/pos.bash` | 289 | Dynamic bash completion | | `apps/install.sh` | 171 | App install/uninstall picker/orchestrator | diff --git a/bin/pos-network-checkport b/bin/pos-network-checkport index 2e77b7d..d531ec3 100755 --- a/bin/pos-network-checkport +++ b/bin/pos-network-checkport @@ -1,45 +1,496 @@ #!/usr/bin/env bash set -euo pipefail -# POS: network checkport — Check TCP port connectivity +# POS: network checkport — Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view +# POS_FLAGS: --tcp --udp --ping --no-banner --versions --timeout usage() { - cat < + cat <<'EOF' +Usage: pos network checkport [target...] [options] -Check if a TCP port is open on a remote host. +Check port reachability with service/version metadata, inline per port. +Remote checks use nmap when available (recommended: real TCP/UDP scan + +version detection), otherwise fall back to bash /dev/tcp + nc. + +Targets: + :[/tcp|/udp] remote port (TCP default; UDP via /udp or --udp) + tcp://: explicit scheme (udp:// too) + [port...] host + port list (proto from --tcp/--udp) + [::1]:443 IPv6 + [/tcp|/udp] bare port = LOCAL check: which interfaces serve it + +Options: + -t, --tcp TCP (default) + -u, --udp UDP + --ping also ping the remote host first + --no-banner skip version detection / banner grab (faster) + --versions full nmap -sV version detection on open ports (slower) + --timeout per-check timeout in seconds (default 2; nmap host + timeout scales with it; use sudo for accurate UDP) + +Exit status: 0 if every TCP check was open (remote) or the port is served on +at least one interface (local); 1 otherwise. Examples: - pos network checkport 192.168.1.1:80 - pos network checkport 10.0.0.5:443 + pos network checkport 192.168.1.1:22 + pos network checkport 10.0.0.5 22 80 443 + pos network checkport 10.0.0.5:53/udp + pos network checkport 80 443 # local interfaces serving them + pos network checkport 22 --udp # local UDP bindings EOF exit 0 } -case "${1:-}" in - -h|--help|"") usage ;; -esac +err() { echo "ERROR: $*" >&2; exit 1; } -target="$1" +# --- port metadata (fallback when nmap reports no service) -------------------- +meta_name=""; meta_desc="" +port_meta() { + local port="$1" proto="$2" line="" + meta_name="unknown"; meta_desc="" + case "$port/$proto" in + 20/tcp) meta_name="ftp-data"; meta_desc="FTP data transfer" ;; + 21/tcp) meta_name="ftp"; meta_desc="FTP control" ;; + 22/tcp) meta_name="ssh"; meta_desc="SSH remote shell" ;; + 23/tcp) meta_name="telnet"; meta_desc="Telnet (unencrypted!)" ;; + 25/tcp) meta_name="smtp"; meta_desc="SMTP mail" ;; + 53/tcp|53/udp) meta_name="dns"; meta_desc="DNS" ;; + 67/udp|68/udp) meta_name="dhcp"; meta_desc="DHCP" ;; + 69/udp) meta_name="tftp"; meta_desc="TFTP (no auth!)" ;; + 80/tcp) meta_name="http"; meta_desc="HTTP web" ;; + 88/tcp|88/udp) meta_name="kerberos"; meta_desc="Kerberos" ;; + 110/tcp) meta_name="pop3"; meta_desc="POP3 mail" ;; + 111/tcp|111/udp) meta_name="rpcbind"; meta_desc="RPC portmapper" ;; + 123/udp) meta_name="ntp"; meta_desc="NTP time" ;; + 135/tcp) meta_name="msrpc"; meta_desc="MS-RPC endpoint mapper" ;; + 137/udp) meta_name="netbios-ns"; meta_desc="NetBIOS name service" ;; + 138/udp) meta_name="netbios-dgm"; meta_desc="NetBIOS datagram" ;; + 139/tcp) meta_name="netbios-ssn"; meta_desc="NetBIOS session" ;; + 143/tcp) meta_name="imap"; meta_desc="IMAP mail" ;; + 161/udp) meta_name="snmp"; meta_desc="SNMP" ;; + 179/tcp) meta_name="bgp"; meta_desc="BGP" ;; + 194/tcp) meta_name="irc"; meta_desc="IRC" ;; + 389/tcp) meta_name="ldap"; meta_desc="LDAP directory" ;; + 443/tcp) meta_name="https"; meta_desc="HTTPS web" ;; + 445/tcp) meta_name="smb"; meta_desc="SMB/CIFS file sharing" ;; + 465/tcp) meta_name="smtps"; meta_desc="SMTPS (implicit TLS)" ;; + 514/udp) meta_name="syslog"; meta_desc="Syslog" ;; + 548/tcp) meta_name="afp"; meta_desc="Apple Filing Protocol" ;; + 587/tcp) meta_name="submission"; meta_desc="SMTP submission" ;; + 631/tcp) meta_name="ipp"; meta_desc="IPP printing" ;; + 636/tcp) meta_name="ldaps"; meta_desc="LDAPS (TLS)" ;; + 873/tcp) meta_name="rsync"; meta_desc="rsync daemon" ;; + 993/tcp) meta_name="imaps"; meta_desc="IMAPS (TLS)" ;; + 995/tcp) meta_name="pop3s"; meta_desc="POP3S (TLS)" ;; + 1080/tcp) meta_name="socks"; meta_desc="SOCKS proxy" ;; + 1433/tcp) meta_name="mssql"; meta_desc="MS SQL Server" ;; + 1521/tcp) meta_name="oracle"; meta_desc="Oracle database" ;; + 1701/udp) meta_name="l2tp"; meta_desc="L2TP VPN" ;; + 1723/tcp) meta_name="pptp"; meta_desc="PPTP VPN" ;; + 1812/udp|1813/udp) meta_name="radius"; meta_desc="RADIUS auth" ;; + 2049/tcp|2049/udp) meta_name="nfs"; meta_desc="NFS" ;; + 2181/tcp) meta_name="zookeeper"; meta_desc="Apache ZooKeeper" ;; + 2375/tcp) meta_name="docker"; meta_desc="Docker API (UNENCRYPTED!)" ;; + 2376/tcp) meta_name="docker-tls"; meta_desc="Docker API (TLS)" ;; + 3000/tcp) meta_name="grafana"; meta_desc="Grafana / dev server" ;; + 3128/tcp) meta_name="squid"; meta_desc="Squid HTTP proxy" ;; + 3268/tcp|3269/tcp) meta_name="ldap-gc"; meta_desc="AD Global Catalog" ;; + 3306/tcp) meta_name="mysql"; meta_desc="MySQL/MariaDB" ;; + 3389/tcp) meta_name="rdp"; meta_desc="RDP remote desktop" ;; + 4369/tcp) meta_name="epmd"; meta_desc="Erlang port mapper" ;; + 5000/tcp) meta_name="upnp"; meta_desc="UPnP / registry" ;; + 5432/tcp) meta_name="postgres"; meta_desc="PostgreSQL" ;; + 5672/tcp) meta_name="amqp"; meta_desc="AMQP messaging" ;; + 5900/tcp) meta_name="vnc"; meta_desc="VNC remote desktop" ;; + 5985/tcp) meta_name="winrm"; meta_desc="WinRM (PowerShell remoting)" ;; + 6000/tcp) meta_name="x11"; meta_desc="X11 display" ;; + 6379/tcp) meta_name="redis"; meta_desc="Redis" ;; + 6443/tcp) meta_name="k8s-api"; meta_desc="Kubernetes API" ;; + 8006/tcp) meta_name="proxmox"; meta_desc="Proxmox web UI" ;; + 8080/tcp) meta_name="http-alt"; meta_desc="HTTP alternate" ;; + 8443/tcp) meta_name="https-alt"; meta_desc="HTTPS alternate" ;; + 8888/tcp) meta_name="http-alt2"; meta_desc="HTTP alternate" ;; + 9090/tcp) meta_name="prometheus"; meta_desc="Prometheus metrics" ;; + 9092/tcp) meta_name="kafka"; meta_desc="Apache Kafka" ;; + 9100/tcp) meta_name="printer"; meta_desc="network printer (raw)" ;; + 9200/tcp) meta_name="elasticsearch"; meta_desc="Elasticsearch" ;; + 9418/tcp) meta_name="git"; meta_desc="git daemon" ;; + 9997/tcp) meta_name="splunk"; meta_desc="Splunk" ;; + 11211/tcp) meta_name="memcached"; meta_desc="Memcached" ;; + 15672/tcp) meta_name="rabbitmq-mgmt"; meta_desc="RabbitMQ management" ;; + 25565/tcp) meta_name="minecraft"; meta_desc="Minecraft server" ;; + 27017/tcp) meta_name="mongodb"; meta_desc="MongoDB" ;; + 32400/tcp) meta_name="plex"; meta_desc="Plex media server" ;; + esac + if [ "$meta_name" = "unknown" ]; then + line="$(awk -v p="$port/$proto" 'BEGIN{IGNORECASE=1} $1 !~ /^#/ && $2 == p {print $1, $3; exit}' /etc/services 2>/dev/null || true)" + if [ -n "$line" ]; then set -- $line; meta_name="$1"; meta_desc="${2:-}"; fi + fi +} -if [[ "$target" != *:* ]]; then - echo "ERROR: Expected format, got '$target'" - echo "Usage: pos network checkport " - exit 1 -fi +# --- fallback engine (no nmap): bash /dev/tcp + nc --------------------------- +check_tcp() { + local ip="$1" port="$2" tmo="$3" rc="" + timeout "$tmo" bash -c "exec 3<>/dev/tcp/$ip/$port" 2>/dev/null + rc=$? + if [ "$rc" -eq 0 ]; then printf 'open' + elif [ "$rc" -eq 124 ]; then printf 'timeout' + else printf 'closed'; fi +} -ip="${target%:*}" -port="${target#*:}" +udp_payload() { + case "$1" in + 53) printf '\\x12\\x34\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x07example\\x03com\\x00\\x00\\x01\\x00\\x01' ;; + 123) printf '\\x1b'; printf '\\x00%.0s' {1..47} ;; + *) printf 'x' ;; + esac +} -if [[ -z "$ip" || -z "$port" ]]; then - echo "ERROR: Invalid target '$target'" - exit 1 -fi +check_udp() { + local ip="$1" port="$2" tmo="$3" out="" rc="" + if command -v nc >/dev/null 2>&1; then + out="$(printf '%b' "$(udp_payload "$port")" | timeout "$tmo" nc -u -w"$tmo" "$ip" "$port" 2>&1)" + rc=$? + if [ -n "$out" ]; then printf 'open' + elif printf '%s' "$out" | grep -qiE 'refus|unreachable'; then printf 'closed' + else printf 'no-reply'; fi + else + timeout "$tmo" bash -c "exec 3<>/dev/udp/$ip/$port; printf 'x' >&3" 2>/dev/null + printf 'no-reply' + fi +} -echo "Checking $ip:$port ..." -echo +probe() { + local ip="$1" port="$2" tmo="$3" + case "$port" in + 80|8000|8080|8081|8888|3000|5000|9090|3128|10000) + timeout "$tmo" bash -c "exec 3<>/dev/tcp/$ip/$port; printf 'HEAD / HTTP/1.0\r\n\r\n' >&3; head -c 300 <&3" 2>/dev/null ;; + *) + timeout "$tmo" bash -c "exec 3<>/dev/tcp/$ip/$port; head -c 200 <&3" 2>/dev/null ;; + esac +} -if timeout 2 bash -c "cat < /dev/null > /dev/tcp/$ip/$port" 2>/dev/null; then - echo "OPEN ✔ $ip:$port" +format_banner() { + local first="" server="" + first="$(printf '%s' "$1" | head -n1 | tr -cd '[:print:] ' | tr -s ' ' | head -c 100)" + server="$(printf '%s' "$1" | grep -i '^server:' | head -n1 | tr -cd '[:print:] ' | tr -s ' ' | head -c 60)" + if [ -n "$server" ]; then printf '%s | %s' "$first" "$server"; else printf '%s' "$first"; fi +} + +# --- local mode: which interfaces serve a port -------------------------------- +find_listener() { + local lines="$1" ip="$2" port="$3" line="" la="" exact="" cand="" cands=() + cands=("$ip:$port" "0.0.0.0:$port" "*:$port" "[::]:$port") + [[ "$ip" == *:* ]] && cands+=("[$ip]:$port") + while IFS= read -r line; do + [ -n "$line" ] || continue + la="$(awk '{print $4}' <<< "$line")" + for cand in "${cands[@]}"; do + if [ "$la" = "$cand" ]; then + if [ "$la" = "$ip:$port" ] || [ "$la" = "[$ip]:$port" ]; then printf '%s' "$line"; return 0; fi + [ -z "$exact" ] && exact="$line" + fi + done + done <<< "$lines" + printf '%s' "$exact" +} + +listener_proc() { + sed -n 's/.*users:(("\([^"]*\)",pid=\([0-9]*\).*/\1 (pid \2)/p' <<< "$1" +} + +local_addrs() { + { ip -o -4 addr show; ip -o -6 addr show; } 2>/dev/null | + awk '{d=$2; sub(/:$/,"",d); a=$4; sub(/\/.*/,"",a); if (a !~ /^fe80:/) print d" "a}' +} + +local_check() { + local port="$1" proto="$2" addrs="" slines="" dev="" ip="" la="" state="" proc="" label="" + local banner_txt="" banner_done=0 + port_meta "$port" "$proto" + printf '\n %s/%s — %s%s\n' "$port" "$proto" "$meta_name" "${meta_desc:+ ($meta_desc)}" + addrs="$(local_addrs)" + [ -n "$addrs" ] || err "could not enumerate local interfaces (ip command?)" + if [ "$proto" = "tcp" ]; then + slines="$(ss -tlnpH "sport = :$port" 2>/dev/null || true)" + else + slines="$(ss -ulnp "sport = :$port" 2>/dev/null || true)" + fi + while read -r dev ip; do + la="$(find_listener "$slines" "$ip" "$port")" + if [ "$proto" = "tcp" ]; then + state="$(check_tcp "$ip" "$port" "$tmo")" + else + [ -n "$la" ] && state="bound" || state="not-bound" + fi + [ -n "$la" ] && proc="$(listener_proc "$la")" || proc="" + if [ "$state" = "open" ] && [ "$banner" -eq 1 ] && [ "$banner_done" -eq 0 ]; then + banner_txt="$(format_banner "$(probe "$ip" "$port" "$tmo")")"; banner_done=1 + fi + case "$state" in + open) label="OPEN" ;; + bound) label="bound" ;; + closed) label="CLOSED" ;; + timeout) label="no reply (filtered?)" ;; + no-reply) label="no reply" ;; + not-bound) label="not bound" ;; + esac + printf ' %-10s %-26s %-14s %s\n' "$dev" "$ip:$port" "$label" "$proc" + done <<< "$addrs" + if [ -z "$slines" ]; then + printf ' (no listener for this port on any interface)\n' + rc=1 + else + [ -n "$banner_txt" ] && printf ' banner: %s\n' "$banner_txt" + fi + printf ' %s/%s: %d interface(s) checked\n' "$port" "$proto" "$(wc -l <<< "$addrs")" +} + +# --- remote mode -------------------------------------------------------------- +ping_host() { + local h="$1" + if ping -c 1 -W 1 "$h" >/dev/null 2>&1; then + printf ' host %-26s up (ICMP reply)\n' "$h" + else + printf ' host %-26s no ICMP reply (may still serve ports)\n' "$h" + fi +} + +state_label() { + case "$1" in + open) printf 'OPEN' ;; + closed) printf 'CLOSED' ;; + filtered) printf 'FILTERED' ;; + "open|filtered") printf 'open|filtered' ;; + timeout) printf 'no reply (filtered?)' ;; + no-reply) printf 'no reply (open|filtered)' ;; + *) printf '%s' "$1" ;; + esac +} + +# nmap engine: pass 1 = fast states + service names; pass 2 (--versions) = +# full version detection on open ports only; fast banner probe fills gaps. +# UDP needs root for nmap (raw ICMP) — unprivileged UDP falls back to nc. +nmap_check() { + local host="$1"; shift + local pairs=("$@") port="" proto="" nmapports="" out="" vout="" vmap="" + local tports=() uports=() openports="" line="" state="" service="" version="" + local styp=() disp="" pp="" vline="" isv6=0 root=0 hasudp=0 + [[ "$host" == *:* ]] && isv6=1 + [ "$(id -u)" -eq 0 ] && root=1 + for pair in "${pairs[@]}"; do + port="${pair%%:*}"; proto="${pair##*:}" + if [ "$proto" = "tcp" ]; then tports+=("$port"); else uports+=("$port"); fi + done + # unprivileged UDP → nc fallback per port (nmap -sU needs root) + if [ "$root" -eq 0 ] && [ ${#uports[@]} -gt 0 ]; then + for pair in "${pairs[@]}"; do + port="${pair%%:*}"; proto="${pair##*:}" + [ "$proto" = "udp" ] && fallback_check "$host" "$port" "udp" + done + fi + # ports that nmap will scan (tcp always; udp only when root) + for pair in "${pairs[@]}"; do + port="${pair%%:*}"; proto="${pair##*:}" + if [ "$proto" = "udp" ] && [ "$root" -eq 0 ]; then continue; fi + [ -n "$nmapports" ] && nmapports="$nmapports," + nmapports="$nmapports$port" + [ "$proto" = "udp" ] && hasudp=1 + done + [ -n "$nmapports" ] || return 0 + styp=(-sT); [ "$hasudp" -eq 1 ] && styp+=(-sU) + [ ${#tports[@]} -eq 0 ] && styp=(-sU) + [ "$isv6" -eq 1 ] && styp+=(-6) + printf ' %s (via nmap)\n' "$host" + # pass 1: fast states + service names + out="$(timeout "$((tmo*4+15))" nmap -Pn -T4 --max-retries 1 -p "$nmapports" "${styp[@]}" --host-timeout "$((tmo*4))s" "$host" 2>/dev/null)" || true + # pass 2 (opt-in): version detection on open ports only + if [ "$versions" -eq 1 ]; then + openports="" + for pair in "${pairs[@]}"; do + port="${pair%%:*}"; proto="${pair##*:}" + if [ "$proto" = "udp" ] && [ "$root" -eq 0 ]; then continue; fi + pp="$port/$proto" + state="$(printf '%s\n' "$out" | awk -v p="$pp" '$1==p {print $2; exit}')" + if [ "$state" = "open" ]; then + [ -n "$openports" ] && openports="$openports," + openports="$openports$port" + fi + done + if [ -n "$openports" ]; then + # version probing is slow on unresponsive services; give it room + vout="$(timeout "$((tmo*10+100))" nmap -Pn -T4 --max-retries 1 -p "$openports" "${styp[@]}" -sV --version-light --host-timeout "$((tmo*10+90))s" "$host" 2>/dev/null)" || true + while IFS= read -r vline; do + [ -n "$vline" ] || continue + vmap="$vmap$(awk '{print $1"|"$0}' <<< "$vline")"$'\n' + done <<< "$(printf '%s\n' "$vout" | grep -E '^[0-9]+/(tcp|udp)')" + fi + fi + # display + for pair in "${pairs[@]}"; do + port="${pair%%:*}"; proto="${pair##*:}" + if [ "$proto" = "udp" ] && [ "$root" -eq 0 ]; then continue; fi + pp="$port/$proto" + line="$(printf '%s\n' "$out" | awk -v p="$pp" '$1==p {print; exit}')" + if [ -n "$line" ]; then + state="$(awk '{print $2}' <<< "$line")" + service="$(awk '{print $3}' <<< "$line")" + else + state="no output"; service="" + fi + version="" + if [ -n "$vmap" ]; then + # strip "port/proto state service" (arbitrary padding) via variable sub + version="$(printf '%s' "$vmap" | awk -F'|' -v p="$pp" '$1==p {v=$2; sub(/^[^ ]+ +[^ ]+ +[^ ]+( +|$)/,"",v); print v; exit}')" + fi + if [ "$service" = "unknown" ] || [ -z "$service" ]; then + port_meta "$port" "$proto" + service="$meta_name"; [ -n "$meta_desc" ] && service="$service ($meta_desc)" + fi + if [ -z "$version" ] && [ "$proto" = "tcp" ] && [ "$state" = "open" ] && [ "$banner" -eq 1 ]; then + version="$(format_banner "$(probe "$host" "$port" "$tmo")")" + [ -n "$version" ] || version="(no banner)" + fi + case "$state" in + open) total_open=$((total_open+1)) ;; + closed) rc=1; total_closed=$((total_closed+1)) ;; + filtered|"open|filtered") rc=1 ;; + *) rc=1 ;; + esac + disp="$(state_label "$state")" + if [ -n "$version" ]; then + printf ' %-30s %-14s %s %s\n' "${host}:${port}/${proto}" "$disp" "$service" "$version" + else + printf ' %-30s %-14s %s\n' "${host}:${port}/${proto}" "$disp" "$service" + fi + done +} + +# fallback engine (no nmap): per-port bash/nc checks +fallback_check() { + local host="$1" port="$2" proto="$3" state="" banner_txt="" disp="" + port_meta "$port" "$proto" + if [ "$proto" = "tcp" ]; then + state="$(check_tcp "$host" "$port" "$tmo")" + if [ "$banner" -eq 1 ] && [ "$state" = "open" ]; then + banner_txt="$(format_banner "$(probe "$host" "$port" "$tmo")")" + [ -n "$banner_txt" ] || banner_txt="(no banner)" + fi + else + state="$(check_udp "$host" "$port" "$tmo")" + case "$port" in + 53) banner_txt="probe: dns query" ;; + 123) banner_txt="probe: ntp request" ;; + *) banner_txt="" ;; + esac + fi + case "$state" in + open) total_open=$((total_open+1)) ;; + closed) rc=1; total_closed=$((total_closed+1)) ;; + timeout|no-reply) rc=1 ;; + esac + disp="$(state_label "$state")" + local metadisp="$meta_name"; [ -n "$meta_desc" ] && metadisp="$metadisp ($meta_desc)" + if [ "$proto" = "tcp" ] || [ -n "$banner_txt" ]; then + printf ' %-30s %-14s %s %s\n' "${host}:${port}/${proto}" "$disp" "$metadisp" "${banner_txt:-}" + else + printf ' %-30s %-14s %s\n' "${host}:${port}/${proto}" "$disp" "$metadisp" + fi +} + +# group targets by host, then run one engine pass per host +remote_group() { + local reqs=("$@") req="" host="" rest="" port="" proto="" seen_hosts="" h="" pairs=() + # unique hosts, preserving order + for req in "${reqs[@]}"; do + host="${req%%|*}" + case " $seen_hosts " in *" $host "*) ;; *) seen_hosts="$seen_hosts $host" ;; esac + done + for h in $seen_hosts; do + pairs=() + for req in "${reqs[@]}"; do + host="${req%%|*}"; rest="${req#*|}" + port="${rest%%|*}"; proto="${rest##*|}" + if [ "$host" = "$h" ]; then + pairs+=("${port}:${proto}") + fi + done + if command -v nmap >/dev/null 2>&1; then + nmap_check "$h" "${pairs[@]}" + else + for port in "${pairs[@]}"; do + fallback_check "$h" "${port%%:*}" "${port##*:}" + done + fi + done +} + +# --- main --------------------------------------------------------------------- +mode="tcp"; do_ping=0; banner=1; tmo=2; versions=0 +targets=() +while [ $# -gt 0 ]; do + case "$1" in + -t|--tcp) mode="tcp"; shift ;; + -u|--udp) mode="udp"; shift ;; + --ping) do_ping=1; shift ;; + --no-banner) banner=0; shift ;; + --versions) versions=1; shift ;; + --timeout) [ $# -ge 2 ] || err "--timeout needs a value"; tmo="$2"; shift 2 ;; + -h|--help) usage ;; + -*) err "unknown option '$1'" ;; + *) targets+=("$1"); shift ;; + esac +done +[ ${#targets[@]} -gt 0 ] || usage +[[ "$tmo" =~ ^[0-9]+$ ]] && [ "$tmo" -ge 1 ] || err "--timeout must be a number >= 1" + +rc=0; total_open=0; total_closed=0 + +if [[ "${targets[0]}" =~ ^[0-9]+(/[a-z]+)?$ ]]; then + # bare port → local interface check + for t in "${targets[@]}"; do + port="$t"; proto="$mode" + if [[ "$t" != ^[0-9]+$ && "$t" == */* ]]; then port="${t%%/*}"; proto="${t##*/}"; fi + [[ "$port" =~ ^[0-9]{1,5}$ ]] || err "bad port '$t'" + [ "$port" -ge 1 ] && [ "$port" -le 65535 ] || err "port out of range '$t'" + [ "$proto" = "tcp" ] || [ "$proto" = "udp" ] || err "bad protocol '$t'" + local_check "$port" "$proto" + done else - echo "CLOSED ✖ $ip:$port" + reqs=() + if [[ "${targets[0]}" != *:* ]]; then + # host + port list + host="${targets[0]}" + [ ${#targets[@]} -gt 1 ] || err "host form needs at least one port: $host [port...]" + [ "$do_ping" -eq 1 ] && ping_host "$host" + for p in "${targets[@]:1}"; do + proto="$mode" + case "$p" in */*) proto="${p##*/}"; p="${p%%/*}" ;; esac + [[ "$p" =~ ^[0-9]{1,5}$ ]] || err "bad port '$p'" + [ "$p" -ge 1 ] && [ "$p" -le 65535 ] || err "port out of range '$p'" + reqs+=("$host|$p|$proto") + done + else + # full targets + for t in "${targets[@]}"; do + proto="$mode" + case "$t" in + tcp://*) proto="tcp"; t="${t#tcp://}" ;; + udp://*) proto="udp"; t="${t#udp://}" ;; + esac + port="${t##*:}"; host="${t%:*}" + host="${host#[[]}"; host="${host%]}" + case "$port" in */*) proto="${port#*/}"; port="${port%/*}" ;; esac + [ "$proto" = "tcp" ] || [ "$proto" = "udp" ] || err "bad protocol in '$t' (use /tcp or /udp)" + [[ "$port" =~ ^[0-9]{1,5}$ ]] || err "bad port in '$t'" + [ "$port" -ge 1 ] && [ "$port" -le 65535 ] || err "port out of range '$t'" + [ -n "$host" ] && [[ "$host" != *" "* ]] || err "bad host in '$t'" + reqs+=("$host|$port|$proto") + done + if [ "$do_ping" -eq 1 ]; then + for req in "${reqs[@]}"; do ping_host "${req%%|*}"; done + fi + fi + remote_group "${reqs[@]}" fi + +exit $rc diff --git a/completions/pos.bash b/completions/pos.bash index 86a5659..4a49322 100644 --- a/completions/pos.bash +++ b/completions/pos.bash @@ -10,6 +10,7 @@ _pos_flags[communication-telegram-sender]="--type --caption --parse-mode --no-pr _pos_flags[entertainment-send]="--print --markdown" _pos_flags[media-mp3]="--output --no-playlist --cookies --by-artist --dry-run" _pos_flags[media-mp4]="--format --best --worst --output --no-playlist --cookies --dry-run" +_pos_flags[network-checkport]="--tcp --udp --ping --no-banner --versions --timeout" _pos_flags[network-hotspot]="--foreground" _pos_flags[share-usb-server]="--ls --ls-shared --share --unshare --auto-share --callback --close-callback --auto-connect --disconnect --nickname --timeout --port --info --version" _pos_flags[system-backup]="--service"