#!/usr/bin/env bash set -euo pipefail # Simple inter-qube FIPS connectivity test + optional direct connect bootstrap. # Usage: # ./test_qube_pair.sh [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_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/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} :2121 udp" echo "4) From remote qube, connect back to this qube: sudo fipsctl connect :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