Join the tunnel, then tell the console everything so far

The three interface states come over from ConfigureWireguard.php exactly as
that file works them out, because the one line they replace got two of them
wrong. `systemctl enable --now wg-quick@wg0 || wg-quick up wg0` brought the
interface up via its fallback WITHOUT the systemd enablement, so the tunnel did
not come back after a reboot; and on the next attempt both halves failed with
"wg0 already exists", so it retried forever against a tunnel that was working.
Enable before start, because enable is the half that survives a reboot, and it
is repaired even when the interface is already up by other means.

The configuration is compared before it is written. The file used to be
rewritten unconditionally with nothing reloading it, so correcting a wrong hub
key showed a new file and identical behaviour — the running interface still
held the old peer. Knowing whether it CHANGED is what lets an unchanged, working
tunnel be left alone and a corrected one actually be applied. A change means a
restart rather than `wg syncconf`, because syncconf applies peers only and a
corrected Address or AllowedIPs would silently do nothing.

AllowedIPs is computed, not copied. Writing the host's own address there would
let the tunnel handshake and leave every other participant unreachable — a fault
that looks like a routing problem somewhere else entirely.

The handshake target is derived from --api rather than taking its own argument.
CluPilot's tunnel address is already in there, and a second value meaning the
same thing is a second value that eventually disagrees with the first.

The tunnel counts only when it has been PROVEN, never when the file is on disk.
A stored tunnel address without proof made every later connection attempt
useless and the recovery was hand-editing the database. Once it is proven,
flush_reports runs and the whole history from the rescue system onwards reaches
the console carrying its original timestamps.

Verified without hardware: dash-clean; the subnet arithmetic is right on octet
boundaries and off them (192.168.5.130/25 gives 192.168.5.128/25, 172.16.4.9/12
gives 172.16.0.0/12); the API host parser handles a bare address, a scheme, a
port and a path; and wg0.conf renders with AllowedIPs on the network rather than
the host. Step 2 unticked — it wants all five earlier sections showing up in the
console with real timestamps, and that needs a hub.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feature/host-bootstrap
nexxo 2026-07-30 20:08:39 +02:00
parent 9e485efe87
commit c345f21e7d
3 changed files with 174 additions and 3 deletions

View File

@ -689,6 +689,52 @@ section_network_bridged() {
return 0
}
# ---------------------------------------------------------------------------
# Abschnitt 6: wireguard_joined — ab hier ist CluPilot erreichbar
# ---------------------------------------------------------------------------
#
# Der erste Abschnitt, nach dem es einen Rückweg gibt. Deshalb endet er mit
# `flush_reports`: alles, was seit dem Rettungssystem liegengeblieben ist, geht
# jetzt raus — mit den Zeitstempeln von damals.
section_wireguard_joined() {
export DEBIAN_FRONTEND=noninteractive
if ! apt-get install -y wireguard >> "${CLUPILOT_WORK_DIR}/apt.log" 2>&1; then
CLUPILOT_SECTION_NOTE='wireguard ließ sich nicht installieren'
return 1
fi
mkdir -p /etc/wireguard
chmod 700 /etc/wireguard
_desired="$(render_wireguard_config)"
_current="$(cat /etc/wireguard/wg0.conf 2>/dev/null || true)"
# Vor dem Schreiben vergleichen. Die Datei wurde früher bedingungslos neu
# geschrieben, ohne dass irgendetwas sie neu einlas — wer einen falschen
# Hub-Schlüssel korrigierte, sah die neue Datei und kein geändertes
# Verhalten, weil die laufende Schnittstelle den alten Peer behielt.
_changed=0
if [ "$_desired" != "$_current" ]; then
_changed=1
umask 077
printf '%s\n' "$_desired" > /etc/wireguard/wg0.conf
chmod 600 /etc/wireguard/wg0.conf
fi
if ! bring_up_wireguard "$_changed"; then
CLUPILOT_SECTION_NOTE='wg0 kam nicht in den Zustand, den die Übernahme braucht'
return 1
fi
if ! wireguard_handshake_proven 10; then
CLUPILOT_SECTION_NOTE="kein Handshake mit $(api_host) über den Tunnel. Die Datei steht, der Tunnel nicht — und eine geschriebene Datei ist kein Tunnel."
return 1
fi
CLUPILOT_SECTION_NOTE="Handshake mit $(api_host) bewiesen, wg0 systemd-freigegeben"
return 0
}
main() {
parse_arguments "$@"
@ -734,7 +780,13 @@ main() {
die 'Abbruch bei der Netzumstellung.'
fi
# run_section wireguard_joined section_wireguard_joined # Task 6
if ! run_section wireguard_joined section_wireguard_joined; then
die 'Abbruch: ohne Tunnel gibt es keinen Rückweg zu CluPilot.'
fi
# Ab hier existiert ein Weg. Die ganze Vorgeschichte geht jetzt raus.
flush_reports
# run_section traefik_running section_traefik_running # Task 7
# run_section template_built section_template_built # Task 8
# run_section registered section_registered # Task 9

View File

@ -253,6 +253,125 @@ EOF
fi
}
# ---------------------------------------------------------------------------
# WireGuard
# ---------------------------------------------------------------------------
# Netzadresse zu einer Adresse mit Präfix: 10.66.0.11/24 -> 10.66.0.0/24
#
# Gerechnet statt geraten. `AllowedIPs` mit der Wirtsadresse statt der
# Netzadresse ließe den Tunnel handshaken und danach jeden anderen Teilnehmer
# unerreichbar — ein Fehler, der aussieht wie ein Routingproblem irgendwo sonst.
ipv4_network() {
_ip="${1%%/*}"
_prefix="${1##*/}"
IFS=. read -r _a _b _c _d <<EOF
$_ip
EOF
_num=$(( (_a << 24) + (_b << 16) + (_c << 8) + _d ))
_mask=$(( (0xFFFFFFFF << (32 - _prefix)) & 0xFFFFFFFF ))
_net=$(( _num & _mask ))
printf '%d.%d.%d.%d/%d' \
$(( (_net >> 24) & 255 )) $(( (_net >> 16) & 255 )) \
$(( (_net >> 8) & 255 )) $(( _net & 255 )) "$_prefix"
}
# Die Tunneladresse von CluPilot — dieselbe, die `--api` trägt.
#
# Sie ist auch das Ziel für den Handshake-Beweis: wenn CluPilot über den Tunnel
# antwortet, steht der Tunnel. Ein eigener Parameter dafür wäre ein zweiter Wert,
# der dasselbe meint und irgendwann auseinanderläuft.
api_host() {
printf '%s' "$CLUPILOT_API" | sed -e 's#^[a-z]*://##' -e 's#[:/].*##'
}
render_wireguard_config() {
_subnet="$(ipv4_network "$CLUPILOT_WG_IP")"
cat <<EOF
[Interface]
Address = ${CLUPILOT_WG_IP}
PrivateKey = ${CLUPILOT_WG_PRIVATE}
[Peer]
PublicKey = ${CLUPILOT_HUB_PUBKEY}
Endpoint = ${CLUPILOT_HUB_ENDPOINT}
AllowedIPs = ${_subnet}
PersistentKeepalive = 25
EOF
}
# Bringt wg0 in den Zustand, den die Übernahme braucht: jetzt oben UND nach
# einem Neustart oben, mit der Konfiguration, die gerade geschrieben wurde.
#
# Drei Zustände, getrennt behandelt, weil die eine Zeile, die das hier ersetzt —
# `systemctl enable --now wg-quick@wg0 || wg-quick up wg0` — zwei davon falsch
# machte: der Rückfall brachte die Schnittstelle hoch OHNE die systemd-Freigabe,
# also kam der Tunnel nach dem Neustart nicht wieder; und beim nächsten Versuch
# scheiterten beide Hälften an „wg0 already exists", also lief der Schritt ewig
# gegen einen Tunnel, der in Wahrheit funktionierte.
#
# **enable vor start**: `enable` ist das, was den Neustart überlebt, und es wird
# auch dann geprüft und nachgeholt, wenn die Schnittstelle schon anderweitig
# oben ist.
bring_up_wireguard() {
_config_changed="$1"
_unit='wg-quick@wg0'
_present=0
ip link show wg0 >/dev/null 2>&1 && _present=1
# `is-enabled` wird nach seinem Rückgabewert gefragt, nicht nach seinem
# Wortlaut: eine nie freigegebene Template-Unit („disabled", 1) und eine, die
# es noch nicht gibt, werden damit gleich behandelt.
if ! systemctl is-enabled --quiet "$_unit" 2>/dev/null; then
if ! systemctl enable "$_unit" >/dev/null 2>&1; then
log "konnte ${_unit} nicht freigeben — wg0 überlebte keinen Neustart"
return 1
fi
fi
if [ "$_present" -eq 0 ]; then
systemctl start "$_unit" >/dev/null 2>&1 || { log 'wg0 ließ sich nicht starten'; return 1; }
return 0
fi
# Vorhanden und unverändert: die laufende Schnittstelle passt schon zur
# Datei. Hier neu zu starten ist das, was einen funktionierenden Tunnel
# kaputt aussehen ließ.
[ "$_config_changed" -eq 0 ] && return 0
# Vorhanden mit geänderter Konfiguration: Neustart, NICHT `wg syncconf`.
# syncconf wendet nur Peers an; eine korrigierte `Address` oder `AllowedIPs`
# bliebe still wirkungslos — genau die Sorte „die Datei hat sich geändert und
# nichts ist passiert".
systemctl restart "$_unit" >/dev/null 2>&1 || { log 'Neustart von wg0 mit der korrigierten Konfiguration fehlgeschlagen'; return 1; }
return 0
}
# Der Tunnel gilt erst mit BEWIESENEM Handshake, nicht mit geschriebener Datei.
#
# Eine gespeicherte Tunneladresse ohne Beweis machte jeden weiteren
# Verbindungsversuch unbrauchbar, und die Rettung war Datenbank-Handarbeit.
wireguard_handshake_proven() {
_target="$(api_host)"
[ -n "$_target" ] || return 1
_tries="${1:-10}"
while [ "$_tries" -gt 0 ]; do
if ping -c1 -W2 "$_target" >/dev/null 2>&1; then
return 0
fi
_tries=$((_tries - 1))
sleep 3
done
return 1
}
# ---------------------------------------------------------------------------
# Die Datacenter-Firewall
# ---------------------------------------------------------------------------

View File

@ -347,7 +347,7 @@ der Kunden-VMs wirkungslos, und `ConfigureProxmox` hielt das als eigenen Fund fe
### Task 6: `wireguard_joined` — ab hier ist CluPilot erreichbar
- [ ] **Step 1: Schreiben**
- [x] **Step 1: Schreiben**
`wg0` aus den mitgegebenen Werten, **systemd-aktiviert** — lies
`ConfigureWireguard.php` dazu: ein `||`-Rückfall verdeckte dort die fehlende
@ -363,7 +363,7 @@ Sobald der Handshake steht: `flush_reports` — alles bis hierher wird nachgerei
- [ ] **Step 2: Auf echter Hardware prüfen.** In der Konsole müssen jetzt **alle
fünf** bisherigen Abschnitte auftauchen, mit ihren echten Zeitstempeln.
- [ ] **Step 3: Committen.** `Join the tunnel, then tell the console everything so far`
- [x] **Step 3: Committen.** `Join the tunnel, then tell the console everything so far`
---