#!/bin/bash # Forced command for a dedicated SSH key entry on CT122's OWN root user (see # docs/device-discovery.md). Unlike the generic collector forced-commands on # omv/ripper, this one is parameterized: forced commands ignore whatever the # client literally requested, but OpenSSH still exposes it via # $SSH_ORIGINAL_COMMAND, which this script reads and validates strictly # before using -- never passed to a shell for interpretation. # # Runs a handful of *targeted, on-demand* active probes against a single LAN # IP (admin-triggered from the dashboard, not automatic/scheduled): mDNS # reverse resolve, SSDP/UPnP query (many smart-home devices announce a # friendlyName/manufacturer this way), a small curated TCP port scan, and an # HTTP title/server grab on anything open. Bounded timeouts throughout so a # single check finishes in well under 15s. set -euo pipefail IP="${SSH_ORIGINAL_COMMAND:-}" if ! [[ "$IP" =~ ^192\.168\.1\.([0-9]{1,3})$ ]]; then echo '{"error":"invalid or missing target IP"}' exit 1 fi OCTET="${BASH_REMATCH[1]}" if (( OCTET < 1 || OCTET > 254 )); then echo '{"error":"target IP out of range"}' exit 1 fi MDNS=$(timeout 2 avahi-resolve -a "$IP" 2>/dev/null | awk '{print $2}' || true) RDNS=$(timeout 2 getent hosts "$IP" 2>/dev/null | awk '{print $2}' | head -1 || true) SSDP_LOCATION=$(timeout 4 python3 - "$IP" <<'PYEOF' || true import socket, sys, time target = sys.argv[1] msg = "\r\n".join([ "M-SEARCH * HTTP/1.1", "HOST: 239.255.255.250:1900", 'MAN: "ssdp:discover"', "MX: 3", "ST: ssdp:all", "", "" ]).encode() sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.settimeout(3) sock.sendto(msg, ("239.255.255.250", 1900)) start = time.time() while time.time() - start < 3: try: data, addr = sock.recvfrom(4096) if addr[0] == target: for line in data.decode(errors="replace").split("\r\n"): if line.lower().startswith("location:"): print(line.split(":", 1)[1].strip()) sys.exit(0) except socket.timeout: break PYEOF ) SSDP_FRIENDLY="" SSDP_MANUFACTURER="" SSDP_MODEL="" if [[ -n "$SSDP_LOCATION" ]]; then XML=$(timeout 3 curl -s -m 2 "$SSDP_LOCATION" 2>/dev/null || true) SSDP_FRIENDLY=$(echo "$XML" | grep -o -m1 '[^<]*' | sed 's///' || true) SSDP_MANUFACTURER=$(echo "$XML" | grep -o -m1 '[^<]*' | sed 's///' || true) SSDP_MODEL=$(echo "$XML" | grep -o -m1 '[^<]*' | sed 's///' || true) fi PORTS=(21 22 23 80 443 554 5000 8000 8008 8009 8060 8080 8443 9100 32400 62078) OPEN_PORTS_DIR="$(mktemp -d)" trap 'rm -rf "$OPEN_PORTS_DIR"' EXIT for p in "${PORTS[@]}"; do ( timeout 1 bash -c "echo >/dev/tcp/$IP/$p" 2>/dev/null && touch "$OPEN_PORTS_DIR/$p" ) & done wait open_ports=() for p in "${PORTS[@]}"; do [[ -f "$OPEN_PORTS_DIR/$p" ]] && open_ports+=("$p") done http_titles=() for p in "${open_ports[@]}"; do case "$p" in 80|8000|8008|8080|8060|5000) scheme="http" ;; 443|8443) scheme="https" ;; *) continue ;; esac title=$(timeout 3 curl -sk -m 2 "$scheme://$IP:$p/" 2>/dev/null | grep -o -m1 -i '[^<]*' | sed -E 's/<title>//i' || true) server=$(timeout 3 curl -sIk -m 2 "$scheme://$IP:$p/" 2>/dev/null | grep -i '^server:' | head -1 | cut -d: -f2- | tr -d '\r' | sed 's/^ *//' || true) http_titles+=("$p|$title|$server") done python3 - "$IP" "$MDNS" "$RDNS" "$SSDP_LOCATION" "$SSDP_FRIENDLY" "$SSDP_MANUFACTURER" "$SSDP_MODEL" \ "$(IFS=,; echo "${open_ports[*]}")" "$(printf '%s\n' "${http_titles[@]}")" <<'PYEOF' import json, sys ip, mdns, rdns, ssdp_loc, ssdp_friendly, ssdp_mfr, ssdp_model, ports_csv, http_raw = sys.argv[1:10] http = [] for line in http_raw.splitlines(): if not line: continue port, title, server = (line.split("|", 2) + ["", ""])[:3] http.append({ "port": int(port), "title": title or None, "server": server or None, }) result = { "ip": ip, "mdnsHostname": mdns or None, "reverseDns": rdns or None, "ssdp": { "location": ssdp_loc or None, "friendlyName": ssdp_friendly or None, "manufacturer": ssdp_mfr or None, "modelName": ssdp_model or None, } if ssdp_loc else None, "openPorts": [int(p) for p in ports_csv.split(",") if p], "http": http, } print(json.dumps(result)) PYEOF