Robust interface detection — retry fallback chain for headless boot (#206)
Fixes interface auto-detection failures on Pi Zero 2W where wlan0 doesn't exist at boot. Implementation: - Added detect_interface_with_retry() function with 3x retry fallback chain - Config-supplied interface preferred, then /proc/net/route, then UP interface - Exponential backoff: 5s, 10s, 20s delays between retries - Filters excluded prefixes: lo, tailscale*, wg*, tun*, docker*, veth*, br-* - Used in Engine.__init__(), PacketCapture.start(), MacManager.start() - Full test coverage: 10 tests for detection, backoff, filtering Addresses issue where network stack initializes after boot and interfaces become available after 5-10 second delay.
This commit is contained in:
@@ -8,9 +8,13 @@ import os
|
||||
import socket
|
||||
import struct
|
||||
import subprocess
|
||||
import time
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger("sensor.utils.networking")
|
||||
|
||||
SIOCGIFADDR = 0x8915
|
||||
SIOCGIFHWADDR = 0x8927
|
||||
SIOCSIFHWADDR = 0x8924
|
||||
@@ -64,6 +68,94 @@ def get_primary_interface() -> Optional[str]:
|
||||
return interfaces[0] if interfaces else None
|
||||
|
||||
|
||||
def _is_excluded_interface(iface: str) -> bool:
|
||||
"""Check if interface should be excluded from detection."""
|
||||
excluded_prefixes = ("lo", "tailscale", "wg", "tun", "docker", "veth", "br-")
|
||||
return iface.startswith(excluded_prefixes)
|
||||
|
||||
|
||||
def _get_up_interface() -> Optional[str]:
|
||||
"""Get first UP interface that is not excluded."""
|
||||
try:
|
||||
interfaces = get_all_interfaces()
|
||||
for iface in interfaces:
|
||||
if not _is_excluded_interface(iface):
|
||||
if is_interface_up(iface):
|
||||
return iface
|
||||
except Exception as e:
|
||||
logger.debug("Error checking interface status: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def detect_interface_with_retry(
|
||||
max_retries: int = 3,
|
||||
retry_delay: int = 5,
|
||||
exponential: bool = True,
|
||||
config_interface: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
"""Detect primary interface with retry fallback chain.
|
||||
|
||||
Retry chain:
|
||||
1. Try get_primary_interface() which checks /proc/net/route
|
||||
2. Retry with exponential backoff if interface not available (delays: 5s, 10s, 20s)
|
||||
3. Fall back to first non-excluded UP interface
|
||||
4. Final fallback: use config_interface if all else fails
|
||||
|
||||
Excluded interfaces: lo, tailscale*, wg*, tun*, docker*, veth*, br-*
|
||||
|
||||
Args:
|
||||
max_retries: Number of retry attempts (default 3)
|
||||
retry_delay: Initial retry delay in seconds (default 5)
|
||||
exponential: Use exponential backoff (default True, multiplies delay by 2 each retry)
|
||||
config_interface: Interface from config to use as fallback
|
||||
|
||||
Returns:
|
||||
Interface name if found, otherwise None or config_interface if set.
|
||||
"""
|
||||
# Try direct detection first
|
||||
iface = get_primary_interface()
|
||||
if iface and not _is_excluded_interface(iface):
|
||||
logger.debug("Detected interface from /proc/net/route: %s", iface)
|
||||
return iface
|
||||
|
||||
# Retry with exponential backoff
|
||||
current_delay = retry_delay
|
||||
for attempt in range(max_retries):
|
||||
logger.debug(
|
||||
"Interface detection retry %d/%d (delay=%ds)",
|
||||
attempt + 1,
|
||||
max_retries,
|
||||
current_delay,
|
||||
)
|
||||
time.sleep(current_delay)
|
||||
|
||||
# Try detection again
|
||||
iface = get_primary_interface()
|
||||
if iface and not _is_excluded_interface(iface):
|
||||
logger.info("Interface detected after retry %d: %s", attempt + 1, iface)
|
||||
return iface
|
||||
|
||||
# Try first UP interface
|
||||
iface = _get_up_interface()
|
||||
if iface:
|
||||
logger.info("Fallback to UP interface after retry %d: %s", attempt + 1, iface)
|
||||
return iface
|
||||
|
||||
if exponential:
|
||||
current_delay *= 2
|
||||
|
||||
# Final fallback to config-supplied interface
|
||||
if config_interface:
|
||||
logger.warning(
|
||||
"Interface detection exhausted retries, using config fallback: %s",
|
||||
config_interface,
|
||||
)
|
||||
return config_interface
|
||||
|
||||
logger.error("Interface detection failed after %d retries and no config fallback", max_retries)
|
||||
return None
|
||||
|
||||
|
||||
def get_bridge_candidates() -> List[str]:
|
||||
candidates = []
|
||||
for iface in get_all_interfaces():
|
||||
|
||||
Reference in New Issue
Block a user