#!/bin/bash # Managed by Ansible (role: pia-gateway). Do not edit by hand. # # Observability only. This script NEVER modifies routing, firewall, or # WireGuard state — a failed check must close the path (already true by # construction, via the kill switch in tasks/routing.yaml/firewall.yaml), # never "recover" it. Output goes to the journal # (journalctl -u pia-gateway-healthcheck); wire alerting off of that # separately (see plan.md Phase 9) rather than adding remediation here. # # The "route decision" check below confirms what the kernel FIB *would* # do for a VLAN 50 source — it does not send a packet and is not proof of # actual PIA egress or identity. That proof requires the packet-capture # test matrix in plan.md Phase 5; do not treat a clean run of this script # as substituting for it. set -uo pipefail WG_IF="{{ pia_wg_interface }}" TABLE="{{ pia_route_table_name }}" SUBNET="{{ pia_vlan50_subnet }}" PRIORITY="{{ pia_rule_priority }}" CHAIN="{{ pia_iptables_chain }}" MAX_HANDSHAKE_AGE={{ pia_healthcheck_handshake_max_age_sec }} PROBE_SRC="${SUBNET%.*}.2" # a representative VLAN 50 address for the route-decision probe only; never actually used as a source fail=0 # Informational only (not pass/fail) — which region/server this host is # actually registered against right now, per the comment # tasks/register.yaml writes into the generated config. Since PIA # WireGuard registration now falls back across multiple regions # (pia_region_candidates in defaults/main.yaml), "which one is live" # isn't implied by config alone — it's whichever one answered OK last # time the playbook ran, not necessarily the first-configured one. configured_peer=$(grep -m1 '^# PIA WireGuard server actually used' "{{ pia_wg_config_dir }}/{{ pia_wg_interface }}.conf" 2>/dev/null || true) if [[ -n "${configured_peer}" ]]; then echo "INFO configured-peer: ${configured_peer#\# }" fi if ip link show "${WG_IF}" up &>/dev/null; then echo "PASS interface: ${WG_IF} is up" else echo "FAIL interface: ${WG_IF} is not up" fail=1 fi if command -v wg &>/dev/null && wg show "${WG_IF}" latest-handshakes &>/dev/null; then handshake_epoch=$(wg show "${WG_IF}" latest-handshakes 2>/dev/null | awk '{print $2}') now=$(date +%s) if [[ -n "${handshake_epoch}" && "${handshake_epoch}" -gt 0 ]]; then age=$((now - handshake_epoch)) if (( age <= MAX_HANDSHAKE_AGE )); then echo "PASS handshake: ${age}s old (max ${MAX_HANDSHAKE_AGE}s)" else echo "FAIL handshake: ${age}s old, exceeds max ${MAX_HANDSHAKE_AGE}s" fail=1 fi else echo "FAIL handshake: no handshake recorded yet" fail=1 fi else echo "WARN handshake: unable to query wg show (interface absent or wg missing)" fail=1 fi if ip rule show | grep -qE "^${PRIORITY}:[[:space:]]*from ${SUBNET} lookup ${TABLE}\$"; then echo "PASS rule: from ${SUBNET} lookup ${TABLE} present at priority ${PRIORITY}" else echo "FAIL rule: from ${SUBNET} lookup ${TABLE} missing" fail=1 fi default_route=$(ip route show table "${TABLE}" 2>/dev/null | grep '^default' || true) if [[ "${default_route}" == *"dev ${WG_IF}"* ]]; then echo "PASS route: table ${TABLE} default is via ${WG_IF} (open)" elif [[ "${default_route}" == *unreachable* ]] || ip route show table "${TABLE}" 2>/dev/null | grep -q '^unreachable default'; then echo "PASS route: table ${TABLE} default is unreachable (closed, kill switch engaged)" else echo "FAIL route: table ${TABLE} has no default route at all (neither open via ${WG_IF} nor a terminal unreachable) — investigate immediately" fail=1 fi # Not a check on FORWARD's own default policy — that's deliberately left # alone (see tasks/firewall.yaml for why: minisforum runs Flannel, which # needs its own broad FORWARD ACCEPTs). What actually matters is that the # dedicated chain exists, is jumped into first for this source, and ends # in a real DROP. if iptables -C FORWARD -s "${SUBNET}" -j "${CHAIN}" 2>/dev/null; then echo "PASS firewall: FORWARD jumps to ${CHAIN} for ${SUBNET}" else echo "FAIL firewall: FORWARD does not jump to ${CHAIN} for ${SUBNET}" fail=1 fi if iptables -S "${CHAIN}" 2>/dev/null | grep -qE '^-A '"${CHAIN}"' -j DROP$'; then echo "PASS firewall: ${CHAIN} ends in an unconditional DROP" else echo "FAIL firewall: ${CHAIN} has no unconditional DROP catch-all" fail=1 fi route_decision=$(ip route get 1.1.1.1 from "${PROBE_SRC}" 2>&1 || true) if [[ "${route_decision}" == *"dev ${WG_IF}"* ]]; then echo "PASS route-decision (not proof of live egress — see header comment): ${SUBNET} sourced traffic resolves via ${WG_IF}" else echo "WARN route-decision (not proof of live egress — see header comment): ${route_decision}" fi exit "${fail}"