#!/bin/bash #=============================================================================== # pve-netconfig.sh # # Gestion réseau pour hôte Proxmox VE 9 (Debian 13 Trixie) autonome : # - vmbr0 : DHCP avec fallback IP statique # - vmbr1 : DHCP sans fallback # - Génère /etc/network/interfaces # - Synchronise /etc/hosts avec les IP réellement obtenues (ip -j a) # # Usage : # pve-netconfig.sh apply-interfaces # (Ré)écrit /etc/network/interfaces # pve-netconfig.sh sync-hosts # Met à jour /etc/hosts seulement # pve-netconfig.sh check # Vérifie DHCP vmbr0 -> fallback si KO # pve-netconfig.sh status # Affiche l'état courant # # Auteur : # Licence : usage interne #=============================================================================== set -uo pipefail #=============================================================================== # C O N F I G U R A T I O N #=============================================================================== # --- Interfaces bridges gérées --------------------------------------------- # vmbr0 = LAN principal (management PVE) # vmbr1 = second réseau (DHCP seul, pas de fallback) BRIDGE_MAIN="vmbr0" BRIDGE_SECONDARY="vmbr1" # --- Ports physiques rattachés aux bridges --------------------------------- # Laisser vide ("") pour conserver la configuration existante détectée # dans /etc/network/interfaces (recommandé après une install PVE standard). # Sinon, forcer explicitement : PORT_MAIN="eno1" PORT_MAIN="" PORT_SECONDARY="" # --- Fallback statique pour vmbr0 (si le DHCP ne répond pas) ---------------- FALLBACK_IP="192.168.1.250" FALLBACK_CIDR="24" FALLBACK_GATEWAY="192.168.1.1" FALLBACK_DNS="192.168.1.1 9.9.9.9" # --- Comportement DHCP ------------------------------------------------------ DHCP_TIMEOUT=25 # Secondes d'attente d'un bail DHCP avant fallback DHCP_RETRY_INTERVAL=300 # Secondes entre 2 tentatives de retour au DHCP # (0 = ne jamais retenter, reste en statique) # --- /etc/hosts ------------------------------------------------------------- # Nom court + FQDN de l'hôte. Par défaut déduits du système. HOSTNAME_SHORT="$(hostname -s)" HOSTNAME_FQDN="$(hostname -f 2>/dev/null || echo "${HOSTNAME_SHORT}.local")" # Alias supplémentaires ajoutés à la ligne de l'IP principale HOSTS_EXTRA_ALIASES="pve" # Marqueurs délimitant le bloc géré dans /etc/hosts HOSTS_BEGIN="# >>> pve-netconfig BEGIN (ne pas editer manuellement) >>>" HOSTS_END="# <<< pve-netconfig END <<<" # --- Divers ----------------------------------------------------------------- LOG_TAG="pve-netconfig" STATE_DIR="/var/lib/pve-netconfig" LOCK_FILE="/run/pve-netconfig.lock" INTERFACES_FILE="/etc/network/interfaces" HOSTS_FILE="/etc/hosts" BACKUP_DIR="/var/backups/pve-netconfig" # Mettre à 1 pour appliquer les changements réseau à chaud (ifreload -a) # après réécriture de /etc/network/interfaces. Sinon, reboot requis. AUTO_RELOAD=1 #=============================================================================== # F I N C O N F I G U R A T I O N #=============================================================================== umask 022 mkdir -p "$STATE_DIR" "$BACKUP_DIR" log() { logger -t "$LOG_TAG" -p daemon.info -- "$*"; echo "[*] $*"; } warn() { logger -t "$LOG_TAG" -p daemon.warning -- "$*"; echo "[!] $*" >&2; } err() { logger -t "$LOG_TAG" -p daemon.err -- "$*"; echo "[X] $*" >&2; } require_root() { if [[ $EUID -ne 0 ]]; then err "Ce script doit être exécuté en root." exit 1 fi } # Verrou pour éviter les exécutions concurrentes (hook DHCP + timer + boot) acquire_lock() { exec 9>"$LOCK_FILE" if ! flock -w 30 9; then warn "Impossible d'obtenir le verrou ($LOCK_FILE), abandon." exit 0 fi } backup_file() { local f="$1" [[ -f "$f" ]] || return 0 local ts base ts="$(date +%Y%m%d-%H%M%S)" base="$(basename "$f")" cp -a "$f" "${BACKUP_DIR}/${base}.${ts}" # Rotation : conserver les 20 dernières sauvegardes par fichier ls -1t "${BACKUP_DIR}/${base}."* 2>/dev/null | tail -n +21 | xargs -r rm -f } #------------------------------------------------------------------------------- # Détection des ports physiques déjà déclarés dans /etc/network/interfaces #------------------------------------------------------------------------------- detect_bridge_ports() { local br="$1" awk -v br="$br" ' $1=="iface" && $2==br {inblk=1; next} inblk && $1=="iface" {inblk=0} inblk && ($1=="bridge-ports" || $1=="bridge_ports") { $1=""; sub(/^[ \t]+/,""); print; exit } ' "$INTERFACES_FILE" 2>/dev/null } resolve_ports() { if [[ -z "$PORT_MAIN" ]]; then PORT_MAIN="$(detect_bridge_ports "$BRIDGE_MAIN")" fi if [[ -z "$PORT_SECONDARY" ]]; then PORT_SECONDARY="$(detect_bridge_ports "$BRIDGE_SECONDARY")" fi [[ -z "$PORT_MAIN" ]] && PORT_MAIN="none" [[ -z "$PORT_SECONDARY" ]] && PORT_SECONDARY="none" } #------------------------------------------------------------------------------- # Génération de /etc/network/interfaces #------------------------------------------------------------------------------- apply_interfaces() { require_root resolve_ports log "Génération de $INTERFACES_FILE (${BRIDGE_MAIN}=dhcp+fallback, ${BRIDGE_SECONDARY}=dhcp)" backup_file "$INTERFACES_FILE" local tmp tmp="$(mktemp /etc/network/.interfaces.XXXXXX)" cat > "$tmp" <> "$tmp" <> "$tmp" <> "$tmp" </dev/null; then echo "source /etc/network/interfaces.d/*" >> "$tmp" fi chmod 0644 "$tmp" mv "$tmp" "$INTERFACES_FILE" log "$INTERFACES_FILE écrit." if [[ "$AUTO_RELOAD" == "1" ]]; then if command -v ifreload >/dev/null 2>&1; then log "Rechargement à chaud (ifreload -a)..." ifreload -a || warn "ifreload a retourné une erreur, vérifier 'journalctl -u networking'." else warn "ifreload absent (ifupdown2 non installé ?) — un redémarrage sera nécessaire." fi fi } #------------------------------------------------------------------------------- # Récupération des IPv4 via 'ip -j a' (JSON, parsé avec jq) #------------------------------------------------------------------------------- get_ipv4() { local ifc="$1" ip -4 -j addr show dev "$ifc" 2>/dev/null \ | jq -r '.[0].addr_info[]? | select(.family=="inet" and .scope=="global") | .local' \ | head -n1 } get_ipv4_cidr() { local ifc="$1" ip -4 -j addr show dev "$ifc" 2>/dev/null \ | jq -r '.[0].addr_info[]? | select(.family=="inet" and .scope=="global") | "\(.local)/\(.prefixlen)"' \ | head -n1 } # Détecte si l'adresse courante de vmbr0 provient d'un bail DHCP has_dhcp_lease() { local ifc="$1" # dhclient (isc-dhcp-client) if [[ -f "/var/lib/dhcp/dhclient.${ifc}.leases" ]] \ && grep -q "fixed-address" "/var/lib/dhcp/dhclient.${ifc}.leases" 2>/dev/null; then pgrep -f "dhclient.*${ifc}" >/dev/null 2>&1 && return 0 fi # dhcpcd if [[ -f "/var/lib/dhcpcd/${ifc}.lease" ]] || [[ -f "/var/lib/dhcpcd5/dhcpcd-${ifc}.lease" ]]; then pgrep -f "dhcpcd.*${ifc}" >/dev/null 2>&1 && return 0 fi return 1 } is_static_fallback_active() { [[ -f "${STATE_DIR}/fallback_active" ]] } #------------------------------------------------------------------------------- # Application / retrait du fallback statique sur vmbr0 #------------------------------------------------------------------------------- apply_fallback() { log "Application du fallback statique sur ${BRIDGE_MAIN} : ${FALLBACK_IP}/${FALLBACK_CIDR}" # Arrêt du client DHCP sur cette interface pkill -f "dhclient.*${BRIDGE_MAIN}" 2>/dev/null if command -v dhcpcd >/dev/null 2>&1; then dhcpcd -k "$BRIDGE_MAIN" 2>/dev/null fi # Purge des adresses IPv4 globales existantes ip -4 addr flush dev "$BRIDGE_MAIN" scope global 2>/dev/null ip addr add "${FALLBACK_IP}/${FALLBACK_CIDR}" dev "$BRIDGE_MAIN" ip link set "$BRIDGE_MAIN" up if [[ -n "$FALLBACK_GATEWAY" ]]; then ip route replace default via "$FALLBACK_GATEWAY" dev "$BRIDGE_MAIN" 2>/dev/null \ || warn "Impossible d'installer la route par défaut via ${FALLBACK_GATEWAY}" fi write_resolv_conf $FALLBACK_DNS touch "${STATE_DIR}/fallback_active" log "Fallback actif. Interface de management : https://${FALLBACK_IP}:8006" } clear_fallback() { if is_static_fallback_active; then log "Bail DHCP retrouvé sur ${BRIDGE_MAIN} — retrait du fallback statique." ip addr del "${FALLBACK_IP}/${FALLBACK_CIDR}" dev "$BRIDGE_MAIN" 2>/dev/null rm -f "${STATE_DIR}/fallback_active" fi } write_resolv_conf() { # N'écrase resolv.conf que si ce n'est pas un lien géré (resolvconf/systemd) if [[ -L /etc/resolv.conf ]]; then warn "/etc/resolv.conf est un lien symbolique — DNS non modifié." return 0 fi backup_file /etc/resolv.conf { echo "# Généré par pve-netconfig.sh (fallback statique) $(date -Is)" local domain domain="${HOSTNAME_FQDN#*.}" [[ "$domain" != "$HOSTNAME_FQDN" ]] && echo "search ${domain}" local ns for ns in "$@"; do echo "nameserver ${ns}" done } > /etc/resolv.conf } #------------------------------------------------------------------------------- # Tentative de récupération d'un bail DHCP sur vmbr0 #------------------------------------------------------------------------------- try_dhcp() { local ifc="$1" log "Tentative d'obtention d'un bail DHCP sur ${ifc} (timeout ${DHCP_TIMEOUT}s)..." if command -v dhclient >/dev/null 2>&1; then timeout "$((DHCP_TIMEOUT + 5))" \ dhclient -1 -timeout "$DHCP_TIMEOUT" -v "$ifc" >/dev/null 2>&1 elif command -v dhcpcd >/dev/null 2>&1; then timeout "$((DHCP_TIMEOUT + 5))" \ dhcpcd -t "$DHCP_TIMEOUT" "$ifc" >/dev/null 2>&1 else err "Aucun client DHCP trouvé (dhclient / dhcpcd). Installer isc-dhcp-client." return 1 fi [[ -n "$(get_ipv4 "$ifc")" ]] } #------------------------------------------------------------------------------- # Contrôle principal : DHCP ou fallback #------------------------------------------------------------------------------- do_check() { require_root acquire_lock # --- Attente du lien physique (link carrier) ---------------------------- local waited=0 while [[ $waited -lt 30 ]]; do if [[ "$(cat "/sys/class/net/${BRIDGE_MAIN}/operstate" 2>/dev/null)" == "up" ]]; then break fi sleep 2 waited=$((waited + 2)) done local current_ip current_ip="$(get_ipv4 "$BRIDGE_MAIN")" if [[ -n "$current_ip" ]] && has_dhcp_lease "$BRIDGE_MAIN"; then # DHCP OK clear_fallback log "${BRIDGE_MAIN} : bail DHCP actif (${current_ip})." elif is_static_fallback_active; then # On est déjà en fallback : on retente périodiquement le DHCP if [[ "$DHCP_RETRY_INTERVAL" != "0" ]]; then log "Fallback actif — nouvelle tentative DHCP sur ${BRIDGE_MAIN}." if try_dhcp "$BRIDGE_MAIN"; then clear_fallback log "Retour en DHCP réussi : $(get_ipv4 "$BRIDGE_MAIN")" else log "Toujours pas de serveur DHCP — maintien du fallback ${FALLBACK_IP}." fi fi else # Pas d'IP et pas encore de fallback warn "${BRIDGE_MAIN} : aucune adresse IPv4. Tentative DHCP..." if try_dhcp "$BRIDGE_MAIN"; then log "Bail DHCP obtenu : $(get_ipv4 "$BRIDGE_MAIN")" else warn "Échec DHCP sur ${BRIDGE_MAIN} — bascule sur le fallback statique." apply_fallback fi fi # --- vmbr1 : DHCP uniquement, aucun fallback --------------------------- if ip link show "$BRIDGE_SECONDARY" >/dev/null 2>&1; then local ip2 ip2="$(get_ipv4 "$BRIDGE_SECONDARY")" if [[ -z "$ip2" ]]; then log "${BRIDGE_SECONDARY} : pas d'IP, tentative DHCP (sans fallback)." try_dhcp "$BRIDGE_SECONDARY" >/dev/null 2>&1 \ && log "${BRIDGE_SECONDARY} : $(get_ipv4 "$BRIDGE_SECONDARY")" \ || log "${BRIDGE_SECONDARY} : aucun bail DHCP (comportement attendu si non raccordé)." else log "${BRIDGE_SECONDARY} : ${ip2}" fi fi sync_hosts_locked } #------------------------------------------------------------------------------- # Mise à jour de /etc/hosts #------------------------------------------------------------------------------- sync_hosts_locked() { local ip_main ip_sec block tmp ip_main="$(get_ipv4 "$BRIDGE_MAIN")" ip_sec="$(get_ipv4 "$BRIDGE_SECONDARY")" # PVE ne DOIT jamais se retrouver sans résolution du hostname. if [[ -z "$ip_main" ]]; then if is_static_fallback_active; then ip_main="$FALLBACK_IP" else warn "Aucune IP sur ${BRIDGE_MAIN} — /etc/hosts conserve son contenu actuel." return 1 fi fi block="${HOSTS_BEGIN}\n" block+="${ip_main}\t${HOSTNAME_FQDN} ${HOSTNAME_SHORT}" local a for a in $HOSTS_EXTRA_ALIASES; do [[ "$a" == "$HOSTNAME_SHORT" ]] && continue block+=" ${a}" done block+="\n" if [[ -n "$ip_sec" && "$ip_sec" != "$ip_main" ]]; then block+="${ip_sec}\t${HOSTNAME_SHORT}-${BRIDGE_SECONDARY}\n" fi block+="${HOSTS_END}" tmp="$(mktemp /etc/.hosts.XXXXXX)" # 1) Base statique (loopback + IPv6) toujours présente cat > "$tmp" <> "$tmp" # 3) Conservation des entrées personnalisées hors bloc et hors base if [[ -f "$HOSTS_FILE" ]]; then awk -v b="$HOSTS_BEGIN" -v e="$HOSTS_END" ' $0==b {skip=1; next} $0==e {skip=0; next} skip {next} /^127\.0\.0\.1/ {next} /^::1|^fe00::|^ff00::|^ff02::/ {next} /^# The following lines are desirable/ {next} /^[[:space:]]*$/ {next} {print} ' "$HOSTS_FILE" >> "$tmp" fi # Vérification de cohérence avant remplacement if ! grep -qE "^${ip_main}[[:space:]]" "$tmp"; then err "Contrôle d'intégrité échoué, /etc/hosts non modifié." rm -f "$tmp" return 1 fi if [[ -f "$HOSTS_FILE" ]] && diff -q "$tmp" "$HOSTS_FILE" >/dev/null 2>&1; then rm -f "$tmp" log "/etc/hosts déjà à jour (${ip_main})." return 0 fi backup_file "$HOSTS_FILE" chmod 0644 "$tmp" mv "$tmp" "$HOSTS_FILE" log "/etc/hosts mis à jour : ${HOSTNAME_FQDN} -> ${ip_main}" # Recharge pveproxy/pvedaemon pour prendre en compte la nouvelle IP if systemctl is-active --quiet pveproxy; then systemctl reload-or-restart pveproxy pvedaemon 2>/dev/null \ || warn "Rechargement de pveproxy/pvedaemon impossible." fi } sync_hosts() { require_root acquire_lock sync_hosts_locked } #------------------------------------------------------------------------------- # Affichage de l'état #------------------------------------------------------------------------------- do_status() { echo "=== pve-netconfig : état ===" echo "Hostname : ${HOSTNAME_SHORT} (${HOSTNAME_FQDN})" printf "%-8s : %s\n" "$BRIDGE_MAIN" "$(get_ipv4_cidr "$BRIDGE_MAIN" || echo '(aucune)')" printf "%-8s : %s\n" "$BRIDGE_SECONDARY" "$(get_ipv4_cidr "$BRIDGE_SECONDARY" || echo '(aucune)')" echo -n "Mode vmbr0 : " if is_static_fallback_active; then echo "FALLBACK STATIQUE (${FALLBACK_IP}/${FALLBACK_CIDR})" elif has_dhcp_lease "$BRIDGE_MAIN"; then echo "DHCP" else echo "indéterminé" fi echo echo "--- Route par défaut ---" ip route show default echo echo "--- Bloc géré dans /etc/hosts ---" sed -n "/${HOSTS_BEGIN//\//\\/}/,/${HOSTS_END//\//\\/}/p" "$HOSTS_FILE" 2>/dev/null } #------------------------------------------------------------------------------- # Point d'entrée #------------------------------------------------------------------------------- case "${1:-}" in apply-interfaces) apply_interfaces ;; sync-hosts) sync_hosts ;; check) do_check ;; status) do_status ;; *) cat <