#!/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 -------------------------------------------------------------
[[ -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' \
| 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' \
[[ -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