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
|
// probeEthernet reports the wired uplink state of iface. Active requires both
|
||||||
// a live link (`carrier == 1`) and a DHCP-assigned IPv4 — detected either by
|
// a live link (`carrier == 1`) and a default route via the interface.
|
||||||
// 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).
|
// 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) {
|
func probeEthernet(iface string) (*models.EthernetStatus, error) {
|
||||||
if !hasCarrier(iface) {
|
if !hasCarrier(iface) {
|
||||||
return &models.EthernetStatus{Active: false, Interface: iface}, nil
|
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 {
|
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") {
|
if strings.TrimSpace(string(out)) == "" {
|
||||||
fields := strings.Fields(line)
|
return &models.EthernetStatus{Active: false, Interface: iface}, nil
|
||||||
var addr string
|
}
|
||||||
hasInet := false
|
|
||||||
isDHCP := false
|
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 {
|
for i, f := range fields {
|
||||||
switch f {
|
switch f {
|
||||||
case "inet":
|
case "dev":
|
||||||
hasInet = true
|
|
||||||
if i+1 < len(fields) {
|
if i+1 < len(fields) {
|
||||||
addr = fields[i+1]
|
dev = fields[i+1]
|
||||||
}
|
}
|
||||||
case "dynamic":
|
case "src":
|
||||||
isDHCP = true
|
if i+1 < len(fields) {
|
||||||
case "valid_lft":
|
src = fields[i+1]
|
||||||
if i+1 < len(fields) && fields[i+1] != "forever" {
|
|
||||||
isDHCP = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if hasInet && isDHCP {
|
if dev != iface {
|
||||||
if slash := strings.IndexByte(addr, '/'); slash >= 0 {
|
return ""
|
||||||
addr = addr[:slash]
|
|
||||||
}
|
}
|
||||||
return &models.EthernetStatus{Active: true, Interface: iface, IPv4: addr}, nil
|
return src
|
||||||
}
|
|
||||||
}
|
|
||||||
return &models.EthernetStatus{Active: false, Interface: iface}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureUplink returns the current Ethernet status, and starts wpa_supplicant
|
// 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
|
// 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
|
// caller must avoid any D-Bus call to fi.w1.wpa_supplicant1, which would
|
||||||
// otherwise re-activate the daemon via dbus-activation.
|
// 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)
|
log.Printf("DHCP address %s on %s, leaving wpa_supplicant alone", eth.IPv4, iface)
|
||||||
return eth
|
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 {
|
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)))
|
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