From a042e195496a88f20592ec2e65dad0efc223eb7d Mon Sep 17 00:00:00 2001 From: henrygd Date: Fri, 25 Sep 2026 19:53:35 -0400 Subject: [PATCH] wifi(linux): query associated station directly mdlayher/wifi only dumps stations, which some full-MAC drivers (e.g. out-of-tree Realtek USB) answer with an empty list, leaving RSSI unavailable. Request the associated BSSID's station directly, as `iw link` does, and parse it with native.ParseStationInfo. --- agent/wifi/wifi_linux.go | 76 ++++++++++++++++++++++++++++++----- agent/wifi/wifi_linux_test.go | 13 +++++- go.mod | 4 +- 3 files changed, 79 insertions(+), 14 deletions(-) diff --git a/agent/wifi/wifi_linux.go b/agent/wifi/wifi_linux.go index fa71e0dcc..d2c212897 100644 --- a/agent/wifi/wifi_linux.go +++ b/agent/wifi/wifi_linux.go @@ -3,24 +3,85 @@ package wifi import ( - "bytes" "context" + "errors" + "net" "time" "github.com/henrygd/beszel/internal/entities/system" + "github.com/mdlayher/genetlink" + "github.com/mdlayher/netlink" native "github.com/mdlayher/wifi" + "golang.org/x/sys/unix" ) type linuxClient interface { Interfaces() ([]*native.Interface, error) BSS(*native.Interface) (*native.BSS, error) - StationInfo(*native.Interface) ([]*native.StationInfo, error) + Station(*native.Interface, net.HardwareAddr) (*native.StationInfo, error) SetDeadline(time.Time) error Close() error } -func collect(ctx context.Context) map[string]system.WiFi { +// nl80211Client adds a targeted GET_STATION request, as used by `iw link`. +// mdlayher/wifi only dumps stations, which some full-MAC drivers (e.g. +// out-of-tree Realtek USB) answer with an empty list. +type nl80211Client struct { + *native.Client + conn *genetlink.Conn + family genetlink.Family +} + +func newNL80211Client() (*nl80211Client, error) { client, err := native.New() + if err != nil { + return nil, err + } + conn, err := genetlink.Dial(nil) + if err != nil { + client.Close() + return nil, err + } + family, err := conn.GetFamily(unix.NL80211_GENL_NAME) + if err != nil { + conn.Close() + client.Close() + return nil, err + } + return &nl80211Client{Client: client, conn: conn, family: family}, nil +} + +func (c *nl80211Client) Station(ifi *native.Interface, mac net.HardwareAddr) (*native.StationInfo, error) { + ae := netlink.NewAttributeEncoder() + ae.Uint32(unix.NL80211_ATTR_IFINDEX, uint32(ifi.Index)) + ae.Bytes(unix.NL80211_ATTR_MAC, mac) + data, err := ae.Encode() + if err != nil { + return nil, err + } + msgs, err := c.conn.Execute(genetlink.Message{ + Header: genetlink.Header{Command: unix.NL80211_CMD_GET_STATION, Version: c.family.Version}, + Data: data, + }, c.family.ID, netlink.Request) + if err != nil { + return nil, err + } + if len(msgs) == 0 { + return nil, errors.New("no station info") + } + return native.ParseStationInfo(msgs[0].Data) +} + +func (c *nl80211Client) SetDeadline(t time.Time) error { + return errors.Join(c.Client.SetDeadline(t), c.conn.SetDeadline(t)) +} + +func (c *nl80211Client) Close() error { + return errors.Join(c.conn.Close(), c.Client.Close()) +} + +func collect(ctx context.Context) map[string]system.WiFi { + client, err := newNL80211Client() if err != nil { return nil } @@ -59,17 +120,12 @@ func collectLinux(ctx context.Context, client linuxClient) map[string]system.WiF // Station statistics may require permissions unavailable in default // containers. Keep association even when RSSI cannot be read. Do not // substitute cached scan signal, which may be arbitrarily old. - stations, err := client.StationInfo(iface) - if err == nil { - for _, station := range stations { - if station == nil || len(bss.BSSID) == 0 || !bytes.Equal(station.HardwareAddr, bss.BSSID) { - continue - } + if len(bss.BSSID) > 0 { + if station, err := client.Station(iface, bss.BSSID); err == nil && station != nil { signal := float64(station.Signal) if signal >= -150 && signal < 0 { reading.Signal = &signal } - break } } result[iface.Name] = reading diff --git a/agent/wifi/wifi_linux_test.go b/agent/wifi/wifi_linux_test.go index 9a83042c2..338c77451 100644 --- a/agent/wifi/wifi_linux_test.go +++ b/agent/wifi/wifi_linux_test.go @@ -3,6 +3,7 @@ package wifi import ( + "bytes" "context" "errors" "net" @@ -27,9 +28,17 @@ func (f *fakeLinuxClient) Interfaces() ([]*native.Interface, error) { func (f *fakeLinuxClient) BSS(i *native.Interface) (*native.BSS, error) { return f.bss[i.Name], f.bssErr } -func (f *fakeLinuxClient) StationInfo(i *native.Interface) ([]*native.StationInfo, error) { +func (f *fakeLinuxClient) Station(i *native.Interface, mac net.HardwareAddr) (*native.StationInfo, error) { f.stationCalls++ - return f.stations[i.Name], f.stationErr + if f.stationErr != nil { + return nil, f.stationErr + } + for _, station := range f.stations[i.Name] { + if bytes.Equal(station.HardwareAddr, mac) { + return station, nil + } + } + return nil, errors.New("no such station") } func (f *fakeLinuxClient) SetDeadline(d time.Time) error { f.deadline = d; return f.deadlineErr } func (f *fakeLinuxClient) Close() error { return nil } diff --git a/go.mod b/go.mod index acbc8c4df..675d35754 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,8 @@ require ( github.com/fxamacker/cbor/v2 v2.9.4 github.com/gliderlabs/ssh v0.3.8 github.com/lxzan/gws v1.10.2 + github.com/mdlayher/genetlink v1.4.0 + github.com/mdlayher/netlink v1.11.2 github.com/mdlayher/wifi v0.8.0 github.com/nicholas-fedor/shoutrrr v0.21.0 github.com/opencontainers/go-digest v1.0.0 @@ -52,8 +54,6 @@ require ( github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.24 // indirect - github.com/mdlayher/genetlink v1.4.0 // indirect - github.com/mdlayher/netlink v1.11.2 // indirect github.com/mdlayher/socket v0.6.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/pocketbase/ozzo-validation/v4 v4.3.0 // indirect