// Network Manager - manages all network interfaces with priority-based logic package main import ( "fmt" "log" "os" "os/exec" "strings" "syscall" "time" ) const ( eth0Interface = "eth0" wlan0Interface = "wlan0" apFallbackDelay = 120 * time.Second // 120 sec without connection ) // NetworkState represents current network state type NetworkState int const ( StateNoNetwork NetworkState = iota StateEth0Online StateWLAN0ClientOnline StateWLAN0APFallback ) // NetworkManager manages all network interfaces with priority-based logic // Priority 1: eth0 (carrier detect) // Priority 2: wlan0 client (if configured) // Fallback: wlan0 AP (120 sec without connection AND without eth0 carrier) type NetworkManager struct { cfg *Config debug bool stopChan chan struct{} // Scanners reference (needed to stop WiFi scanner when using wlan0) scanners *ScannerManager // State tracking eth0Carrier bool eth0HasIP bool wlan0HasIP bool currentState NetworkState lastOnlineTime time.Time apRunning bool eth0DhcpPid int } // NewNetworkManager creates a new network manager func NewNetworkManager(cfg *Config, scanners *ScannerManager) *NetworkManager { return &NetworkManager{ cfg: cfg, debug: cfg.Debug, stopChan: make(chan struct{}), scanners: scanners, currentState: StateNoNetwork, lastOnlineTime: time.Now(), // Start timer immediately } } // UpdateConfig updates network manager configuration func (nm *NetworkManager) UpdateConfig(cfg *Config) { nm.cfg = cfg } // HasIP returns true if any interface has an IP address func (nm *NetworkManager) HasIP() bool { return nm.eth0HasIP || nm.wlan0HasIP } // HasCarrier returns true if eth0 has carrier func (nm *NetworkManager) HasCarrier() bool { return nm.eth0Carrier } // IsOnline returns true if device is online (eth0 or wlan0 client) func (nm *NetworkManager) IsOnline() bool { return nm.currentState == StateEth0Online || nm.currentState == StateWLAN0ClientOnline } // Start begins network management func (nm *NetworkManager) Start() { log.Println("[netmgr] Starting network manager...") log.Println("[netmgr] Priority: eth0 → wlan0 client → wlan0 AP fallback (120s)") // Start main monitoring loop go nm.monitorLoop() } // Stop stops the network manager func (nm *NetworkManager) Stop() { log.Println("[netmgr] Stopping network manager...") close(nm.stopChan) // Clean up all interfaces nm.stopEth0() nm.stopWLAN0Client() nm.stopAP() } // monitorLoop is the main network management loop // Polls every second and applies priority-based logic func (nm *NetworkManager) monitorLoop() { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-nm.stopChan: return case <-ticker.C: nm.tick() } } } // tick performs one iteration of network state check and management func (nm *NetworkManager) tick() { // Update state nm.eth0Carrier = nm.checkCarrier(eth0Interface) nm.eth0HasIP = nm.hasIP(eth0Interface) nm.wlan0HasIP = nm.hasIP(wlan0Interface) online := nm.eth0HasIP || nm.wlan0HasIP if online { nm.lastOnlineTime = time.Now() } // Priority 1: eth0 if nm.eth0Carrier { if !nm.eth0HasIP { // Carrier UP but no IP - configure network if nm.cfg.Network.Eth0.Static { log.Println("[netmgr] eth0 carrier UP - configuring static IP") nm.configureEth0Static() } else { log.Println("[netmgr] eth0 carrier UP - starting DHCP") nm.startEth0DHCP() } } // eth0 has priority - disable wlan0 client and AP if running if nm.currentState != StateEth0Online { log.Println("[netmgr] eth0 online - stopping wlan0 client/AP") nm.stopWLAN0Client() nm.stopAP() nm.currentState = StateEth0Online // Start WiFi scanner if enabled (wlan0 is free) if nm.cfg.WiFi.MonitorEnabled && !nm.scanners.IsWiFiRunning() { log.Println("[netmgr] eth0 online - starting WiFi scanner") nm.scanners.StartWiFi(nm.cfg.ZMQAddrWiFi, nm.cfg.WiFiIface) } } return } // No eth0 carrier - stop eth0 DHCP/IP if nm.eth0HasIP { log.Println("[netmgr] eth0 carrier DOWN - stopping network") nm.stopEth0() } // Priority 2: wlan0 client (if configured and eth0 is down) if nm.cfg.WiFi.ClientEnabled && nm.cfg.WiFi.SSID != "" { if !nm.wlan0HasIP { // Should connect log.Printf("[netmgr] Connecting wlan0 client to %s...", nm.cfg.WiFi.SSID) nm.stopAP() // Stop AP before connecting client if err := nm.connectWLAN0Client(); err != nil { log.Printf("[netmgr] wlan0 client connection failed: %v", err) // Will fallback to AP after 120s } } if nm.wlan0HasIP && nm.currentState != StateWLAN0ClientOnline { log.Println("[netmgr] wlan0 client online") nm.currentState = StateWLAN0ClientOnline nm.stopAP() // Ensure AP is stopped } return } // Fallback: wlan0 AP (if offline for 120 seconds and no eth0 carrier) timeSinceOnline := time.Since(nm.lastOnlineTime) if !online && !nm.eth0Carrier && timeSinceOnline >= apFallbackDelay { if !nm.apRunning { log.Printf("[netmgr] No network for %v - starting AP fallback", timeSinceOnline) nm.startAP() } // Keep trying wlan0 client even with AP running if nm.cfg.WiFi.ClientEnabled && nm.cfg.WiFi.SSID != "" { // Try to reconnect every 30 seconds if int(timeSinceOnline.Seconds())%30 == 0 { log.Println("[netmgr] AP running - retrying wlan0 client...") if err := nm.connectWLAN0Client(); err != nil { if nm.debug { log.Printf("[netmgr] wlan0 client retry failed: %v", err) } } else if nm.hasIP(wlan0Interface) { log.Println("[netmgr] wlan0 client connected - stopping AP") nm.stopAP() nm.currentState = StateWLAN0ClientOnline } } } } } // ======================================================================================= // eth0 management // ======================================================================================= func (nm *NetworkManager) startEth0DHCP() { // Stop any existing DHCP first nm.stopEth0DHCP() // Flush old IPs exec.Command("ip", "addr", "flush", "dev", eth0Interface).Run() // Bring interface up exec.Command("ifconfig", eth0Interface, "up").Run() // Start udhcpc pidFile := fmt.Sprintf("/var/run/udhcpc.%s.pid", eth0Interface) cmd := exec.Command("udhcpc", "-i", eth0Interface, "-b", "-p", pidFile) if err := cmd.Start(); err != nil { log.Printf("[netmgr] Failed to start DHCP on eth0: %v", err) return } time.Sleep(500 * time.Millisecond) // Read PID pidData, err := os.ReadFile(pidFile) if err == nil { fmt.Sscanf(string(pidData), "%d", &nm.eth0DhcpPid) } } func (nm *NetworkManager) configureEth0Static() { nm.stopEth0DHCP() // Bring interface up exec.Command("ifconfig", eth0Interface, "up").Run() // Configure IP if nm.cfg.Network.Eth0.Address != "" { cmd := exec.Command("ip", "addr", "add", nm.cfg.Network.Eth0.Address, "dev", eth0Interface) if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "exists") { log.Printf("[netmgr] Failed to configure IP %s on eth0: %v", nm.cfg.Network.Eth0.Address, err) return } log.Printf("[netmgr] Configured static IP %s on eth0", nm.cfg.Network.Eth0.Address) } // Configure gateway if nm.cfg.Network.Eth0.Gateway != "" { exec.Command("ip", "route", "del", "default").Run() if err := exec.Command("ip", "route", "add", "default", "via", nm.cfg.Network.Eth0.Gateway).Run(); err != nil { log.Printf("[netmgr] Failed to configure gateway %s: %v", nm.cfg.Network.Eth0.Gateway, err) } else { log.Printf("[netmgr] Configured gateway %s", nm.cfg.Network.Eth0.Gateway) } } // Configure DNS if nm.cfg.Network.Eth0.DNS != "" { dnsContent := fmt.Sprintf("nameserver %s\n", nm.cfg.Network.Eth0.DNS) os.WriteFile("/etc/resolv.conf", []byte(dnsContent), 0644) } } func (nm *NetworkManager) stopEth0DHCP() { pidFile := fmt.Sprintf("/var/run/udhcpc.%s.pid", eth0Interface) pidData, err := os.ReadFile(pidFile) if err == nil { var pid int if _, err := fmt.Sscanf(string(pidData), "%d", &pid); err == nil { syscall.Kill(pid, syscall.SIGTERM) os.Remove(pidFile) } } exec.Command("killall", "-q", "udhcpc").Run() nm.eth0DhcpPid = 0 } func (nm *NetworkManager) stopEth0() { nm.stopEth0DHCP() exec.Command("ip", "addr", "flush", "dev", eth0Interface).Run() } // ======================================================================================= // wlan0 client management // ======================================================================================= func (nm *NetworkManager) connectWLAN0Client() error { // Stop WiFi scanner (can't run both) if nm.scanners.IsWiFiRunning() { log.Println("[netmgr] Stopping WiFi scanner before connecting client") nm.scanners.StopWiFi() } // Use wifi-connect.sh script scriptPath := "/opt/mybeacon/bin/wifi-connect.sh" cmd := exec.Command(scriptPath, nm.cfg.WiFi.SSID, nm.cfg.WiFi.PSK) output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("wifi-connect.sh failed: %w (output: %s)", err, string(output)) } return nil } func (nm *NetworkManager) stopWLAN0Client() { exec.Command("killall", "wpa_supplicant", "udhcpc", "dhcpcd").Run() exec.Command("ip", "addr", "flush", "dev", wlan0Interface).Run() os.RemoveAll("/var/run/wpa_supplicant/wlan0") } // ======================================================================================= // wlan0 AP fallback management // ======================================================================================= func (nm *NetworkManager) startAP() { if nm.apRunning { return } log.Println("[netmgr] Starting AP fallback mode...") // Stop client if running nm.stopWLAN0Client() // Stop WiFi scanner if nm.scanners.IsWiFiRunning() { nm.scanners.StopWiFi() } // Create ap0 interface exec.Command("iw", "phy", "phy0", "interface", "add", "ap0", "type", "__ap").Run() time.Sleep(500 * time.Millisecond) // Configure AP IP exec.Command("ifconfig", "ap0", "192.168.4.1", "netmask", "255.255.255.0", "up").Run() // Start hostapd exec.Command("hostapd", "-B", "/etc/hostapd/hostapd.conf").Start() // Start dnsmasq for DHCP exec.Command("dnsmasq", "-C", "/etc/dnsmasq.d/ap0.conf").Start() nm.apRunning = true nm.currentState = StateWLAN0APFallback log.Println("[netmgr] AP fallback started: SSID=Luckfox_Setup, IP=192.168.4.1") } func (nm *NetworkManager) stopAP() { if !nm.apRunning { return } log.Println("[netmgr] Stopping AP fallback...") exec.Command("killall", "hostapd", "dnsmasq").Run() exec.Command("ip", "link", "del", "ap0").Run() nm.apRunning = false } // ======================================================================================= // Helper functions // ======================================================================================= func (nm *NetworkManager) checkCarrier(iface string) bool { carrierPath := fmt.Sprintf("/sys/class/net/%s/carrier", iface) data, err := os.ReadFile(carrierPath) if err != nil { return false } return strings.TrimSpace(string(data)) == "1" } func (nm *NetworkManager) hasIP(iface string) bool { cmd := exec.Command("ip", "addr", "show", iface) output, err := cmd.Output() if err != nil { return false } return strings.Contains(string(output), "inet ") }