#!/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}" # Live auf echter Hardware gefunden (01.09.2026): eine vorkonfigurierte SSID # war im Moment des Scans nicht sichtbar (Kunde stand ungluecklich, Handy im # selben WLAN war aber verbunden) - bisher brach das Modul beim ersten # Fehlschlag sofort fatal ab. Ein Kunde kann den Standort wechseln oder ein # Netzwerkkabel einstecken, wenn er ein paar Sekunden Zeit dazu bekommt - # deshalb Wiederholungsversuche mit Pause statt sofortigem Abbruch. readonly MAX_CONNECTION_ATTEMPTS="${TUXFLOTTE_NETWORK_MAX_ATTEMPTS:-10}" readonly RETRY_DELAY_SECONDS="${TUXFLOTTE_NETWORK_RETRY_DELAY:-15}" 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." } connect_wifi_noninteractive() { local wifi_device="$1" local ssid="${TUXFLOTTE_WIFI_SSID}" log "Verbinde mit vorkonfiguriertem WLAN '${ssid}' (nicht-interaktiv)." if [[ -n "${TUXFLOTTE_WIFI_PSK:-}" ]]; then if ! "${NMCLI}" \ device wifi connect "${ssid}" \ password "${TUXFLOTTE_WIFI_PSK}" \ ifname "${wifi_device}" \ >/dev/null; then warn "WLAN-Anmeldung mit vorkonfigurierten Zugangsdaten ist fehlgeschlagen." return 1 fi else if ! "${NMCLI}" \ device wifi connect "${ssid}" \ ifname "${wifi_device}" \ >/dev/null; then warn "Verbindung mit dem offenen, vorkonfigurierten WLAN konnte nicht hergestellt werden." 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 || true )" 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." } attempt_connection() { if check_existing_connection; then return 0 fi if try_ethernet; then return 0 fi local wifi_device wifi_device="$(get_wifi_device || true)" if [[ -z "${wifi_device}" ]]; then warn "Keine funktionierende Ethernet-Verbindung und keine WLAN-Hardware gefunden." return 1 fi log "Ethernet ist nicht verfügbar. WLAN-Initialisierung wird gestartet." if [[ -n "${TUXFLOTTE_WIFI_SSID:-}" ]]; then connect_wifi_noninteractive "${wifi_device}" else connect_wifi "${wifi_device}" fi } # Wiederholt Ethernet-Prüfung + WLAN-Verbindungsversuch mit Pause dazwischen, # statt beim ersten Fehlschlag sofort abzubrechen - siehe Fund oben. Prueft # bei jedem Versuch wieder von vorne (nicht nur WLAN erneut), damit ein in # der Wartezeit eingestecktes Netzwerkkabel ebenfalls erkannt wird. establish_network_connection() { local attempt=1 while (( attempt <= MAX_CONNECTION_ATTEMPTS )); do if attempt_connection; then return 0 fi if (( attempt < MAX_CONNECTION_ATTEMPTS )); then warn "Verbindungsversuch ${attempt}/${MAX_CONNECTION_ATTEMPTS} fehlgeschlagen. Neuer Versuch in ${RETRY_DELAY_SECONDS}s - Gerät ggf. näher ans WLAN bringen oder ein Netzwerkkabel einstecken." sleep "${RETRY_DELAY_SECONDS}" fi ((attempt += 1)) done return 1 } main() { require_root require_command "${NMCLI}" require_command "${CURL}" prepare_runtime_directory start_networkmanager_if_possible enable_networking log "Prüfe vorhandene Netzwerkverbindungen." establish_network_connection || fatal "Nach ${MAX_CONNECTION_ATTEMPTS} Versuchen konnte keine Verbindung zum Tuxflotte-Server hergestellt werden." store_network_state export_connection_profile log "Netzwerkinitialisierung erfolgreich abgeschlossen." } main "$@"