diff --git a/test_qube_pair.sh b/test_qube_pair.sh new file mode 100755 index 0000000..6408f23 --- /dev/null +++ b/test_qube_pair.sh @@ -0,0 +1,106 @@ +#!/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 + +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}" + +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 + +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 "==> 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" +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" +TMP_BODY="$(mktemp)" +trap 'rm -f "${TMP_BODY}"' EXIT +HTTP_CODE="$(curl -sS --max-time "${CURL_TIMEOUT}" -o "${TMP_BODY}" -w '%{http_code}' "http://${REMOTE_FQDN}/" || true)" +BYTES="$(wc -c < "${TMP_BODY}" | tr -d '[:space:]')" +echo "http_code=${HTTP_CODE:-000} bytes=${BYTES}" + +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) In EACH qube, get underlay IP: ip -4 -o addr show dev eth0" +echo "2) From this qube, connect to remote: sudo fipsctl connect ${REMOTE_NPUB} :2121 udp" +echo "3) From remote qube, connect back to this qube: sudo fipsctl connect :2121 udp" +echo "4) Re-run this script in both qubes." +exit 1 diff --git a/update_and_deploy_fips.sh b/update_and_deploy_fips.sh index 64d4cf4..0219e1c 100755 --- a/update_and_deploy_fips.sh +++ b/update_and_deploy_fips.sh @@ -192,6 +192,23 @@ ask_yes_no() { done } +print_connectivity_diagnostics() { + echo "--- ip -6 route ---" >&2 + ip -6 route show >&2 || true + + if command -v nft >/dev/null 2>&1; then + echo "--- nft list chain ip6 qubes custom-input ---" >&2 + sudo nft list chain ip6 qubes custom-input >&2 || true + echo "--- nft list chain ip qubes custom-input ---" >&2 + sudo nft list chain ip qubes custom-input >&2 || true + fi + + if command -v ip6tables >/dev/null 2>&1; then + echo "--- ip6tables -S ---" >&2 + sudo ip6tables -S >&2 || true + fi +} + restart_unit_with_diagnostics() { local unit="$1" local required="${2:-yes}" @@ -211,6 +228,7 @@ restart_unit_with_diagnostics() { sudo systemctl status "${unit}" --no-pager >&2 || true echo "--- journalctl -u ${unit} (last ${journal_lines} lines) ---" >&2 sudo journalctl -u "${unit}" -n "${journal_lines}" --no-pager >&2 || true + print_connectivity_diagnostics [[ "${required}" == "yes" ]] && return 1 return 0 } @@ -223,6 +241,7 @@ restart_unit_with_diagnostics() { sudo systemctl status "${unit}" --no-pager >&2 || true echo "--- journalctl -u ${unit} (last ${journal_lines} lines) ---" >&2 sudo journalctl -u "${unit}" -n "${journal_lines}" --no-pager >&2 || true + print_connectivity_diagnostics [[ "${required}" == "yes" ]] && return 1 return 0 } @@ -234,6 +253,7 @@ restart_unit_with_diagnostics() { sudo systemctl status "${unit}" --no-pager >&2 || true echo "--- journalctl -u ${unit} (last ${journal_lines} lines) ---" >&2 sudo journalctl -u "${unit}" -n "${journal_lines}" --no-pager >&2 || true + print_connectivity_diagnostics [[ "${required}" == "yes" ]] && return 1 return 0 fi @@ -345,6 +365,95 @@ EOF fi } +persist_qubes_rc_local_network_overrides() { + local persist_dns="${1:-no}" + local rc_file="/rw/config/rc.local" + local tmp_file + + [[ -d /rw/config ]] || return 0 + + require_sudo + sudo mkdir -p /rw/config + + tmp_file="$(mktemp)" + if [[ -f "${rc_file}" ]]; then + sudo cat "${rc_file}" > "${tmp_file}" + else + printf '%s\n' '#!/bin/sh' > "${tmp_file}" + fi + + awk ' + BEGIN { skip = 0 } + /^# BEGIN FIPS_DEPLOY_NETWORK_OVERRIDES$/ { skip = 1; next } + /^# END FIPS_DEPLOY_NETWORK_OVERRIDES$/ { skip = 0; next } + skip == 0 { print } + ' "${tmp_file}" > "${tmp_file}.clean" + mv "${tmp_file}.clean" "${tmp_file}" + + { + echo "" + echo "# BEGIN FIPS_DEPLOY_NETWORK_OVERRIDES" + echo "if command -v nft >/dev/null 2>&1; then" + echo " nft add rule ip6 qubes custom-input iifname \"fips0\" accept 2>/dev/null || true" + echo " nft add rule ip qubes custom-input iifname \"fips0\" accept 2>/dev/null || true" + echo "fi" + if [[ "${persist_dns}" == "yes" ]]; then + cat <<'SH' +if ! grep -Eq '^[[:space:]]*nameserver[[:space:]]+127\.0\.0\.1([[:space:]]|$)' /etc/resolv.conf 2>/dev/null; then + tmp_resolv="$(mktemp)" + { + echo "nameserver 127.0.0.1" + cat /etc/resolv.conf 2>/dev/null || true + } > "${tmp_resolv}" + install -m 0644 "${tmp_resolv}" /etc/resolv.conf || true + rm -f "${tmp_resolv}" +fi +SH + fi + echo "# END FIPS_DEPLOY_NETWORK_OVERRIDES" + } >> "${tmp_file}" + + sudo install -m 0755 "${tmp_file}" "${rc_file}" + rm -f "${tmp_file}" + + echo "==> Persisted FIPS network overrides in ${rc_file}" +} + +ensure_fips_ingress_allowed() { + local persist_dns="${1:-no}" + + require_sudo + + echo "==> Ensuring inbound traffic on fips0 is allowed" + + if command -v nft >/dev/null 2>&1; then + if sudo nft list chain ip6 qubes custom-input >/dev/null 2>&1; then + if ! sudo nft list chain ip6 qubes custom-input 2>/dev/null | grep -q 'iifname "fips0" accept'; then + sudo nft add rule ip6 qubes custom-input iifname "fips0" accept + fi + if sudo nft list chain ip qubes custom-input >/dev/null 2>&1; then + if ! sudo nft list chain ip qubes custom-input 2>/dev/null | grep -q 'iifname "fips0" accept'; then + sudo nft add rule ip qubes custom-input iifname "fips0" accept + fi + fi + persist_qubes_rc_local_network_overrides "${persist_dns}" + return 0 + fi + fi + + if command -v ip6tables >/dev/null 2>&1; then + if ! sudo ip6tables -C INPUT -i fips0 -j ACCEPT >/dev/null 2>&1; then + sudo ip6tables -I INPUT 1 -i fips0 -j ACCEPT + fi + fi + + if command -v iptables >/dev/null 2>&1; then + if ! sudo iptables -C INPUT -i fips0 -j ACCEPT >/dev/null 2>&1; then + sudo iptables -I INPUT 1 -i fips0 -j ACCEPT + fi + fi +} + ensure_build_toolchain ensure_tun_ready @@ -420,6 +529,7 @@ ADD_DEFAULT_PEERS="$(ask_yes_no "Add default static peers (laantungir/fips-test- ADD_CURRENT_USER_TO_FIPS_GROUP="$(ask_yes_no "Add current user to fips group?" "yes")" START_SERVICE_NOW="$(ask_yes_no "Start/restart fips service now?" "yes")" CONFIGURE_DNSMASQ_FOR_FIPS="$(ask_yes_no "Configure dnsmasq for .fips DNS forwarding in this qube?" "yes")" +ALLOW_FIPS_INGRESS="$(ask_yes_no "Allow inbound traffic on fips0 (needed for inter-qube TCP over FIPS on Qubes)?" "yes")" echo "==> Preparing ${INSTALL_MODE} payload" @@ -561,6 +671,7 @@ echo "==> Cleaning potentially stale fd00::/8 route state" require_sudo sudo ip -6 route del fd00::/8 >/dev/null 2>&1 || true +DNS_FORWARDING_ACTIVE="no" if [[ "${START_SERVICE_NOW}" == "yes" ]]; then echo "==> Starting/restarting FIPS services" sudo systemctl daemon-reload @@ -568,7 +679,15 @@ if [[ "${START_SERVICE_NOW}" == "yes" ]]; then restart_unit_with_diagnostics "fips-dns.service" "no" if [[ "${CONFIGURE_DNSMASQ_FOR_FIPS}" == "yes" ]]; then - configure_dnsmasq_fips_dns || echo "Warning: dnsmasq configuration failed; continuing." >&2 + if configure_dnsmasq_fips_dns; then + DNS_FORWARDING_ACTIVE="yes" + else + echo "Warning: dnsmasq configuration failed; continuing." >&2 + fi + fi + + if [[ "${ALLOW_FIPS_INGRESS}" == "yes" ]]; then + ensure_fips_ingress_allowed "${DNS_FORWARDING_ACTIVE}" fi echo "==> Running post-deploy health check" @@ -582,6 +701,25 @@ else fi echo +if command -v fipsctl >/dev/null 2>&1; then + STATUS_JSON="$(sudo fipsctl show status 2>/dev/null || true)" + if [[ -n "${STATUS_JSON}" ]]; then + echo "==> Local node summary" + python3 - "${STATUS_JSON}" <<'PY' +import json +import sys + +s = json.loads(sys.argv[1]) +print(f"npub: {s.get('npub', 'n/a')}") +print(f"ipv6: {s.get('ipv6_addr', 'n/a')}") +PY + ETH0_IPV4="$(ip -4 -o addr show dev eth0 2>/dev/null | awk '{print $4}' | head -n1 | cut -d/ -f1)" + if [[ -n "${ETH0_IPV4}" ]]; then + echo "connect hint: sudo fipsctl connect ${ETH0_IPV4}:2121 udp" + fi + fi +fi + echo "Done. fips has been built and ${INSTALL_MODE} has completed." echo "Check service status with: sudo systemctl status fips --no-pager" echo "Follow logs with: sudo journalctl -u fips -f"