#!/usr/bin/env bash # net-ops :: reverse-probe.sh # Diagnose a TARGET host from OUTSIDE — useful when the local probe on the # target says "all good" but external services / users still report problems. # Runs from this machine against a target host you can reach (LAN, tailnet, # public IP, etc). # # Usage: # scripts/reverse-probe.sh # use default ports/checks # scripts/reverse-probe.sh [port...] # add custom TCP ports to probe # # Examples: # scripts/reverse-probe.sh example.local # scripts/reverse-probe.sh 100.84.X.X 8080 5432 # scripts/reverse-probe.sh api.mycompany.com 443 set -u TARGET="${1:-}" if [[ -z "$TARGET" ]]; then echo "Usage: $0 [extra_tcp_port ...]" >&2 exit 1 fi shift EXTRA_PORTS=("$@") DEFAULT_PORTS=(22 80 443) TIMEOUT=4 # shellcheck source=_lib/redact.sh source "$(dirname "$0")/_lib/redact.sh" # shellcheck source=_lib/output.sh source "$(dirname "$0")/_lib/output.sh" parse_redact_flag "$@" parse_output_flags "$@" maybe_redact_self "$TARGET" "$@" # Resolve target — separates DNS issues from reachability issues section "1. NAME RESOLUTION FROM HERE" if [[ "$TARGET" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then pass "Target is literal IP" "$TARGET" TARGET_IP="$TARGET" else resolved=$(dig +short +time=3 +tries=1 "$TARGET" 2>/dev/null | head -1) if [[ -n "$resolved" ]]; then pass "Resolved $TARGET (dig, bypass resolver)" "$resolved" TARGET_IP="$resolved" else fail "Resolved $TARGET" "no answer from local DNS — can't proceed past name layer" emit_summary exit 1 fi fi section "2. ICMP REACHABILITY" if ping -c 2 -W $((TIMEOUT * 1000)) "$TARGET_IP" >/dev/null 2>&1; then pass "Ping $TARGET_IP" else fail "Ping $TARGET_IP" "no ICMP response (or ICMP filtered)" fi section "3. TCP PORT REACHABILITY" # De-duplicate ports — extras may overlap defaults all_ports=$(printf '%s\n' "${DEFAULT_PORTS[@]}" ${EXTRA_PORTS[@]+"${EXTRA_PORTS[@]}"} | awk '!seen[$0]++') while read -r port; do [[ -z "$port" ]] && continue if nc -zv -G "$TIMEOUT" "$TARGET_IP" "$port" >/dev/null 2>&1; then pass "TCP/$port -> $TARGET_IP" "open" else fail "TCP/$port -> $TARGET_IP" "closed or filtered" fi done <<< "$all_ports" section "4. TLS / HTTPS HEALTH (if 443 open)" if nc -zv -G "$TIMEOUT" "$TARGET_IP" 443 >/dev/null 2>&1; then if [[ "$TARGET" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then # Connect by IP; cert check will fail SNI but we can still probe out=$(curl -sS -o /dev/null -w "%{http_code}|%{time_total}" --max-time "$TIMEOUT" -k "https://$TARGET_IP" 2>&1) pass "HTTPS to IP (cert SNI may not match)" "$out" else out=$(curl -sS -o /dev/null -w "%{http_code}|%{time_total}" --max-time "$TIMEOUT" "https://$TARGET" 2>&1) if [[ "$out" =~ ^[0-9]+\|[0-9.]+$ ]]; then pass "HTTPS to $TARGET" "$out" else fail "HTTPS to $TARGET" "$out" fi fi fi section "5. PATH / ROUTING" case "$(uname -s)" in Darwin) # macOS traceroute: -w timeout (sec), -m max hops, -q probes per hop info " traceroute (first 8 hops):" traceroute -n -w 2 -q 1 -m 8 "$TARGET_IP" 2>/dev/null | head -10 | sed 's/^/ /' || true ;; Linux) if command -v traceroute >/dev/null 2>&1; then info " traceroute (first 8 hops):" traceroute -n -w 2 -q 1 -m 8 "$TARGET_IP" 2>/dev/null | head -10 | sed 's/^/ /' || true elif command -v mtr >/dev/null 2>&1; then info " mtr report (5 cycles):" mtr -nrc 5 "$TARGET_IP" 2>/dev/null | tail -10 | sed 's/^/ /' || true fi ;; esac emit_summary