diff --git a/internal/app/network.go b/internal/app/network.go index 4e93898..07502f1 100644 --- a/internal/app/network.go +++ b/internal/app/network.go @@ -22,67 +22,50 @@ func hasCarrier(iface string) bool { } // probeEthernet reports the wired uplink state of iface. Active requires both -// a live link (`carrier == 1`) and a default route via the interface. -// -// The default route is the reliable cross-client signal: udhcpc (BusyBox) -// installs the leased address with a plain `ip addr add`, leaving no lease -// lifetime — the address shows `valid_lft forever` and no `dynamic` flag, so -// address flags can't tell a DHCP uplink apart from a static LAN/hotspot -// address on the same interface. Only the real uplink installs a default route -// through this interface, and that holds for udhcpc and full iproute2 alike. +// 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", "route", "show", "default", "dev", iface).Output() + out, err := exec.Command("ip", "-4", "-o", "addr", "show", "dev", iface).Output() if err != nil { - return nil, fmt.Errorf("ip route show default dev %s: %w", iface, err) + return nil, fmt.Errorf("ip addr show %s: %w", iface, err) } - if strings.TrimSpace(string(out)) == "" { - return &models.EthernetStatus{Active: false, Interface: iface}, nil - } - - return &models.EthernetStatus{ - Active: true, - Interface: iface, - IPv4: uplinkSourceIP(iface), - }, nil -} - -// uplinkSourceIP returns the source address the kernel uses for off-link -// traffic through iface, so the UI shows the uplink (DHCP) address rather than -// a static LAN/hotspot address that shares the interface. Returns "" when the -// kernel would route off-link traffic through a different interface — the -// caller has already confirmed iface carries a default route, so an empty -// string only affects the displayed IP, not the active decision. -func uplinkSourceIP(iface string) string { - out, err := exec.Command("ip", "-4", "route", "get", "1.1.1.1").Output() - if err != nil { - return "" - } - fields := strings.Fields(string(out)) - var dev, src string - for i, f := range fields { - switch f { - case "dev": - if i+1 < len(fields) { - dev = fields[i+1] - } - case "src": - if i+1 < len(fields) { - src = fields[i+1] + 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 + } } - if dev != iface { - return "" - } - return src + return &models.EthernetStatus{Active: false, Interface: iface}, nil } // ensureUplink returns the current Ethernet status, and starts wpa_supplicant -// when the interface carries no default route. When Ethernet is already +// 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. @@ -96,7 +79,7 @@ func ensureUplink(iface string) *models.EthernetStatus { log.Printf("DHCP address %s on %s, leaving wpa_supplicant alone", eth.IPv4, iface) return eth } - log.Printf("No usable uplink on %s (carrier or default route missing), starting wpa_supplicant", iface) + 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))) } diff --git a/internal/wifi/wpasupplicant/backend.go b/internal/wifi/wpasupplicant/backend.go index 7a0dec3..1516330 100644 --- a/internal/wifi/wpasupplicant/backend.go +++ b/internal/wifi/wpasupplicant/backend.go @@ -54,7 +54,7 @@ func (b *WPABackend) getInterfacePath(interfaceName string) (dbus.ObjectPath, er var interfacePath dbus.ObjectPath // Try to get existing interface - err := callNoAutoStart(b.wpasupplicant, Service+".GetInterface", interfaceName).Store(&interfacePath) + err := b.wpasupplicant.Call(Service+".GetInterface", 0, interfaceName).Store(&interfacePath) if err == nil { return interfacePath, nil } @@ -64,7 +64,7 @@ func (b *WPABackend) getInterfacePath(interfaceName string) (dbus.ObjectPath, er "Ifname": dbus.MakeVariant(interfaceName), } - err = callNoAutoStart(b.wpasupplicant, Service+".CreateInterface", args).Store(&interfacePath) + err = b.wpasupplicant.Call(Service+".CreateInterface", 0, args).Store(&interfacePath) if err != nil { return "", fmt.Errorf("failed to create interface: %v", err) } diff --git a/internal/wifi/wpasupplicant/bss.go b/internal/wifi/wpasupplicant/bss.go index e161eea..ab1b5b7 100644 --- a/internal/wifi/wpasupplicant/bss.go +++ b/internal/wifi/wpasupplicant/bss.go @@ -38,49 +38,49 @@ func (b *BSS) GetProperties() (*BSSProperties, error) { props := &BSSProperties{} // Get SSID - if ssidProp, err := getPropNoAutoStart(b.obj, BSSInterface+".SSID"); err == nil { + if ssidProp, err := b.obj.GetProperty(BSSInterface + ".SSID"); err == nil { if ssid, ok := ssidProp.Value().([]byte); ok { props.SSID = ssid } } // Get BSSID - if bssidProp, err := getPropNoAutoStart(b.obj, BSSInterface+".BSSID"); err == nil { + if bssidProp, err := b.obj.GetProperty(BSSInterface + ".BSSID"); err == nil { if bssid, ok := bssidProp.Value().([]byte); ok { props.BSSID = bssid } } // Get Signal - if signalProp, err := getPropNoAutoStart(b.obj, BSSInterface+".Signal"); err == nil { + if signalProp, err := b.obj.GetProperty(BSSInterface + ".Signal"); err == nil { if signal, ok := signalProp.Value().(int16); ok { props.Signal = signal } } // Get Frequency - if freqProp, err := getPropNoAutoStart(b.obj, BSSInterface+".Frequency"); err == nil { + if freqProp, err := b.obj.GetProperty(BSSInterface + ".Frequency"); err == nil { if freq, ok := freqProp.Value().(uint16); ok { props.Frequency = uint32(freq) } } // Get Privacy - if privacyProp, err := getPropNoAutoStart(b.obj, BSSInterface+".Privacy"); err == nil { + if privacyProp, err := b.obj.GetProperty(BSSInterface + ".Privacy"); err == nil { if privacy, ok := privacyProp.Value().(bool); ok { props.Privacy = privacy } } // Get RSN (WPA2) information - if rsnProp, err := getPropNoAutoStart(b.obj, BSSInterface+".RSN"); err == nil { + if rsnProp, err := b.obj.GetProperty(BSSInterface + ".RSN"); err == nil { if rsn, ok := rsnProp.Value().(map[string]dbus.Variant); ok { props.RSN = rsn } } // Get WPA information - if wpaProp, err := getPropNoAutoStart(b.obj, BSSInterface+".WPA"); err == nil { + if wpaProp, err := b.obj.GetProperty(BSSInterface + ".WPA"); err == nil { if wpa, ok := wpaProp.Value().(map[string]dbus.Variant); ok { props.WPA = wpa } @@ -91,7 +91,7 @@ func (b *BSS) GetProperties() (*BSSProperties, error) { // GetSSIDString returns the SSID as a string func (b *BSS) GetSSIDString() (string, error) { - prop, err := getPropNoAutoStart(b.obj, BSSInterface+".SSID") + prop, err := b.obj.GetProperty(BSSInterface + ".SSID") if err != nil { return "", fmt.Errorf("failed to get SSID property: %v", err) } @@ -106,7 +106,7 @@ func (b *BSS) GetSSIDString() (string, error) { // GetBSSIDString returns the BSSID as a formatted MAC address string func (b *BSS) GetBSSIDString() (string, error) { - prop, err := getPropNoAutoStart(b.obj, BSSInterface+".BSSID") + prop, err := b.obj.GetProperty(BSSInterface + ".BSSID") if err != nil { return "", fmt.Errorf("failed to get BSSID property: %v", err) } @@ -122,7 +122,7 @@ func (b *BSS) GetBSSIDString() (string, error) { // GetSignal returns the signal strength in dBm func (b *BSS) GetSignal() (int16, error) { - prop, err := getPropNoAutoStart(b.obj, BSSInterface+".Signal") + prop, err := b.obj.GetProperty(BSSInterface + ".Signal") if err != nil { return 0, fmt.Errorf("failed to get Signal property: %v", err) } diff --git a/internal/wifi/wpasupplicant/dbusutil.go b/internal/wifi/wpasupplicant/dbusutil.go deleted file mode 100644 index 217089f..0000000 --- a/internal/wifi/wpasupplicant/dbusutil.go +++ /dev/null @@ -1,60 +0,0 @@ -package wpasupplicant - -import ( - "fmt" - "strings" - - "github.com/godbus/dbus/v5" -) - -// D-Bus interactions with the fi.w1.wpa_supplicant1 name must never trigger -// service activation. repeater is the sole launcher of the daemon (via -// `service wpa_supplicant start` in the app layer); if a call reached the bus -// name while the daemon was down — a startup race, or a stray call in -// Ethernet-uplink mode — the bus would auto-activate it from its .service -// file, spawning `wpa_supplicant -u` and bypassing repeater's own launch. -// -// godbus only suppresses activation when FlagNoAutoStart is set on the message -// (msg.Flags = flags & (FlagNoAutoStart | FlagNoReplyExpected)); the default -// flag value of 0 leaves activation enabled. The helpers below set the flag on -// every method call and property access. A leaked call then fails cleanly with -// a ServiceUnknown error instead of resurrecting the daemon. - -// callNoAutoStart performs a method call with D-Bus auto-activation disabled. -func callNoAutoStart(obj dbus.BusObject, method string, args ...interface{}) *dbus.Call { - return obj.Call(method, dbus.FlagNoAutoStart, args...) -} - -// getPropNoAutoStart reads a property (interface.member notation) with -// auto-activation disabled. It mirrors dbus.Object.GetProperty, which -// otherwise issues Properties.Get with the activation-enabling flag 0. -func getPropNoAutoStart(obj dbus.BusObject, p string) (dbus.Variant, error) { - var result dbus.Variant - iface, prop, err := splitProperty(p) - if err != nil { - return result, err - } - err = obj.Call("org.freedesktop.DBus.Properties.Get", dbus.FlagNoAutoStart, iface, prop).Store(&result) - return result, err -} - -// setPropNoAutoStart writes a property with auto-activation disabled. The value -// is passed through as-is, so callers wrap it in a dbus.Variant just as they -// would for dbus.Object.SetProperty. -func setPropNoAutoStart(obj dbus.BusObject, p string, v interface{}) error { - iface, prop, err := splitProperty(p) - if err != nil { - return err - } - return obj.Call("org.freedesktop.DBus.Properties.Set", dbus.FlagNoAutoStart, iface, prop, v).Err -} - -// splitProperty splits an "interface.member" property name into its interface -// and member parts, matching godbus's own parsing. -func splitProperty(p string) (iface, prop string, err error) { - idx := strings.LastIndex(p, ".") - if idx == -1 || idx+1 == len(p) { - return "", "", fmt.Errorf("dbus: invalid property %s", p) - } - return p[:idx], p[idx+1:], nil -} diff --git a/internal/wifi/wpasupplicant/interface.go b/internal/wifi/wpasupplicant/interface.go index 4484605..e27d1a0 100644 --- a/internal/wifi/wpasupplicant/interface.go +++ b/internal/wifi/wpasupplicant/interface.go @@ -28,7 +28,7 @@ func (i *WPAInterface) Scan(scanType string) error { "Type": scanType, // "active" or "passive" } - err := callNoAutoStart(i.obj, InterfaceInterface+".Scan", args).Err + err := i.obj.Call(InterfaceInterface+".Scan", 0, args).Err if err != nil { return fmt.Errorf("scan failed: %v", err) } @@ -37,7 +37,7 @@ func (i *WPAInterface) Scan(scanType string) error { // GetBSSs returns a list of BSS (Basic Service Set) object paths func (i *WPAInterface) GetBSSs() ([]dbus.ObjectPath, error) { - prop, err := getPropNoAutoStart(i.obj, InterfaceInterface+".BSSs") + prop, err := i.obj.GetProperty(InterfaceInterface + ".BSSs") if err != nil { return nil, fmt.Errorf("failed to get BSSs property: %v", err) } @@ -52,7 +52,7 @@ func (i *WPAInterface) GetBSSs() ([]dbus.ObjectPath, error) { // GetState returns the current connection state func (i *WPAInterface) GetState() (WPAState, error) { - prop, err := getPropNoAutoStart(i.obj, InterfaceInterface+".State") + prop, err := i.obj.GetProperty(InterfaceInterface + ".State") if err != nil { return "", fmt.Errorf("failed to get State property: %v", err) } @@ -67,7 +67,7 @@ func (i *WPAInterface) GetState() (WPAState, error) { // GetCurrentBSS returns the currently connected BSS object path func (i *WPAInterface) GetCurrentBSS() (dbus.ObjectPath, error) { - prop, err := getPropNoAutoStart(i.obj, InterfaceInterface+".CurrentBSS") + prop, err := i.obj.GetProperty(InterfaceInterface + ".CurrentBSS") if err != nil { return "", fmt.Errorf("failed to get CurrentBSS property: %v", err) } @@ -90,7 +90,7 @@ func (i *WPAInterface) AddNetwork(config map[string]interface{}) (dbus.ObjectPat dbusConfig[key] = dbus.MakeVariant(value) } - err := callNoAutoStart(i.obj, InterfaceInterface+".AddNetwork", dbusConfig).Store(&networkPath) + err := i.obj.Call(InterfaceInterface+".AddNetwork", 0, dbusConfig).Store(&networkPath) if err != nil { return "", fmt.Errorf("failed to add network: %v", err) } @@ -100,7 +100,7 @@ func (i *WPAInterface) AddNetwork(config map[string]interface{}) (dbus.ObjectPat // SelectNetwork connects to a network func (i *WPAInterface) SelectNetwork(networkPath dbus.ObjectPath) error { - err := callNoAutoStart(i.obj, InterfaceInterface+".SelectNetwork", networkPath).Err + err := i.obj.Call(InterfaceInterface+".SelectNetwork", 0, networkPath).Err if err != nil { return fmt.Errorf("failed to select network: %v", err) } @@ -110,7 +110,7 @@ func (i *WPAInterface) SelectNetwork(networkPath dbus.ObjectPath) error { // EnableNetwork marks a network configuration as enabled (eligible for auto-connect) func (i *WPAInterface) EnableNetwork(networkPath dbus.ObjectPath) error { netObj := i.conn.Object(Service, networkPath) - err := setPropNoAutoStart(netObj, NetworkInterface+".Enabled", dbus.MakeVariant(true)) + err := netObj.SetProperty(NetworkInterface+".Enabled", dbus.MakeVariant(true)) if err != nil { return fmt.Errorf("failed to enable network: %v", err) } @@ -119,7 +119,7 @@ func (i *WPAInterface) EnableNetwork(networkPath dbus.ObjectPath) error { // RemoveNetwork removes a network configuration func (i *WPAInterface) RemoveNetwork(networkPath dbus.ObjectPath) error { - err := callNoAutoStart(i.obj, InterfaceInterface+".RemoveNetwork", networkPath).Err + err := i.obj.Call(InterfaceInterface+".RemoveNetwork", 0, networkPath).Err if err != nil { return fmt.Errorf("failed to remove network: %v", err) } @@ -128,7 +128,7 @@ func (i *WPAInterface) RemoveNetwork(networkPath dbus.ObjectPath) error { // GetNetworks returns the object paths of all configured networks func (i *WPAInterface) GetNetworks() ([]dbus.ObjectPath, error) { - prop, err := getPropNoAutoStart(i.obj, InterfaceInterface+".Networks") + prop, err := i.obj.GetProperty(InterfaceInterface + ".Networks") if err != nil { return nil, fmt.Errorf("failed to get Networks property: %v", err) } @@ -143,7 +143,7 @@ func (i *WPAInterface) GetNetworks() ([]dbus.ObjectPath, error) { // Disconnect disconnects from the current network func (i *WPAInterface) Disconnect() error { - err := callNoAutoStart(i.obj, InterfaceInterface+".Disconnect").Err + err := i.obj.Call(InterfaceInterface+".Disconnect", 0).Err if err != nil { return fmt.Errorf("disconnect failed: %v", err) } @@ -152,7 +152,7 @@ func (i *WPAInterface) Disconnect() error { // SaveConfig saves the current configuration to the wpa_supplicant config file func (i *WPAInterface) SaveConfig() error { - err := callNoAutoStart(i.obj, InterfaceInterface+".SaveConfig").Err + err := i.obj.Call(InterfaceInterface+".SaveConfig", 0).Err if err != nil { return fmt.Errorf("save config failed: %v", err) } @@ -166,7 +166,7 @@ func (i *WPAInterface) GetPath() dbus.ObjectPath { // GetScanning returns whether a scan is currently in progress func (i *WPAInterface) GetScanning() (bool, error) { - prop, err := getPropNoAutoStart(i.obj, InterfaceInterface+".Scanning") + prop, err := i.obj.GetProperty(InterfaceInterface + ".Scanning") if err != nil { return false, fmt.Errorf("failed to get Scanning property: %v", err) } diff --git a/internal/wifi/wpasupplicant/network.go b/internal/wifi/wpasupplicant/network.go index 39ce080..c018e3f 100644 --- a/internal/wifi/wpasupplicant/network.go +++ b/internal/wifi/wpasupplicant/network.go @@ -29,7 +29,7 @@ func (n *Network) GetPath() dbus.ObjectPath { // GetProperties returns properties of the network configuration func (n *Network) GetProperties() (map[string]dbus.Variant, error) { - prop, err := getPropNoAutoStart(n.obj, NetworkInterface+".Properties") + prop, err := n.obj.GetProperty(NetworkInterface + ".Properties") if err != nil { return nil, err }