From d57c08a6c437e58fbe5246adeb4c497aef6bd3f8 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 1 May 2026 22:35:07 +0800 Subject: [PATCH] app: Start wpa_supplicant only when Ethernet has no DHCP lease Probe the configured Ethernet interface (default eth0, overridable via -ethernet-interface) at startup. If no DHCP-assigned IPv4 is present, start the wpa_supplicant service so the WiFi backend has something to talk to; otherwise leave it alone and rely on the wired uplink. --- internal/app/app.go | 5 ++++ internal/app/network.go | 54 +++++++++++++++++++++++++++++++++++++++ internal/config/cli.go | 1 + internal/config/config.go | 42 +++++++++++++++--------------- 4 files changed, 82 insertions(+), 20 deletions(-) create mode 100644 internal/app/network.go diff --git a/internal/app/app.go b/internal/app/app.go index 8b89b65..8c3d315 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -57,6 +57,11 @@ func (a *App) Initialize(cfg *config.Config) error { // Store config reference a.Config = cfg + // If Ethernet uplink is not already providing connectivity (no DHCP lease + // on the configured interface), bring up wpa_supplicant so the WiFi + // backend has something to talk to. + ensureUplink(cfg.EthernetInterface) + // Initialize WiFi backend if err := wifi.Initialize(cfg.WifiInterface, cfg.WifiBackend); err != nil { return err diff --git a/internal/app/network.go b/internal/app/network.go new file mode 100644 index 0000000..eb0e496 --- /dev/null +++ b/internal/app/network.go @@ -0,0 +1,54 @@ +package app + +import ( + "fmt" + "log" + "os/exec" + "strings" +) + +// hasDHCPAddress reports whether iface holds an IPv4 address that ip(8) marks +// as "dynamic" — the flag set on addresses with a finite valid_lft, which is +// how DHCP-assigned leases appear (static addresses are "permanent"). +func hasDHCPAddress(iface string) (bool, error) { + out, err := exec.Command("ip", "-4", "-o", "addr", "show", "dev", iface).Output() + if err != nil { + return false, fmt.Errorf("ip addr show %s: %w", iface, err) + } + for _, line := range strings.Split(string(out), "\n") { + fields := strings.Fields(line) + hasInet := false + hasDynamic := false + for _, f := range fields { + switch f { + case "inet": + hasInet = true + case "dynamic": + hasDynamic = true + } + } + if hasInet && hasDynamic { + return true, nil + } + } + return false, nil +} + +// ensureUplink boots the WiFi uplink only if Ethernet is not already providing +// connectivity: when iface has no DHCP-assigned IPv4, start wpa_supplicant so +// the WiFi backend can take over. +func ensureUplink(iface string) { + has, err := hasDHCPAddress(iface) + if err != nil { + log.Printf("Could not probe %s for DHCP address (%v); starting wpa_supplicant as fallback", iface, err) + } else if has { + log.Printf("DHCP address present on %s, leaving wpa_supplicant alone", iface) + return + } else { + log.Printf("No DHCP address on %s, starting wpa_supplicant", iface) + } + + if out, err := exec.Command("systemctl", "start", "wpa_supplicant").CombinedOutput(); err != nil { + log.Printf("Failed to start wpa_supplicant: %v: %s", err, strings.TrimSpace(string(out))) + } +} diff --git a/internal/config/cli.go b/internal/config/cli.go index 4dad1b0..3e86245 100644 --- a/internal/config/cli.go +++ b/internal/config/cli.go @@ -9,6 +9,7 @@ func declareFlags(o *Config) { flag.StringVar(&o.Bind, "bind", "127.0.0.1:8080", "Bind address (host:port). Defaults to localhost; set to ':8080' to expose on the LAN — but note: there is no built-in authentication.") flag.StringVar(&o.WifiInterface, "wifi-interface", "wlan0", "WiFi interface name") flag.StringVar(&o.HotspotInterface, "hotspot-interface", "wlan1", "Hotspot WiFi interface name") + flag.StringVar(&o.EthernetInterface, "ethernet-interface", "eth0", "Ethernet interface to probe for a DHCP-assigned address at startup; if no DHCP address is present, wpa_supplicant is started") flag.StringVar(&o.WifiBackend, "wifi-backend", "", "WiFi backend to use: 'iwd' or 'wpasupplicant' (required)") flag.StringVar(&o.StationBackend, "station-backend", "hostapd", "Station discovery backend: 'arp', 'dhcp', or 'hostapd'") flag.StringVar(&o.DHCPLeasesPath, "dhcp-leases-path", "/var/lib/dhcp/dhcpd.leases", "Path to DHCP leases file") diff --git a/internal/config/config.go b/internal/config/config.go index 14570c7..1e89e82 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,17 +9,18 @@ import ( ) type Config struct { - Bind string - WifiInterface string - HotspotInterface string - WifiBackend string - StationBackend string // "arp", "dhcp", or "hostapd" - DHCPLeasesPath string - ARPTablePath string - SyslogEnabled bool - SyslogPath string - SyslogFilter []string - SyslogSource string + Bind string + WifiInterface string + HotspotInterface string + EthernetInterface string + WifiBackend string + StationBackend string // "arp", "dhcp", or "hostapd" + DHCPLeasesPath string + ARPTablePath string + SyslogEnabled bool + SyslogPath string + SyslogFilter []string + SyslogSource string } // ConsolidateConfig fills an Options struct by reading configuration from @@ -29,15 +30,16 @@ type Config struct { func ConsolidateConfig() (opts *Config, err error) { // Define defaults options opts = &Config{ - Bind: "127.0.0.1:8080", - WifiInterface: "wlan0", - HotspotInterface: "wlan1", - DHCPLeasesPath: "/var/lib/dhcp/dhcpd.leases", - ARPTablePath: "/proc/net/arp", - SyslogEnabled: false, - SyslogPath: "/var/log/messages", - SyslogFilter: []string{"daemon.info wpa_supplicant:", "daemon.info iwd:", "daemon.info hostapd:"}, - SyslogSource: "iwd", + Bind: "127.0.0.1:8080", + WifiInterface: "wlan0", + HotspotInterface: "wlan1", + EthernetInterface: "eth0", + DHCPLeasesPath: "/var/lib/dhcp/dhcpd.leases", + ARPTablePath: "/proc/net/arp", + SyslogEnabled: false, + SyslogPath: "/var/log/messages", + SyslogFilter: []string{"daemon.info wpa_supplicant:", "daemon.info iwd:", "daemon.info hostapd:"}, + SyslogSource: "iwd", } declareFlags(opts)