feat: pos network checkport nmap engine overhaul — 2-pass scan, --versions probe, udp/v6 fixes
This commit is contained in:
@@ -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 <value> [--markdown]` contract via `notify_sender_name()`'s default key→tool mapping, no lib changes). **Sender** (`bin/pos-communication-matrix-sender`): `send <value> [--markdown] [--room <id|alias>]` 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-<room>`, `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: `["<msg>" if ] <check-command> <op> <threshold>` (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.
|
||||
|
||||
@@ -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 |
|
||||
<!-- GEN:END filetable -->
|
||||
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
|
||||
|
||||
|
||||
+477
-26
@@ -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 <<EOF
|
||||
Usage: pos network checkport <ip:port>
|
||||
cat <<'EOF'
|
||||
Usage: pos network checkport <target> [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:
|
||||
<ip|host>:<port>[/tcp|/udp] remote port (TCP default; UDP via /udp or --udp)
|
||||
tcp://<ip|host>:<port> explicit scheme (udp:// too)
|
||||
<ip|host> <port> [port...] host + port list (proto from --tcp/--udp)
|
||||
[::1]:443 IPv6
|
||||
<port>[/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 <s> 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 <ip:port> format, got '$target'"
|
||||
echo "Usage: pos network checkport <ip:port>"
|
||||
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> [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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user