1. #!/usr/bin/env bash
  2. # portcheck.sh - is a TCP port reachable from the internet?
  3. #
  4. # Usage: ./portcheck.sh [port]
  5. # Exit : 0 = OPEN (externally reachable), 1 = CLOSED/FILTERED, 2 = inconclusive
  6. #
  7. # One external endpoint (check-host.net), TCP only, short timeouts.
  8. set -uo pipefail
  9.  
  10. PORT="${1:-8899}"
  11. TIMEOUT=3 # local connect timeout, seconds
  12. MAX_WAIT=45 # max wait for external nodes, seconds
  13.  
  14. # ---- dependencies ----------------------------------------------------------
  15. for c in curl jq; do
  16. command -v "$c" >/dev/null 2>&1 || { echo "Missing dependency: $c"; exit 2; }
  17. done
  18.  
  19. # ---- public IP -------------------------------------------------------------
  20. IP=$(curl -s --max-time 6 'https://api.ipify.org' \
  21. || curl -s --max-time 6 'https://ifconfig.me/ip' \
  22. || curl -s --max-time 6 'https://icanhazip.com')
  23. [[ -z "$IP" ]] && { echo "Cannot determine public IP"; exit 2; }
  24.  
  25. echo "IP : $IP"
  26. echo "Port : $PORT/tcp"
  27.  
  28. # ---- local probe (fast, tells us if hairpin works) -------------------------
  29. if command -v nc >/dev/null 2>&1; then
  30. R=$(timeout $((TIMEOUT+2)) nc -zv -w "$TIMEOUT" "$IP" "$PORT" 2>&1)
  31. if grep -qi succeeded <<<"$R"; then echo "Local : reachable (hairpin OK)"
  32. elif grep -qi refused <<<"$R"; then echo "Local : refused"
  33. else echo "Local : timeout/filtered"; fi
  34. fi
  35.  
  36. # ---- external probe (the real test) ----------------------------------------
  37. RID=$(curl -sS --max-time 10 -H 'Accept: application/json' \
  38. | jq -r '.request_id // empty')
  39. [[ -z "$RID" ]] && { echo "RESULT: INCONCLUSIVE (probe service unavailable)"; exit 2; }
  40.  
  41. WAIT=0
  42. while (( WAIT < MAX_WAIT )); do
  43. sleep 3; WAIT=$((WAIT+3))
  44. OUT=$(curl -sS --max-time 10 -H 'Accept: application/json' \
  45. [[ -z "$OUT" ]] && continue
  46.  
  47. OPEN=$(jq -r '[to_entries[] | select(.value[0].time != null)] | length' <<<"$OUT" 2>/dev/null || echo 0)
  48. FAIL=$(jq -r '[to_entries[] | select(.value[0].error != null or .value[0].timeout != null)] | length' <<<"$OUT" 2>/dev/null || echo 0)
  49. PEND=$(jq -r '[to_entries[] | select(.value[0] == null)] | length' <<<"$OUT" 2>/dev/null || echo 0)
  50.  
  51. # A single node connecting proves the port is open - stop early.
  52. (( OPEN >= 1 )) && break
  53. # All nodes reported a failure - stop early.
  54. (( PEND == 0 )) && break
  55. done
  56.  
  57. echo "Nodes: open=$OPEN failed=$FAIL pending=$PEND"
  58.  
  59. if (( OPEN >= 2 )); then echo "RESULT: OPEN - port $PORT IS externally reachable"
  60. elif (( OPEN == 1 )); then echo "RESULT: OPEN (single node) - port $PORT IS externally reachable"
  61. elif (( FAIL >= 1 )); then echo "RESULT: CLOSED/FILTERED - port $PORT is NOT externally reachable"
  62. else echo "RESULT: INCONCLUSIVE"; exit 2
  63. fi
  64.  
  65. (( OPEN >= 1 )) && exit 0 || exit 1