CluPilotCloud/deploy/bootstrap/lib/bridge.sh

185 lines
7.5 KiB
Bash

# shellcheck shell=sh
#
# Die Brücke über die primäre Netzkarte — Erkennen, Sichern, Zeitgeber, Bauen,
# Nachsehen.
#
# ---------------------------------------------------------------------------
# Warum das eine eigene Datei ist
# ---------------------------------------------------------------------------
#
# Diese Zeilen standen in `network.sh`, geschrieben für den stillgelegten
# Rettungssystem-Weg, und borgten sich vier Dinge von woanders:
# `detect_primary_interface` aus `proxmox.sh`, `log`, `http_get` und
# `CLUPILOT_PROBE_URL` aus `clupilot-bootstrap.sh`. Der Debian-Weg lädt sie
# einzeln auf den Host und fährt sie dort — geborgte Helfer wären dann nicht da.
#
# Also allein lauffähig. `network.sh` behält seine WireGuard- und
# nftables-Hälfte; diese Datei wird von beiden Wegen benutzt und existiert genau
# einmal. Eine zweite Fassung wären zwei Installationen, die bei jeder
# Proxmox-Version nachgezogen werden müssten — und die zweite fiele erst auf,
# wenn jemand sie benutzt.
#
# ---------------------------------------------------------------------------
# Warum aus dem LAUFENDEN Zustand abgeleitet wird
# ---------------------------------------------------------------------------
#
# Nicht aus `/etc/network/interfaces`. Was läuft, ist bei jedem Anbieter gleich
# strukturiert; wie es aufgeschrieben wurde, nicht. Das ist der Teil, der
# Hetzner und netcup zugleich trägt.
# ---------------------------------------------------------------------------
# Stellschrauben — überschreibbar, damit die Bibliothek ohne Host prüfbar ist
# ---------------------------------------------------------------------------
CLUPILOT_BRIDGE="${CLUPILOT_BRIDGE:-vmbr0}"
CLUPILOT_WORK_DIR="${CLUPILOT_WORK_DIR:-/var/lib/clupilot/bridge}"
CLUPILOT_INTERFACES_FILE="${CLUPILOT_INTERFACES_FILE:-/etc/network/interfaces}"
CLUPILOT_INTERFACES_D="${CLUPILOT_INTERFACES_D:-/etc/network/interfaces.d}"
CLUPILOT_SYS_NET="${CLUPILOT_SYS_NET:-/sys/class/net}"
CLUPILOT_IP="${CLUPILOT_IP:-ip}"
CLUPILOT_IFRELOAD="${CLUPILOT_IFRELOAD:-ifreload}"
CLUPILOT_PROBE_URL="${CLUPILOT_PROBE_URL:-http://deb.debian.org/}"
CLUPILOT_ROLLBACK_UNIT="${CLUPILOT_ROLLBACK_UNIT:-clupilot-network-rollback}"
CLUPILOT_NET_BACKUP="${CLUPILOT_NET_BACKUP:-/var/lib/clupilot/interfaces.vor-der-bruecke}"
CLUPILOT_WG_HUB_PUBKEY="${CLUPILOT_WG_HUB_PUBKEY:-}"
CLUPILOT_WG_HANDSHAKE_MAX_AGE="${CLUPILOT_WG_HANDSHAKE_MAX_AGE:-180}"
# ---------------------------------------------------------------------------
# Die zwei Helfer, die diese Datei früher geborgt hat
# ---------------------------------------------------------------------------
#
# Unter Definitionsschutz: `clupilot-bootstrap.sh` bringt eigene mit, und wer
# zuerst da ist, behält recht. Sonst überschriebe das Laden dieser Bibliothek
# die Protokollierung des Rettungssystem-Wegs.
if ! command -v log >/dev/null 2>&1; then
log() {
printf '%s %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$1"
}
fi
if ! command -v http_get >/dev/null 2>&1; then
http_get() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 3 --retry-delay 2 --max-time 20 "$1"
else
wget -qO- --timeout=20 "$1"
fi
}
fi
# ---------------------------------------------------------------------------
# Feststellen, was da ist
# ---------------------------------------------------------------------------
# Die Schnittstelle, über die die Vorgabe-Route geht. Nicht „die erste, die
# nicht lo heißt": eine Maschine mit zwei Karten hat oft eine angeschlossene und
# eine nicht.
detect_primary_interface() {
"$CLUPILOT_IP" -4 route show default 2>/dev/null \
| awk '{ for (i = 1; i < NF; i++) if ($i == "dev") { print $(i+1); exit } }'
}
# Ist das eine physische Karte?
#
# `bridge_ports` auf einem Bond oder einer bestehenden Bridge ist falsch — die
# Brücke nähme sich ihren eigenen Unterbau als Port, und was dabei herauskommt,
# ist von hier aus nicht mehr zu reparieren. VLAN-Geräte und Bonds haben keinen
# `device`-Verweis; die beiden weiteren Prüfungen sind der Gürtel dazu.
interface_is_physical() {
_if="${1:-}"
[ -n "$_if" ] || return 1
[ -e "${CLUPILOT_SYS_NET}/${_if}/device" ] || return 1
[ ! -d "${CLUPILOT_SYS_NET}/${_if}/bridge" ] || return 1
[ ! -d "${CLUPILOT_SYS_NET}/${_if}/bonding" ] || return 1
return 0
}
# Kam die Adresse per DHCP?
#
# Aus dem laufenden Zustand: der Kernel markiert eine geleaste Adresse mit
# `dynamic`. Das steht bei jedem Anbieter gleich da — anders als die Datei, in
# die er es geschrieben hat.
address_is_dynamic() {
"$CLUPILOT_IP" -4 -o addr show dev "$1" scope global 2>/dev/null \
| grep -q '[[:space:]]dynamic[[:space:]]'
}
# Wie der Anbieter das Netz aufzieht. Eine Brücke, die für den einen Fall
# richtig ist, nimmt den anderen vom Netz.
#
# - `dhcp` — die Adresse kommt per DHCP (Cloud-Produkte).
# - `routed` — geroutete Einzeladresse, Gateway AUSSERHALB des eigenen Subnetzes
# (Hetzner-dediziert mit /32). Braucht eine pointopoint-Route,
# sonst findet der Kernel das Gateway nicht.
# - `subnet` — gewöhnliches Subnetz, Gateway darin.
detect_network_style() {
_iface="${1:-}"
[ -n "$_iface" ] || _iface="$(detect_primary_interface)"
# Laufender Zustand zuerst. Die Datei des Anbieters ist nur das Zweitsignal
# — sie sagt, was jemand aufgeschrieben hat, nicht was gilt.
if address_is_dynamic "$_iface"; then
printf 'dhcp'
return 0
fi
if grep -qsE "iface[[:space:]]+${_iface}[[:space:]]+inet[[:space:]]+dhcp" \
"$CLUPILOT_INTERFACES_FILE" "${CLUPILOT_INTERFACES_D}"/* 2>/dev/null; then
printf 'dhcp'
return 0
fi
_cidr="$("$CLUPILOT_IP" -4 -o addr show dev "$_iface" scope global 2>/dev/null | awk '{ print $4; exit }')"
_gw="$("$CLUPILOT_IP" -4 route show default 2>/dev/null | awk '{ print $3; exit }')"
_prefix="${_cidr##*/}"
# /32 heißt: das eigene Subnetz besteht aus der eigenen Adresse. Ein Gateway
# darin kann es nicht geben.
if [ "$_prefix" = '32' ]; then
printf 'routed'
return 0
fi
# Liegt das Gateway im eigenen Subnetz? `ip route get` beantwortet das, ohne
# dass dieses Skript Netzmasken rechnen muss — und rechnet dabei mit
# derselben Logik, die der Kernel später anwendet.
if [ -n "$_gw" ] && "$CLUPILOT_IP" -4 route get "$_gw" 2>/dev/null | grep -q "dev ${_iface}.*src"; then
printf 'subnet'
return 0
fi
printf 'routed'
}
bridge_exists() {
"$CLUPILOT_IP" link show "${1:-$CLUPILOT_BRIDGE}" >/dev/null 2>&1
}
# Trägt die Brücke wirklich den Verkehr, oder existiert sie nur?
#
# Eine `vmbr0` ohne Adresse und ohne Vorgaberoute ist eine Brücke im Sinne von
# `ip link show` und sonst nichts. Der alte Schritt prüfte genau das und warf das
# Ergebnis weg; sein Kommentar behauptete, er halte die Abwesenheit fest, und er
# hielt nichts fest.
bridge_carries_default_route() {
_want="${1:-$CLUPILOT_BRIDGE}"
_dev="$("$CLUPILOT_IP" -4 route show default 2>/dev/null \
| awk '{ for (i = 1; i < NF; i++) if ($i == "dev") { print $(i+1); exit } }')"
[ "$_dev" = "$_want" ]
}
bridge_has_address() {
[ -n "$("$CLUPILOT_IP" -4 -o addr show dev "${1:-$CLUPILOT_BRIDGE}" scope global 2>/dev/null | awk '{ print $4; exit }')" ]
}
# Die drei zusammen. „Da" reicht nicht — sie muss tragen.
bridge_is_up() {
bridge_exists "$CLUPILOT_BRIDGE" \
&& bridge_carries_default_route "$CLUPILOT_BRIDGE" \
&& bridge_has_address "$CLUPILOT_BRIDGE"
}