diff --git a/docker/wg/clusev-wg.sh b/docker/wg/clusev-wg.sh index d52e618..d68d16e 100755 --- a/docker/wg/clusev-wg.sh +++ b/docker/wg/clusev-wg.sh @@ -15,6 +15,12 @@ WG_ENV="${STATE_DIR}/wg.env" GATE_MARKER="${STATE_DIR}/wg-gate.enabled" PANEL_PORTS="80,443" # see spec §3 post-DNAT note: matches the published container ports (Caddy 80/443) +# Project root (for the run/ bridge dir). The collect timer sets CLUSEV_DIR; otherwise derive it +# from this script's location (docker/wg/clusev-wg.sh -> ../..). +SELF_DIR="$(cd "$(dirname "$0")" && pwd)" +CLUSEV_DIR="${CLUSEV_DIR:-$(cd "${SELF_DIR}/../.." && pwd)}" +RUN_DIR="${CLUSEV_DIR}/run" + bold() { printf '\033[1m%s\033[0m\n' "$*"; } info() { printf ' %s\n' "$*"; } warn() { printf '\033[33m ! %s\033[0m\n' "$*" >&2; } @@ -259,6 +265,61 @@ EOF info "1) Client importieren (QR oben), 2) http://${server_ip} ueber den Tunnel oeffnen, 3) dann: clusev wg up" } +# Emit run/wg-status.json (atomic temp+mv). Public, NO secrets. If WG isn't configured, emits +# {"configured":false}. Run every few seconds by clusev-wg-collect.timer; the dashboard reads it. +cmd_collect() { + mkdir -p "$RUN_DIR" 2>/dev/null || true + local now tmp dest + now="$(date +%s 2>/dev/null || echo 0)" + dest="${RUN_DIR}/wg-status.json" + tmp="$(mktemp "${RUN_DIR}/.wg-status.XXXXXX" 2>/dev/null)" || tmp="${RUN_DIR}/.wg-status.tmp" + + if [ ! -f "$WG_CONF" ]; then + printf '{"at":%s,"configured":false}\n' "$now" > "$tmp" + mv -f "$tmp" "$dest" 2>/dev/null || { rm -f "$tmp"; return 0; } + chmod 644 "$dest" 2>/dev/null || true + return 0 + fi + + # shellcheck disable=SC1090 + [ -f "$WG_ENV" ] && . "$WG_ENV" + local gate_marker=false gate_chain=false wgq + [ -f "$GATE_MARKER" ] && gate_marker=true + iptables -nL "$GATE_CHAIN" >/dev/null 2>&1 && gate_chain=true + wgq="$(systemctl is-active "wg-quick@${WG_IF}" 2>/dev/null || echo inactive)" + + # Peers from `wg show wg0 dump`, named via the wg0.conf `# clusev-peer:` comments. Peer names are + # valid_name-restricted and pubkeys/endpoints are from safe charsets, so the values are JSON-safe. + local peers_json + peers_json="$({ wg show "$WG_IF" dump 2>/dev/null || true; } | awk -v conf="$WG_CONF" ' + BEGIN { + name="" + while ((getline line < conf) > 0) { + if (line ~ /^# clusev-peer:/) { sub(/^# clusev-peer:[ \t]*/,"",line); name=line } + else if (name != "" && line ~ /^PublicKey[ \t]*=/) { p=line; sub(/^PublicKey[ \t]*=[ \t]*/,"",p); names[p]=name; name="" } + } + close(conf); out=""; first=1 + } + NR==1 { next } # interface line + { + pk=$1; endp=$3; hs=$5+0; rx=$6+0; tx=$7+0 + if (endp=="(none)") endp="" + nm=(pk in names)?names[pk]:"" + rec="{\"name\":\"" nm "\",\"pubkey\":\"" pk "\",\"endpoint\":\"" endp "\",\"latest_handshake\":" hs ",\"rx\":" rx ",\"tx\":" tx "}" + if (first) { out=rec; first=0 } else { out=out "," rec } + } + END { printf "[%s]", out } + ')" + [ -n "$peers_json" ] || peers_json="[]" + + printf '{"at":%s,"configured":true,"gate":{"marker":%s,"chain":%s},"wg_quick":"%s","server":{"subnet":"%s","server_ip":"%s","port":%s,"endpoint":"%s","pubkey":"%s"},"peers":%s}\n' \ + "$now" "$gate_marker" "$gate_chain" "$wgq" \ + "${WG_SUBNET:-}" "${WG_SERVER_IP:-}" "${WG_PORT:-0}" "${WG_ENDPOINT:-}" "${WG_SERVER_PUBKEY:-}" \ + "$peers_json" > "$tmp" + mv -f "$tmp" "$dest" 2>/dev/null || { rm -f "$tmp"; return 0; } + chmod 644 "$dest" 2>/dev/null || true +} + usage() { cat <<'EOF' clusev wg — WireGuard-Zugang (Host) @@ -283,6 +344,7 @@ case "$cmd" in add-peer) cmd_add_peer "$@" ;; remove-peer) cmd_remove_peer "$@" ;; gate-apply) need_root gate-apply; gate_apply ;; # called by clusev-wg-gate.service + collect) cmd_collect ;; # called by clusev-wg-collect.timer (read-only; no root needed) help|-h|--help) usage ;; *) printf 'Unbekannter Befehl: %s\n\n' "$cmd" >&2; usage; exit 64 ;; esac