package app import ( "fmt" "log" "os" "os/exec" "strings" "github.com/nemunaire/repeater/internal/models" ) // hasCarrier reports whether the kernel sees an active link on iface. // `/sys/class/net//carrier` is "1" when the link layer is up; reading // it on an admin-down interface yields EINVAL, which we treat the same way. func hasCarrier(iface string) bool { data, err := os.ReadFile("/sys/class/net/" + iface + "/carrier") if err != nil { return false } return strings.TrimSpace(string(data)) == "1" } // probeEthernet reports the wired uplink state of iface. Active requires both // a live link (`carrier == 1`) and a DHCP-assigned IPv4 — detected either by // the `dynamic` flag emitted by full iproute2, or by a finite `valid_lft` // (which BusyBox's `ip` does emit even when it omits the flag). func probeEthernet(iface string) (*models.EthernetStatus, error) { if !hasCarrier(iface) { return &models.EthernetStatus{Active: false, Interface: iface}, nil } out, err := exec.Command("ip", "-4", "-o", "addr", "show", "dev", iface).Output() if err != nil { return nil, fmt.Errorf("ip addr show %s: %w", iface, err) } for _, line := range strings.Split(string(out), "\n") { fields := strings.Fields(line) var addr string hasInet := false isDHCP := false for i, f := range fields { switch f { case "inet": hasInet = true if i+1 < len(fields) { addr = fields[i+1] } case "dynamic": isDHCP = true case "valid_lft": if i+1 < len(fields) && fields[i+1] != "forever" { isDHCP = true } } } if hasInet && isDHCP { if slash := strings.IndexByte(addr, '/'); slash >= 0 { addr = addr[:slash] } return &models.EthernetStatus{Active: true, Interface: iface, IPv4: addr}, nil } } return &models.EthernetStatus{Active: false, Interface: iface}, nil } // ensureUplink returns the current Ethernet status, and starts wpa_supplicant // when no DHCP-assigned address is present. When Ethernet is already // providing connectivity the WiFi backend is left alone — and crucially the // caller must avoid any D-Bus call to fi.w1.wpa_supplicant1, which would // otherwise re-activate the daemon via dbus-activation. func ensureUplink(iface string) *models.EthernetStatus { eth, err := probeEthernet(iface) if err != nil { log.Printf("Could not probe %s (%v); starting wpa_supplicant as fallback", iface, err) eth = &models.EthernetStatus{Active: false, Interface: iface} } if eth.Active { log.Printf("DHCP address %s on %s, leaving wpa_supplicant alone", eth.IPv4, iface) return eth } log.Printf("No usable DHCP uplink on %s (carrier or lease missing), starting wpa_supplicant", iface) if out, err := exec.Command("service", "wpa_supplicant", "start").CombinedOutput(); err != nil { log.Printf("Failed to start wpa_supplicant: %v: %s", err, strings.TrimSpace(string(out))) } return eth }