feat(wg): serve-request write-bridge + peer/gate cores (host-side validation)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/v1-foundation
parent
f91d8ed834
commit
695b33cf37
|
|
@ -82,14 +82,100 @@ gate_remove() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── commands ──────────────────────────────────────────────────────────────────────────────────
|
# ── commands ──────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Core: create a peer + print ONLY the client config to stdout (errors to stderr, non-zero on fail).
|
||||||
|
# Shared by the interactive cmd_add_peer and the write-bridge serve-request.
|
||||||
|
_peer_add() {
|
||||||
|
local name="$1"
|
||||||
|
require_setup
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$WG_ENV"
|
||||||
|
[ -n "${WG_SUBNET:-}" ] && [ -n "${WG_SERVER_PUBKEY:-}" ] && [ -n "${WG_ENDPOINT:-}" ] || { echo "wg.env unvollstaendig" >&2; return 1; }
|
||||||
|
grep -qF "# clusev-peer: ${name}" "$WG_CONF" 2>/dev/null && { echo "Peer existiert bereits" >&2; return 1; }
|
||||||
|
local ip cpriv cpub
|
||||||
|
ip="$(next_free_ip)" || { echo "kein freier Adressraum" >&2; return 1; }
|
||||||
|
cpriv="$(wg genkey)"; cpub="$(printf '%s' "$cpriv" | wg pubkey)"
|
||||||
|
cat >> "$WG_CONF" <<EOF
|
||||||
|
|
||||||
|
# clusev-peer: ${name}
|
||||||
|
[Peer]
|
||||||
|
PublicKey = ${cpub}
|
||||||
|
AllowedIPs = ${ip}/32
|
||||||
|
EOF
|
||||||
|
wg set "$WG_IF" peer "$cpub" allowed-ips "${ip}/32"
|
||||||
|
cat <<EOF
|
||||||
|
[Interface]
|
||||||
|
PrivateKey = ${cpriv}
|
||||||
|
Address = ${ip}/32
|
||||||
|
DNS = 1.1.1.1
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = ${WG_SERVER_PUBKEY}
|
||||||
|
Endpoint = ${WG_ENDPOINT}
|
||||||
|
AllowedIPs = ${WG_SUBNET}
|
||||||
|
PersistentKeepalive = 25
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_add_peer() {
|
||||||
|
need_root add-peer
|
||||||
|
local name="${1:-}"; [ -n "$name" ] || die "Name fehlt: clusev wg add-peer <name>"
|
||||||
|
valid_name "$name" || die "Ungueltiger Peer-Name — erlaubt: A-Z a-z 0-9 . _ -"
|
||||||
|
local conf; conf="$(_peer_add "$name")" || die "Anlegen fehlgeschlagen: ${conf}"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
[ -f "$WG_ENV" ] && . "$WG_ENV"
|
||||||
|
local ip; ip="$(printf '%s\n' "$conf" | awk '/^Address/{print $3; exit}')"
|
||||||
|
bold "Peer '${name}' angelegt (${ip%%/*})."
|
||||||
|
printf '%s\n' "$conf"
|
||||||
|
if have qrencode; then
|
||||||
|
printf '%s\n' "$conf" | qrencode -t ansiutf8 || warn "QR-Erzeugung fehlgeschlagen — nutze die Textkonfiguration oben."
|
||||||
|
else
|
||||||
|
warn "qrencode nicht installiert — nutze die Textkonfiguration oben."
|
||||||
|
fi
|
||||||
|
info "AllowedIPs=${WG_SUBNET:-} = Split-Tunnel (nur Panel ueber WG). Voll-Tunnel (0.0.0.0/0) moeglich, aber NICHT empfohlen — leitet allen Client-Verkehr ueber Clusev."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Core: remove the named peer from the running interface + wg0.conf. Errors to stderr.
|
||||||
|
_peer_remove() {
|
||||||
|
local name="$1"
|
||||||
|
require_setup
|
||||||
|
local pub
|
||||||
|
pub="$(WANT="# clusev-peer: ${name}" awk '$0==ENVIRON["WANT"]{f=1;next} f&&/PublicKey/{gsub(/[ \t]/,"");sub(/PublicKey=/,"");print;exit}' "$WG_CONF")"
|
||||||
|
[ -n "$pub" ] || { echo "Peer '${name}' nicht gefunden" >&2; return 1; }
|
||||||
|
wg set "$WG_IF" peer "$pub" remove 2>/dev/null || true
|
||||||
|
local tmp; tmp="$(mktemp)"
|
||||||
|
WANT="# clusev-peer: ${name}" awk '
|
||||||
|
$0==ENVIRON["WANT"] {drop=1; next}
|
||||||
|
drop && /^[[:space:]]*$/ {drop=0; next}
|
||||||
|
drop {next}
|
||||||
|
{print}
|
||||||
|
' "$WG_CONF" > "$tmp"
|
||||||
|
install -m 0600 "$tmp" "$WG_CONF"; rm -f "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_remove_peer() {
|
||||||
|
need_root remove-peer
|
||||||
|
local name="${1:-}"; [ -n "$name" ] || die "Name fehlt: clusev wg remove-peer <name>"
|
||||||
|
valid_name "$name" || die "Ungueltiger Peer-Name — erlaubt: A-Z a-z 0-9 . _ -"
|
||||||
|
_peer_remove "$name" || die "Entfernen fehlgeschlagen."
|
||||||
|
bold "Peer '${name}' entfernt."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Core: enable the gate (write marker + apply). Refuses if wg0 is down. Errors to stderr.
|
||||||
|
_gate_up_now() {
|
||||||
|
require_setup
|
||||||
|
[ -e "/sys/class/net/${WG_IF}" ] || { echo "wg0 nicht aktiv" >&2; return 1; }
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
printf 'enabled\n' > "$GATE_MARKER"
|
||||||
|
gate_apply
|
||||||
|
}
|
||||||
|
|
||||||
cmd_up() {
|
cmd_up() {
|
||||||
need_root up
|
need_root up
|
||||||
require_setup
|
require_setup
|
||||||
[ -e "/sys/class/net/${WG_IF}" ] || die "${WG_IF} ist nicht aktiv. Erst Tunnel testen (siehe 'clusev wg status'), dann 'clusev wg up'."
|
[ -e "/sys/class/net/${WG_IF}" ] || die "${WG_IF} ist nicht aktiv. Erst Tunnel testen (siehe 'clusev wg status'), dann 'clusev wg up'."
|
||||||
warn "Vor dem Gate sicherstellen, dass der Tunnel das Panel erreicht. Notausgang ueber SSH: 'clusev wg down'."
|
warn "Vor dem Gate sicherstellen, dass der Tunnel das Panel erreicht. Notausgang ueber SSH: 'clusev wg down'."
|
||||||
mkdir -p "$STATE_DIR"
|
_gate_up_now
|
||||||
printf 'enabled\n' > "$GATE_MARKER"
|
|
||||||
gate_apply
|
|
||||||
bold "WireGuard-Gate aktiv — Panel (80/443) nur noch ueber den Tunnel erreichbar."
|
bold "WireGuard-Gate aktiv — Panel (80/443) nur noch ueber den Tunnel erreichbar."
|
||||||
info "Notausgang (per SSH, falls der Tunnel nicht erreicht): clusev wg down"
|
info "Notausgang (per SSH, falls der Tunnel nicht erreicht): clusev wg down"
|
||||||
}
|
}
|
||||||
|
|
@ -116,73 +202,6 @@ next_free_ip() {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_add_peer() {
|
|
||||||
need_root add-peer
|
|
||||||
local name="${1:-}"; [ -n "$name" ] || die "Name fehlt: clusev wg add-peer <name>"
|
|
||||||
valid_name "$name" || die "Ungueltiger Peer-Name — erlaubt: A-Z a-z 0-9 . _ -"
|
|
||||||
require_setup
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "$WG_ENV"
|
|
||||||
[ -n "${WG_SUBNET:-}" ] && [ -n "${WG_SERVER_PUBKEY:-}" ] && [ -n "${WG_ENDPOINT:-}" ] || die "wg.env unvollstaendig — Setup erneut ausfuehren."
|
|
||||||
grep -qF "# clusev-peer: ${name}" "$WG_CONF" 2>/dev/null && die "Peer '${name}' existiert bereits."
|
|
||||||
local ip; ip="$(next_free_ip)" || die "Kein freier Adressraum im Subnetz ${WG_SUBNET}."
|
|
||||||
local cpriv cpub
|
|
||||||
cpriv="$(wg genkey)"; cpub="$(printf '%s' "$cpriv" | wg pubkey)"
|
|
||||||
|
|
||||||
cat >> "$WG_CONF" <<EOF
|
|
||||||
|
|
||||||
# clusev-peer: ${name}
|
|
||||||
[Peer]
|
|
||||||
PublicKey = ${cpub}
|
|
||||||
AllowedIPs = ${ip}/32
|
|
||||||
EOF
|
|
||||||
wg set "$WG_IF" peer "$cpub" allowed-ips "${ip}/32" # live, no restart
|
|
||||||
|
|
||||||
local client_conf
|
|
||||||
client_conf="$(cat <<EOF
|
|
||||||
[Interface]
|
|
||||||
PrivateKey = ${cpriv}
|
|
||||||
Address = ${ip}/32
|
|
||||||
DNS = 1.1.1.1
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = ${WG_SERVER_PUBKEY}
|
|
||||||
Endpoint = ${WG_ENDPOINT}
|
|
||||||
AllowedIPs = ${WG_SUBNET}
|
|
||||||
PersistentKeepalive = 25
|
|
||||||
EOF
|
|
||||||
)"
|
|
||||||
bold "Peer '${name}' angelegt (${ip})."
|
|
||||||
printf '%s\n' "$client_conf"
|
|
||||||
if have qrencode; then
|
|
||||||
printf '%s\n' "$client_conf" | qrencode -t ansiutf8 || warn "QR-Erzeugung fehlgeschlagen — nutze die Textkonfiguration oben."
|
|
||||||
else
|
|
||||||
warn "qrencode nicht installiert — nutze die Textkonfiguration oben."
|
|
||||||
fi
|
|
||||||
info "AllowedIPs=${WG_SUBNET} = Split-Tunnel (nur Panel ueber WG). Voll-Tunnel (0.0.0.0/0) moeglich, aber NICHT empfohlen — leitet allen Client-Verkehr ueber Clusev."
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_remove_peer() {
|
|
||||||
need_root remove-peer
|
|
||||||
local name="${1:-}"; [ -n "$name" ] || die "Name fehlt: clusev wg remove-peer <name>"
|
|
||||||
valid_name "$name" || die "Ungueltiger Peer-Name — erlaubt: A-Z a-z 0-9 . _ -"
|
|
||||||
require_setup
|
|
||||||
local pub
|
|
||||||
pub="$(WANT="# clusev-peer: ${name}" awk '$0==ENVIRON["WANT"]{f=1;next} f&&/PublicKey/{gsub(/[ \t]/,"");sub(/PublicKey=/,"");print;exit}' "$WG_CONF")"
|
|
||||||
[ -n "$pub" ] || die "Peer '${name}' nicht gefunden."
|
|
||||||
wg set "$WG_IF" peer "$pub" remove 2>/dev/null || true
|
|
||||||
# drop the block from the marker comment up to (and including) the trailing blank line
|
|
||||||
local tmp; tmp="$(mktemp)"
|
|
||||||
WANT="# clusev-peer: ${name}" awk '
|
|
||||||
$0==ENVIRON["WANT"] {drop=1; next}
|
|
||||||
drop && /^[[:space:]]*$/ {drop=0; next}
|
|
||||||
drop {next}
|
|
||||||
{print}
|
|
||||||
' "$WG_CONF" > "$tmp"
|
|
||||||
install -m 0600 "$tmp" "$WG_CONF"; rm -f "$tmp"
|
|
||||||
bold "Peer '${name}' entfernt."
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_status() {
|
cmd_status() {
|
||||||
need_root status
|
need_root status
|
||||||
bold "WireGuard (${WG_IF})"
|
bold "WireGuard (${WG_IF})"
|
||||||
|
|
@ -265,10 +284,75 @@ EOF
|
||||||
info "1) Client importieren (QR oben), 2) http://${server_ip} ueber den Tunnel oeffnen, 3) dann: clusev wg up"
|
info "1) Client importieren (QR oben), 2) http://${server_ip} ueber den Tunnel oeffnen, 3) dann: clusev wg up"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Minimal-but-correct JSON string escaper (backslash, quote, CR, tab, newline).
|
||||||
|
_json_str() {
|
||||||
|
local s="$1"
|
||||||
|
s="${s//\\/\\\\}"
|
||||||
|
s="${s//\"/\\\"}"
|
||||||
|
s="${s//$'\r'/}"
|
||||||
|
s="${s//$'\t'/\\t}"
|
||||||
|
s="${s//$'\n'/\\n}"
|
||||||
|
printf '"%s"' "$s"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write run/wg-result-<id>.json (0600, owned by the run/ owner = the app uid).
|
||||||
|
_write_result() {
|
||||||
|
local id="$1" ok="$2" msg="$3" config="$4"
|
||||||
|
local f="${RUN_DIR}/wg-result-${id}.json" cfg='null'
|
||||||
|
[ -n "$config" ] && cfg="$(_json_str "$config")"
|
||||||
|
mkdir -p "$RUN_DIR" 2>/dev/null || true
|
||||||
|
printf '{"id":"%s","ok":%s,"message":%s,"config":%s,"at":%s}\n' \
|
||||||
|
"$id" "$ok" "$(_json_str "$msg")" "$cfg" "$(date +%s 2>/dev/null || echo 0)" > "${f}.tmp"
|
||||||
|
mv -f "${f}.tmp" "$f" 2>/dev/null || { rm -f "${f}.tmp"; return 0; }
|
||||||
|
chown "$(stat -c %u:%g "$RUN_DIR" 2>/dev/null || echo 0:0)" "$f" 2>/dev/null || true
|
||||||
|
chmod 600 "$f" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Process one UI write-request. Whitelists the action + re-validates every arg HOST-SIDE.
|
||||||
|
cmd_serve_request() {
|
||||||
|
need_root serve-request
|
||||||
|
local req="${RUN_DIR}/wg-request.json" work="${RUN_DIR}/wg-request.processing"
|
||||||
|
[ -f "$req" ] || return 0
|
||||||
|
mv -f "$req" "$work" 2>/dev/null || return 0
|
||||||
|
local json id action name
|
||||||
|
json="$(cat "$work" 2>/dev/null || true)"; rm -f "$work"
|
||||||
|
|
||||||
|
id="$(printf '%s' "$json" | grep -oE '"id":"[A-Za-z0-9]{16,64}"' | head -n1 | sed -E 's/.*:"//; s/"$//')"
|
||||||
|
[ -n "$id" ] || return 0
|
||||||
|
action="$(printf '%s' "$json" | grep -oE '"action":"[a-z-]{1,24}"' | head -n1 | sed -E 's/.*:"//; s/"$//')"
|
||||||
|
name="$(printf '%s' "$json" | grep -oE '"name":"[A-Za-z0-9._-]{1,64}"' | head -n1 | sed -E 's/.*:"//; s/"$//')"
|
||||||
|
|
||||||
|
local ok=false msg='ok' config=''
|
||||||
|
case "$action" in
|
||||||
|
add-peer)
|
||||||
|
if valid_name "$name"; then
|
||||||
|
if config="$(_peer_add "$name" 2>/dev/null)"; then ok=true; else ok=false; msg='add-failed'; config=''; fi
|
||||||
|
else msg='invalid-name'; fi ;;
|
||||||
|
remove-peer)
|
||||||
|
if valid_name "$name"; then
|
||||||
|
if _peer_remove "$name" >/dev/null 2>&1; then ok=true; else msg='remove-failed'; fi
|
||||||
|
else msg='invalid-name'; fi ;;
|
||||||
|
gate-up) if _gate_up_now >/dev/null 2>&1; then ok=true; else msg='gate-up-failed'; fi ;;
|
||||||
|
gate-down) if cmd_down >/dev/null 2>&1; then ok=true; else msg='gate-down-failed'; fi ;;
|
||||||
|
*) msg='unknown-action' ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
_write_result "$id" "$ok" "$msg" "$config"
|
||||||
|
if [ "$ok" = true ] && [ "$action" = add-peer ] && [ -n "$config" ] && have qrencode; then
|
||||||
|
local qr="${RUN_DIR}/wg-qr-${id}.svg"
|
||||||
|
if printf '%s\n' "$config" | qrencode -t SVG -o "$qr" 2>/dev/null; then
|
||||||
|
chown "$(stat -c %u:%g "$RUN_DIR" 2>/dev/null || echo 0:0)" "$qr" 2>/dev/null || true
|
||||||
|
chmod 600 "$qr" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Emit run/wg-status.json (atomic temp+mv). Public, NO secrets. If WG isn't configured, emits
|
# 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.
|
# {"configured":false}. Run every few seconds by clusev-wg-collect.timer; the dashboard reads it.
|
||||||
cmd_collect() {
|
cmd_collect() {
|
||||||
mkdir -p "$RUN_DIR" 2>/dev/null || true
|
mkdir -p "$RUN_DIR" 2>/dev/null || true
|
||||||
|
# Drop result + QR sidecars older than 60 s (they carry a private key; the app reads them once).
|
||||||
|
find "$RUN_DIR" -maxdepth 1 -type f \( -name 'wg-result-*.json' -o -name 'wg-qr-*.svg' \) -mmin +1 -delete 2>/dev/null || true
|
||||||
local now tmp dest
|
local now tmp dest
|
||||||
now="$(date +%s 2>/dev/null || echo 0)"
|
now="$(date +%s 2>/dev/null || echo 0)"
|
||||||
dest="${RUN_DIR}/wg-status.json"
|
dest="${RUN_DIR}/wg-status.json"
|
||||||
|
|
@ -345,6 +429,7 @@ case "$cmd" in
|
||||||
remove-peer) cmd_remove_peer "$@" ;;
|
remove-peer) cmd_remove_peer "$@" ;;
|
||||||
gate-apply) need_root gate-apply; gate_apply ;; # called by clusev-wg-gate.service
|
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)
|
collect) cmd_collect ;; # called by clusev-wg-collect.timer (read-only; no root needed)
|
||||||
|
serve-request) cmd_serve_request ;; # called by clusev-wg-request.service
|
||||||
help|-h|--help) usage ;;
|
help|-h|--help) usage ;;
|
||||||
*) printf 'Unbekannter Befehl: %s\n\n' "$cmd" >&2; usage; exit 64 ;;
|
*) printf 'Unbekannter Befehl: %s\n\n' "$cmd" >&2; usage; exit 64 ;;
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue