Files
fips_playground/test_qube_pair.sh
2026-05-08 10:54:03 -04:00

176 lines
5.0 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Simple inter-qube FIPS connectivity test + optional direct connect bootstrap.
# Usage:
# ./test_qube_pair.sh <remote_npub> [remote_underlay_addr:port]
# Example:
# ./test_qube_pair.sh npub1... 10.137.0.22:2121
#
# Optional env vars:
# REMOTE_HTTP_PORT=80 # target port for HTTP validation over .fips
# CURL_TIMEOUT=10
# MIN_BYTES=200
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <remote_npub> [remote_underlay_addr:port]" >&2
exit 1
fi
REMOTE_NPUB="$1"
REMOTE_ADDR="${2:-}"
REMOTE_FQDN="${REMOTE_NPUB}.fips"
CURL_TIMEOUT="${CURL_TIMEOUT:-10}"
MIN_BYTES="${MIN_BYTES:-200}"
REMOTE_HTTP_PORT="${REMOTE_HTTP_PORT:-80}"
REMOTE_URL="http://${REMOTE_FQDN}"
if [[ "${REMOTE_HTTP_PORT}" != "80" ]]; then
REMOTE_URL="http://${REMOTE_FQDN}:${REMOTE_HTTP_PORT}/"
else
REMOTE_URL="http://${REMOTE_FQDN}/"
fi
need_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Error: required command not found: $1" >&2
exit 1
fi
}
need_cmd curl
need_cmd python3
need_cmd getent
need_cmd fipsctl
need_cmd ip
echo "==> Local node"
STATUS_JSON="$(sudo fipsctl show status)"
python3 - "${STATUS_JSON}" <<'PY'
import json
import sys
s = json.loads(sys.argv[1])
print(f"npub={s.get('npub')}")
print(f"ipv6={s.get('ipv6_addr')}")
print(f"state={s.get('state')}")
print(f"peer_count={s.get('peer_count')}")
print(f"session_count={s.get('session_count')}")
PY
echo ""
echo "==> Local TUN/route preflight"
TUN_STATE="$(python3 - "${STATUS_JSON}" <<'PY'
import json
import sys
s = json.loads(sys.argv[1])
print(s.get("tun_state", "n/a"))
PY
)"
TUN_NAME="$(python3 - "${STATUS_JSON}" <<'PY'
import json
import sys
s = json.loads(sys.argv[1])
print(s.get("tun_name") or "fips0")
PY
)"
if [[ "${TUN_STATE}" != "active" ]]; then
echo "Error: local tun_state is ${TUN_STATE} (expected active)." >&2
exit 1
fi
if ! ip -o link show dev "${TUN_NAME}" >/dev/null 2>&1; then
echo "Error: local TUN interface ${TUN_NAME} is missing." >&2
exit 1
fi
FD00_ROUTE_LINE="$(ip -6 route show fd00::/8 2>/dev/null | head -n1 || true)"
if [[ -z "${FD00_ROUTE_LINE}" ]]; then
echo "Error: missing local fd00::/8 route." >&2
exit 1
fi
if [[ "${FD00_ROUTE_LINE}" != *"dev ${TUN_NAME}"* ]]; then
echo "Error: local fd00::/8 route is not via ${TUN_NAME}: ${FD00_ROUTE_LINE}" >&2
exit 1
fi
echo "tun_state=${TUN_STATE} tun_name=${TUN_NAME}"
echo "fd00_route=${FD00_ROUTE_LINE}"
echo ""
echo "==> Attempting to resolve ${REMOTE_FQDN}"
REMOTE_IPV6=""
if RESOLVED="$(getent ahosts "${REMOTE_FQDN}" | awk '{print $1}' | head -n1)" && [[ -n "${RESOLVED}" ]]; then
REMOTE_IPV6="${RESOLVED}"
echo "resolved_ipv6=${REMOTE_IPV6}"
else
echo "not_resolved"
if grep -Eq '^[[:space:]]*nameserver[[:space:]]+127\.0\.0\.1([[:space:]]|$)' /etc/resolv.conf 2>/dev/null; then
echo "resolver_hint=127.0.0.1_present"
else
echo "resolver_hint=127.0.0.1_missing_in_/etc/resolv.conf"
fi
fi
if [[ -n "${REMOTE_ADDR}" ]]; then
echo ""
echo "==> Bootstrapping direct connect to ${REMOTE_NPUB} at ${REMOTE_ADDR}"
sudo fipsctl connect "${REMOTE_NPUB}" "${REMOTE_ADDR}" udp || true
sleep 2
fi
echo ""
echo "==> Peer/session check (looking for remote npub)"
PEERS_JSON="$(sudo fipsctl show peers)"
python3 - "${REMOTE_NPUB}" "${PEERS_JSON}" <<'PY'
import json
import sys
needle = sys.argv[1]
obj = json.loads(sys.argv[2])
for p in obj.get("peers", []):
if p.get("npub") == needle:
print("peer_present=yes")
print(f"connectivity={p.get('connectivity')}")
print(f"transport_addr={p.get('transport_addr')}")
print(f"ipv6_addr={p.get('ipv6_addr')}")
break
else:
print("peer_present=no")
PY
echo ""
echo "==> HTTP check over .fips"
echo "remote_url=${REMOTE_URL}"
TMP_BODY="$(mktemp)"
trap 'rm -f "${TMP_BODY}"' EXIT
HTTP_CODE="$(curl -6 -sS --max-time "${CURL_TIMEOUT}" -o "${TMP_BODY}" -w '%{http_code}' "${REMOTE_URL}" || true)"
BYTES="$(wc -c < "${TMP_BODY}" | tr -d '[:space:]')"
echo "http_code=${HTTP_CODE:-000} bytes=${BYTES}"
if [[ "${HTTP_CODE:-000}" == "000" ]]; then
if timeout 2 bash -lc "cat </dev/null >/dev/tcp/${REMOTE_FQDN}/${REMOTE_HTTP_PORT}" >/dev/null 2>&1; then
echo "port_probe=tcp_open_but_http_failed"
else
echo "port_probe=tcp_closed_or_filtered"
fi
fi
if [[ "${HTTP_CODE:-000}" == "200" ]] && (( BYTES >= MIN_BYTES )); then
echo ""
echo "RESULT=PASS (inter-qube communication confirmed via .fips HTTP)"
exit 0
fi
echo ""
echo "RESULT=FAIL (communication not yet proven)"
echo ""
echo "Next actions:"
echo "1) If name did not resolve, ensure /etc/resolv.conf has nameserver 127.0.0.1 and dnsmasq forwards /fips/ to 127.0.0.1#5354"
echo "2) In EACH qube, get underlay IP: ip -4 -o addr show dev eth0"
echo "3) From this qube, connect to remote: sudo fipsctl connect ${REMOTE_NPUB} <REMOTE_ETH0_IP>:2121 udp"
echo "4) From remote qube, connect back to this qube: sudo fipsctl connect <THIS_QUBE_NPUB> <THIS_QUBE_ETH0_IP>:2121 udp"
echo "5) If app is up but script still fails, verify remote listener is IPv6-bound and set REMOTE_HTTP_PORT if non-80"
echo "6) Re-run this script in both qubes."
exit 1