// Network Manager - manages all network interfaces with priority-based logic package main import ( "fmt" "log" "os" "os/exec" "strings" "sync" "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{} devicePassword string // AP fallback password from device state mu sync.Mutex // Scanners reference (needed to stop WiFi scanner when using wlan0) scanners *ScannerManager // State tracking eth0Carrier bool eth0HasIP bool eth0DhcpRunning bool wlan0HasIP bool wasOnline bool currentState NetworkState lastOnlineTime time.Time apRunning bool eth0DhcpPid int } // NewNetworkManager creates a new network manager func NewNetworkManager(cfg *Config, scanners *ScannerManager, devicePassword string) *NetworkManager { return &NetworkManager{ cfg: cfg, debug: cfg.Debug, stopChan: make(chan struct{}), devicePassword: devicePassword, 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) // Stop NTP daemon nm.stopNTP() // Clean up WiFi interfaces only (leave eth0 running for SSH access) nm.stopWLAN0Client() nm.stopAP() // Note: eth0 is NOT stopped to maintain SSH connectivity // Only stop eth0 DHCP process, but keep the IP address if nm.eth0DhcpPid > 0 { syscall.Kill(nm.eth0DhcpPid, syscall.SIGTERM) } exec.Command("killall", "-q", "udhcpc").Run() } // 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() // Start NTP when we transition from offline to online if !nm.wasOnline { log.Println("[netmgr] Network connection established - starting NTP") go nm.startNTP() // Run in goroutine to avoid blocking tick } nm.wasOnline = true } else { nm.wasOnline = false } // Priority 1: eth0 (independent, always configure if carrier UP) if nm.eth0Carrier { if !nm.eth0HasIP && !nm.eth0DhcpRunning { // Carrier UP but no IP and DHCP not running - 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() } } } else { // No eth0 carrier - stop eth0 DHCP/IP if nm.eth0HasIP || nm.eth0DhcpRunning { log.Println("[netmgr] eth0 carrier DOWN - stopping network") nm.stopEth0() } } // Priority 2: wlan0 client (INDEPENDENT of eth0, always connect if configured) 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 nm.scanners.IsWiFiRunning() { log.Println("[netmgr] Stopping WiFi scanner before connecting client") nm.scanners.StopWiFi() } if err := nm.connectWLAN0Client(); err != nil { log.Printf("[netmgr] wlan0 client connection failed: %v", err) // Will fallback to AP after 120s } } if nm.wlan0HasIP { if nm.currentState != StateWLAN0ClientOnline { log.Println("[netmgr] wlan0 client online") nm.currentState = StateWLAN0ClientOnline } nm.stopAP() // Ensure AP is stopped // Stop WiFi scanner (can't run with client) if nm.scanners.IsWiFiRunning() { log.Println("[netmgr] Stopping WiFi scanner (wlan0 client active)") nm.scanners.StopWiFi() } } return // wlan0 client configured - don't start scanner or AP } else { // wlan0 client not configured - stop it if running if nm.wlan0HasIP { log.Println("[netmgr] wlan0 client disabled - disconnecting") nm.stopWLAN0Client() } } // Determine current state if nm.eth0HasIP { nm.currentState = StateEth0Online } else if nm.wlan0HasIP { nm.currentState = StateWLAN0ClientOnline } else { nm.currentState = StateNoNetwork } // Fallback: wlan0 AP (if offline for 120 seconds) - CHECK THIS BEFORE WiFi scanner! timeSinceOnline := time.Since(nm.lastOnlineTime) if !online && timeSinceOnline >= apFallbackDelay { if !nm.apRunning { log.Printf("[netmgr] No network for %v - starting AP fallback", timeSinceOnline) // Stop scanner before starting AP if nm.scanners.IsWiFiRunning() { nm.scanners.StopWiFi() } nm.startAP() } // Keep trying wlan0 client even with AP running (if configured) 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 } } } } else { // Online - stop AP if running if nm.apRunning { log.Println("[netmgr] Network online - stopping AP fallback") nm.stopAP() } } // WiFi Scanner: ONLY if wlan0 is free (no client, no AP) AND monitor enabled // IMPORTANT: Check this AFTER AP fallback logic to avoid race condition if nm.cfg.WiFi.MonitorEnabled && !nm.apRunning { if !nm.scanners.IsWiFiRunning() { log.Println("[netmgr] Starting WiFi scanner (wlan0 free)") // AIC8800 combo chip: Pause BLE scanner ONLY for mode switch bleWasRunning := nm.scanners.IsBLERunning() if bleWasRunning { log.Println("[netmgr] Pausing BLE for WiFi mode switch (AIC8800 combo chip)") nm.scanners.StopBLE() time.Sleep(500 * time.Millisecond) } // Switch wlan0 to monitor mode nm.scanners.StartWiFi(nm.cfg.ZMQAddrWiFi, nm.cfg.WiFiIface) time.Sleep(500 * time.Millisecond) // Restart BLE after mode switch (if it was running and still enabled) if bleWasRunning && nm.cfg.BLE.Enabled { log.Println("[netmgr] Restarting BLE scanner (WiFi mode switch complete)") nm.scanners.StartBLE(nm.cfg.ZMQAddrBLE) } } } else { if nm.scanners.IsWiFiRunning() { log.Println("[netmgr] Stopping WiFi scanner (wlan0 busy or monitor disabled)") nm.scanners.StopWiFi() // Restart BLE scanner if it was running and still enabled if nm.cfg.BLE.Enabled && !nm.scanners.IsBLERunning() { log.Println("[netmgr] Restarting BLE scanner (WiFi scanner stopped)") nm.scanners.StartBLE(nm.cfg.ZMQAddrBLE) } } } } // ======================================================================================= // eth0 management // ======================================================================================= func (nm *NetworkManager) startEth0DHCP() { // Stop any existing DHCP first nm.stopEth0DHCP() // Bring interface up (don't flush IP - udhcpc will handle it) exec.Command("ip", "link", "set", 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 } nm.eth0DhcpRunning = true time.Sleep(500 * time.Millisecond) // Read PID pidData, err := os.ReadFile(pidFile) if err == nil { fmt.Sscanf(string(pidData), "%d", &nm.eth0DhcpPid) if nm.debug { log.Printf("[netmgr] eth0 DHCP started (PID: %d)", nm.eth0DhcpPid) } } else { log.Printf("[netmgr] Warning: Could not read udhcpc PID file: %v", err) } } func (nm *NetworkManager) configureEth0Static() { nm.stopEth0DHCP() // Bring interface up exec.Command("ip", "link", "set", 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 nm.eth0DhcpRunning = false } 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() } // Temporarily stop BLE scanner during WiFi connection (AIC8800 combo chip issue) bleWasRunning := nm.scanners.IsBLERunning() if bleWasRunning { log.Println("[netmgr] Temporarily stopping BLE scanner for WiFi connection") nm.scanners.StopBLE() defer func() { if bleWasRunning && nm.cfg.BLE.Enabled { log.Println("[netmgr] Restarting BLE scanner") nm.scanners.StartBLE(nm.cfg.ZMQAddrBLE) } }() time.Sleep(500 * time.Millisecond) } // 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() } // Get MAC address from wlan0 for SSID macAddr := nm.getInterfaceMAC(wlan0Interface) if macAddr == "" { macAddr = "00:00:00:00:00:00" } // Use last 4 chars of MAC (e.g., "1b:ad" from "38:54:39:4b:1b:ad") macParts := strings.Split(macAddr, ":") macSuffix := "" if len(macParts) >= 2 { macSuffix = macParts[len(macParts)-2] + macParts[len(macParts)-1] } else { macSuffix = "0000" } apSSID := fmt.Sprintf("mybeacon_%s", macSuffix) // Get password from device state apPassword := nm.devicePassword if apPassword == "" { apPassword = "mybeacon" // fallback if not set } // Use ap-start.sh script scriptPath := "/opt/mybeacon/bin/ap-start.sh" cmd := exec.Command(scriptPath, apSSID, apPassword) output, err := cmd.CombinedOutput() if err != nil { log.Printf("[netmgr] AP start failed: %v (output: %s)", err, string(output)) return } nm.apRunning = true nm.currentState = StateWLAN0APFallback log.Printf("[netmgr] AP fallback started: SSID=%s, Password=%s, IP=192.168.4.1", apSSID, apPassword) } func (nm *NetworkManager) stopAP() { if !nm.apRunning { return } log.Println("[netmgr] Stopping AP fallback...") // Use ap-stop.sh script scriptPath := "/opt/mybeacon/bin/ap-stop.sh" cmd := exec.Command(scriptPath) output, err := cmd.CombinedOutput() if err != nil { log.Printf("[netmgr] AP stop failed: %v (output: %s)", err, string(output)) } 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 ") } func (nm *NetworkManager) getInterfaceMAC(iface string) string { addrPath := fmt.Sprintf("/sys/class/net/%s/address", iface) data, err := os.ReadFile(addrPath) if err != nil { return "" } return strings.TrimSpace(string(data)) } // startNTP starts NTP daemon for time synchronization func (nm *NetworkManager) startNTP() { nm.mu.Lock() servers := nm.cfg.Network.NTPServers nm.mu.Unlock() if len(servers) == 0 { servers = []string{"pool.ntp.org"} } // Stop any existing ntpd exec.Command("killall", "-9", "ntpd").Run() time.Sleep(200 * time.Millisecond) log.Println("[netmgr] Starting NTP sync...") // First do a one-shot sync to set time immediately for _, server := range servers { log.Printf("[netmgr] Trying NTP server: %s", server) cmd := exec.Command("ntpd", "-n", "-q", "-p", server) if err := cmd.Run(); err == nil { log.Printf("[netmgr] NTP sync successful from %s", server) break } } // Start daemon for continuous sync (use first server from list) primaryServer := servers[0] cmd := exec.Command("ntpd", "-p", primaryServer) if err := cmd.Start(); err != nil { log.Printf("[netmgr] Failed to start NTP daemon: %v", err) return } log.Printf("[netmgr] NTP daemon started (server: %s)", primaryServer) } // stopNTP stops NTP daemon func (nm *NetworkManager) stopNTP() { log.Println("[netmgr] Stopping NTP daemon...") exec.Command("killall", "-9", "ntpd").Run() }