176 lines
5.0 KiB
Bash
Executable File
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
|