From dc8a2df9d24aaf40323bd7da5a00d75f3cf1c1e1 Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Mon, 13 Jul 2026 09:15:37 +0200 Subject: [PATCH] feat: initialize provisioning network --- scripts/modules/05_network.sh | 497 ++++++++++++++++++++++++++++++++++ 1 file changed, 497 insertions(+) create mode 100755 scripts/modules/05_network.sh diff --git a/scripts/modules/05_network.sh b/scripts/modules/05_network.sh new file mode 100755 index 0000000..3e0286b --- /dev/null +++ b/scripts/modules/05_network.sh @@ -0,0 +1,497 @@ +#!/usr/bin/env bash + +# Tuxflotte Installer +# Phase 2 – Netzwerkinitialisierung +# +# Unterstützt zunächst: +# - Ethernet über DHCP +# - bereits aktive NetworkManager-Verbindungen +# - WPA2/WPA3 Personal +# - interaktive WLAN-Auswahl +# - Prüfung des Tuxflotte-Servers +# - geschützte Runtime-Ablage des aktiven Netzwerkprofils + +set -Eeuo pipefail + +readonly SCRIPT_NAME="${0##*/}" + +readonly RUNTIME_DIR="/run/tuxflotte/network" +readonly STATE_FILE="${RUNTIME_DIR}/state.env" +readonly CONNECTION_EXPORT="${RUNTIME_DIR}/connection.nmconnection" + +readonly SERVER_URL="${TUXFLOTTE_SERVER_URL:-https://anode.tuxflotte.de/health}" +readonly SERVER_TIMEOUT="${TUXFLOTTE_SERVER_TIMEOUT:-10}" + +readonly NMCLI="${NMCLI:-nmcli}" +readonly CURL="${CURL:-curl}" + +log() { + printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2 +} + +warn() { + printf '[%s] WARNUNG: %s\n' "${SCRIPT_NAME}" "$*" >&2 +} + +fatal() { + printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2 + exit 1 +} + +require_root() { + if [[ "${EUID}" -ne 0 ]]; then + fatal "Das Netzwerkmodul muss als root ausgeführt werden." + fi +} + +require_command() { + local command_name="$1" + + command -v "${command_name}" >/dev/null 2>&1 || + fatal "Benötigtes Programm nicht gefunden: ${command_name}" +} + +prepare_runtime_directory() { + install -d \ + --mode=0700 \ + --owner=root \ + --group=root \ + "${RUNTIME_DIR}" + + rm -f -- "${STATE_FILE}" "${CONNECTION_EXPORT}" +} + +networkmanager_is_running() { + "${NMCLI}" -t -f RUNNING general 2>/dev/null | + grep -qx 'running' +} + +start_networkmanager_if_possible() { + if networkmanager_is_running; then + return 0 + fi + + log "NetworkManager ist noch nicht aktiv." + + if command -v systemctl >/dev/null 2>&1; then + log "Versuche NetworkManager zu starten." + systemctl start NetworkManager.service 2>/dev/null || true + fi + + networkmanager_is_running || + fatal "NetworkManager konnte nicht verwendet werden." +} + +enable_networking() { + "${NMCLI}" networking on >/dev/null 2>&1 || true + "${NMCLI}" radio wifi on >/dev/null 2>&1 || true +} + +device_has_ipv4() { + local device="$1" + + "${NMCLI}" -g IP4.ADDRESS device show "${device}" 2>/dev/null | + grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/' +} + +get_active_device() { + local device + local type + + while IFS=: read -r device type _; do + case "${type}" in + ethernet|wifi) + ;; + *) + continue + ;; + esac + + [[ -e "/sys/class/net/${device}/device" ]] || continue + + if device_has_ipv4 "${device}"; then + printf '%s\n' "${device}" + return 0 + fi + done < <( + "${NMCLI}" \ + --terse \ + --fields DEVICE,TYPE,STATE \ + device status + ) + + return 1 +} + + +get_active_connection_name() { + local device="$1" + + "${NMCLI}" \ + --get-values GENERAL.CONNECTION \ + device show "${device}" 2>/dev/null | + head -n 1 +} + +get_device_type() { + local device="$1" + + "${NMCLI}" \ + --get-values GENERAL.TYPE \ + device show "${device}" 2>/dev/null | + head -n 1 +} + +server_is_reachable() { + "${CURL}" \ + --silent \ + --show-error \ + --fail \ + --location \ + --connect-timeout "${SERVER_TIMEOUT}" \ + --max-time "${SERVER_TIMEOUT}" \ + --output /dev/null \ + "${SERVER_URL}" +} + +check_existing_connection() { + local device + + device="$(get_active_device || true)" + + if [[ -z "${device}" ]]; then + return 1 + fi + + if ! device_has_ipv4 "${device}"; then + warn "Interface ${device} ist verbunden, besitzt aber keine IPv4-Adresse." + return 1 + fi + + log "Aktive Netzwerkverbindung über ${device} gefunden." + + if server_is_reachable; then + log "Tuxflotte-Server ist erreichbar." + return 0 + fi + + warn "Netzwerk ist aktiv, aber der Tuxflotte-Server ist nicht erreichbar." + return 1 +} + +get_ethernet_devices() { + local device + + while IFS=: read -r device type state; do + [[ "${type}" == "ethernet" ]] || continue + [[ "${state}" != "unavailable" ]] || continue + [[ -e "/sys/class/net/${device}/device" ]] || continue + + printf '%s\n' "${device}" + done < <( + "${NMCLI}" \ + --terse \ + --fields DEVICE,TYPE,STATE \ + device status + ) +} + +try_ethernet() { + local device + + while IFS= read -r device; do + [[ -n "${device}" ]] || continue + + log "Prüfe Ethernet-Interface ${device}." + + "${NMCLI}" device connect "${device}" >/dev/null 2>&1 || true + + if device_has_ipv4 "${device}"; then + log "Ethernet-Verbindung über ${device} hergestellt." + + if server_is_reachable; then + log "Tuxflotte-Server ist über Ethernet erreichbar." + return 0 + fi + + warn "Ethernet besitzt eine IP-Adresse, aber der Server ist nicht erreichbar." + fi + done < <(get_ethernet_devices) + + return 1 +} + +get_wifi_device() { + "${NMCLI}" \ + --terse \ + --fields DEVICE,TYPE,STATE \ + device status | + awk -F: '$2 == "wifi" && $3 != "unavailable" { print $1; exit }' +} + +scan_wifi_networks() { + local wifi_device="$1" + + "${NMCLI}" device wifi rescan ifname "${wifi_device}" >/dev/null 2>&1 || + true + + sleep 2 + + "${NMCLI}" \ + --terse \ + --escape yes \ + --fields SSID,SIGNAL,SECURITY \ + device wifi list \ + ifname "${wifi_device}" | + awk -F: ' + $1 != "" && !seen[$1]++ { + printf "%s\t%s\t%s\n", $1, $2, $3 + } + ' | + sort -t $'\t' -k2,2nr +} + +choose_wifi_ssid() { + local wifi_device="$1" + local -a networks=() + local entry + local choice + local index=1 + + while IFS= read -r entry; do + [[ -n "${entry}" ]] && networks+=("${entry}") + done < <(scan_wifi_networks "${wifi_device}") + + if [[ "${#networks[@]}" -eq 0 ]]; then + warn "Keine sichtbaren WLAN-Netze gefunden." + + read -r -p "Versteckte SSID manuell eingeben oder leer abbrechen: " WIFI_SSID + + [[ -n "${WIFI_SSID}" ]] + return + fi + + printf '\nVerfügbare WLAN-Netze:\n\n' >&2 + + for entry in "${networks[@]}"; do + IFS=$'\t' read -r ssid signal security <<<"${entry}" + + printf ' %2d) %-32s Signal: %-3s Sicherheit: %s\n' \ + "${index}" \ + "${ssid}" \ + "${signal}" \ + "${security:-offen}" >&2 + + ((index += 1)) + done + + printf '\n' >&2 + read -r -p "WLAN auswählen [1-${#networks[@]}], m = manuell, q = abbrechen: " choice + + case "${choice}" in + q|Q) + return 1 + ;; + m|M) + read -r -p "SSID: " WIFI_SSID + [[ -n "${WIFI_SSID}" ]] + ;; + *) + if [[ ! "${choice}" =~ ^[0-9]+$ ]] || + (( choice < 1 || choice > ${#networks[@]} )); then + warn "Ungültige Auswahl." + return 1 + fi + + IFS=$'\t' read -r WIFI_SSID _ _ <<<"${networks[choice - 1]}" + ;; + esac +} + +wifi_network_is_open() { + local wifi_device="$1" + local ssid="$2" + local security + + security="$( + "${NMCLI}" \ + --terse \ + --escape no \ + --fields SSID,SECURITY \ + device wifi list \ + ifname "${wifi_device}" | + awk -F: -v wanted="${ssid}" ' + $1 == wanted { + print $2 + exit + } + ' + )" + + [[ -z "${security}" || "${security}" == "--" ]] +} + +connect_wifi() { + local wifi_device="$1" + + choose_wifi_ssid "${wifi_device}" || + return 1 + + log "Verbinde mit WLAN '${WIFI_SSID}'." + + if wifi_network_is_open "${wifi_device}" "${WIFI_SSID}"; then + if ! "${NMCLI}" \ + device wifi connect "${WIFI_SSID}" \ + ifname "${wifi_device}" \ + >/dev/null; then + + warn "Verbindung mit dem offenen WLAN konnte nicht hergestellt werden." + return 1 + fi + else + log "NetworkManager fragt die WLAN-Zugangsdaten geschützt ab." + + if ! "${NMCLI}" \ + --ask \ + device wifi connect "${WIFI_SSID}" \ + ifname "${wifi_device}" \ + >/dev/null; then + + warn "WLAN-Anmeldung ist fehlgeschlagen." + return 1 + fi + fi + + if ! device_has_ipv4 "${wifi_device}"; then + warn "WLAN-Verbindung besitzt keine IPv4-Adresse." + return 1 + fi + + log "WLAN-Verbindung wurde hergestellt." + + if ! server_is_reachable; then + warn "WLAN ist verbunden, aber der Tuxflotte-Server ist nicht erreichbar." + return 1 + fi + + log "Tuxflotte-Server ist über WLAN erreichbar." +} + +store_network_state() { + local device + local connection_name + local device_type + local connection_uuid="" + + device="$(get_active_device)" || + fatal "Kein aktives Provisionierungsinterface gefunden." + + connection_name="$( + get_active_connection_name "${device}" || true + )" + + device_type="$(get_device_type "${device}")" + + if [[ -n "${connection_name}" && "${connection_name}" != "--" ]]; then + connection_uuid="$( + "${NMCLI}" \ + --get-values connection.uuid \ + connection show "${connection_name}" 2>/dev/null | + head -n 1 + )" + else + connection_name="" + warn "Interface ${device} wird nicht durch ein aktives NetworkManager-Profil verwaltet." + fi + + umask 077 + + { + printf 'TUXFLOTTE_NETWORK_DEVICE=%q\n' "${device}" + printf 'TUXFLOTTE_NETWORK_TYPE=%q\n' "${device_type}" + printf 'TUXFLOTTE_CONNECTION_NAME=%q\n' "${connection_name}" + printf 'TUXFLOTTE_CONNECTION_UUID=%q\n' "${connection_uuid}" + printf 'TUXFLOTTE_SERVER_URL=%q\n' "${SERVER_URL}" + } >"${STATE_FILE}" + + chmod 0600 "${STATE_FILE}" + + log "Netzwerkstatus wurde unter ${STATE_FILE} gespeichert." +} + +export_connection_profile() { + local device + local connection_name + local source_file + + device="$(get_active_device)" || + fatal "Kein aktives Provisionierungsinterface gefunden." + + connection_name="$( + get_active_connection_name "${device}" || true + )" + + if [[ -z "${connection_name}" || "${connection_name}" == "--" ]]; then + warn "Für Interface ${device} existiert kein aktives NetworkManager-Profil." + return 0 + fi + + source_file="$( + "${NMCLI}" \ + --get-values connection.filename \ + connection show "${connection_name}" 2>/dev/null | + head -n 1 + )" + + if [[ -z "${source_file}" || ! -f "${source_file}" ]]; then + warn "NetworkManager-Profil konnte nicht exportiert werden." + return 0 + fi + + install \ + --mode=0600 \ + --owner=root \ + --group=root \ + "${source_file}" \ + "${CONNECTION_EXPORT}" + + log "Aktives Verbindungsprofil wurde geschützt vorgemerkt." +} + +main() { + require_root + require_command "${NMCLI}" + require_command "${CURL}" + + prepare_runtime_directory + start_networkmanager_if_possible + enable_networking + + log "Prüfe vorhandene Netzwerkverbindungen." + + if check_existing_connection; then + : + elif try_ethernet; then + : + else + local wifi_device + + wifi_device="$(get_wifi_device || true)" + + if [[ -z "${wifi_device}" ]]; then + fatal "Keine funktionierende Ethernet-Verbindung und keine WLAN-Hardware gefunden." + fi + + log "Ethernet ist nicht verfügbar. WLAN-Initialisierung wird gestartet." + + connect_wifi "${wifi_device}" || + fatal "Es konnte keine Verbindung zum Tuxflotte-Server hergestellt werden." + fi + + store_network_state + export_connection_profile + + log "Netzwerkinitialisierung erfolgreich abgeschlossen." +} + +main "$@"