app: Detect Ethernet uplink via default route, not address flags
probeEthernet looked for a "DHCP-assigned" address using the `dynamic` flag or a finite `valid_lft`. udhcpc (BusyBox) installs the leased address with a plain `ip addr add` and no lease lifetime, so the address shows `valid_lft forever` and no `dynamic` flag — indistinguishable from a static LAN/hotspot address on the same interface. The uplink was never recognized and wpa_supplicant was started despite working connectivity. Detect the uplink via the interface's default route instead, which is reliable across udhcpc and full iproute2, and report the uplink source address from `ip route get` so the UI shows the DHCP address rather than a co-located static one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
11ae69de56
commit
310cce2a45
1 changed files with 48 additions and 31 deletions
|
|
@ -22,50 +22,67 @@ func hasCarrier(iface string) bool {
|
|||
}
|
||||
|
||||
// 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).
|
||||
// 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.
|
||||
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()
|
||||
out, err := exec.Command("ip", "-4", "route", "show", "default", "dev", iface).Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ip addr show %s: %w", iface, err)
|
||||
return nil, fmt.Errorf("ip route show default dev %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 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]
|
||||
}
|
||||
}
|
||||
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
|
||||
if dev != iface {
|
||||
return ""
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
// ensureUplink returns the current Ethernet status, and starts wpa_supplicant
|
||||
// when no DHCP-assigned address is present. When Ethernet is already
|
||||
// when the interface carries no default route. 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.
|
||||
|
|
@ -79,7 +96,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 DHCP uplink on %s (carrier or lease missing), starting wpa_supplicant", iface)
|
||||
log.Printf("No usable uplink on %s (carrier or default route 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)))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue