#!/usr/bin/env bash # portcheck.sh - is a TCP port reachable from the internet? # # Usage: ./portcheck.sh [port] # Exit : 0 = OPEN (externally reachable), 1 = CLOSED/FILTERED, 2 = inconclusive # # One external endpoint (check-host.net), TCP only, short timeouts. set -uo pipefail PORT="${1:-8899}" TIMEOUT=3 # local connect timeout, seconds MAX_WAIT=45 # max wait for external nodes, seconds # ---- dependencies ---------------------------------------------------------- for c in curl jq; do command -v "$c" >/dev/null 2>&1 || { echo "Missing dependency: $c"; exit 2; } done # ---- public IP ------------------------------------------------------------- IP=$(curl -s --max-time 6 'https://api.ipify.org' \ || curl -s --max-time 6 'https://ifconfig.me/ip' \ || curl -s --max-time 6 'https://icanhazip.com') [[ -z "$IP" ]] && { echo "Cannot determine public IP"; exit 2; } echo "IP : $IP" echo "Port : $PORT/tcp" # ---- local probe (fast, tells us if hairpin works) ------------------------- if command -v nc >/dev/null 2>&1; then R=$(timeout $((TIMEOUT+2)) nc -zv -w "$TIMEOUT" "$IP" "$PORT" 2>&1) if grep -qi succeeded <<<"$R"; then echo "Local : reachable (hairpin OK)" elif grep -qi refused <<<"$R"; then echo "Local : refused" else echo "Local : timeout/filtered"; fi fi # ---- external probe (the real test) ---------------------------------------- RID=$(curl -sS --max-time 10 -H 'Accept: application/json' \ "https://check-host.net/check-tcp?host=${IP}:${PORT}&max_nodes=3" \ | jq -r '.request_id // empty') [[ -z "$RID" ]] && { echo "RESULT: INCONCLUSIVE (probe service unavailable)"; exit 2; } WAIT=0 while (( WAIT < MAX_WAIT )); do sleep 3; WAIT=$((WAIT+3)) OUT=$(curl -sS --max-time 10 -H 'Accept: application/json' \ "https://check-host.net/check-result/${RID}") [[ -z "$OUT" ]] && continue OPEN=$(jq -r '[to_entries[] | select(.value[0].time != null)] | length' <<<"$OUT" 2>/dev/null || echo 0) FAIL=$(jq -r '[to_entries[] | select(.value[0].error != null or .value[0].timeout != null)] | length' <<<"$OUT" 2>/dev/null || echo 0) PEND=$(jq -r '[to_entries[] | select(.value[0] == null)] | length' <<<"$OUT" 2>/dev/null || echo 0) # A single node connecting proves the port is open - stop early. (( OPEN >= 1 )) && break # All nodes reported a failure - stop early. (( PEND == 0 )) && break done echo "Nodes: open=$OPEN failed=$FAIL pending=$PEND" if (( OPEN >= 2 )); then echo "RESULT: OPEN - port $PORT IS externally reachable" elif (( OPEN == 1 )); then echo "RESULT: OPEN (single node) - port $PORT IS externally reachable" elif (( FAIL >= 1 )); then echo "RESULT: CLOSED/FILTERED - port $PORT is NOT externally reachable" else echo "RESULT: INCONCLUSIVE"; exit 2 fi (( OPEN >= 1 )) && exit 0 || exit 1