feat(wg): clusev-wg.sh collect — emit run/wg-status.json for the dashboard

feat/v1-foundation
boban 2026-06-20 23:27:58 +02:00
parent dfdb0ce732
commit c164c0aec3
1 changed files with 62 additions and 0 deletions

View File

@ -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