//go:build linux package settings import ( "context" "strings" "time" "github.com/sagernet/sing-box/adapter" "github.com/godbus/dbus/v5" ) type iwdMonitor struct { conn *dbus.Conn callback func(adapter.WIFIState) cancel context.CancelFunc signalChan chan *dbus.Signal } func newIWDMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { conn, err := dbus.ConnectSystemBus() if err != nil { return nil, err } iwdObj := conn.Object("net.connman.iwd", "/") ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() call := iwdObj.CallWithContext(ctx, "org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0) if call.Err != nil { conn.Close() return nil, call.Err } return &iwdMonitor{conn: conn, callback: callback}, nil } func (m *iwdMonitor) ReadWIFIState() adapter.WIFIState { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() iwdObj := m.conn.Object("net.connman.iwd", "/") var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant err := iwdObj.CallWithContext(ctx, "org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&objects) if err != nil { return adapter.WIFIState{} } for _, interfaces := range objects { stationProps, hasStation := interfaces["net.connman.iwd.Station"] if !hasStation { continue } stateVariant, hasState := stationProps["State"] if !hasState { continue } state, ok := stateVariant.Value().(string) if !ok || state != "connected" { continue } connectedNetworkVariant, hasNetwork := stationProps["ConnectedNetwork"] if !hasNetwork { continue } networkPath, ok := connectedNetworkVariant.Value().(dbus.ObjectPath) if !ok || networkPath == "/" { continue } networkInterfaces, hasNetworkPath := objects[networkPath] if !hasNetworkPath { continue } networkProps, hasNetworkInterface := networkInterfaces["net.connman.iwd.Network"] if !hasNetworkInterface { continue } nameVariant, hasName := networkProps["Name"] if !hasName { continue } ssid, ok := nameVariant.Value().(string) if !ok { continue } connectedBSSVariant, hasBSS := stationProps["ConnectedAccessPoint"] if !hasBSS { return adapter.WIFIState{SSID: ssid} } bssPath, ok := connectedBSSVariant.Value().(dbus.ObjectPath) if !ok || bssPath == "/" { return adapter.WIFIState{SSID: ssid} } bssInterfaces, hasBSSPath := objects[bssPath] if !hasBSSPath { return adapter.WIFIState{SSID: ssid} } bssProps, hasBSSInterface := bssInterfaces["net.connman.iwd.BasicServiceSet"] if !hasBSSInterface { return adapter.WIFIState{SSID: ssid} } addressVariant, hasAddress := bssProps["Address"] if !hasAddress { return adapter.WIFIState{SSID: ssid} } bssid, ok := addressVariant.Value().(string) if !ok { return adapter.WIFIState{SSID: ssid} } return adapter.WIFIState{ SSID: ssid, BSSID: strings.ToUpper(strings.ReplaceAll(bssid, ":", "")), } } return adapter.WIFIState{} } func (m *iwdMonitor) Start() error { if m.callback == nil { return nil } ctx, cancel := context.WithCancel(context.Background()) m.cancel = cancel m.signalChan = make(chan *dbus.Signal, 10) m.conn.Signal(m.signalChan) err := m.conn.AddMatchSignal( dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), dbus.WithMatchSender("net.connman.iwd"), ) if err != nil { return err } state := m.ReadWIFIState() go m.monitorSignals(ctx, m.signalChan, state) m.callback(state) return nil } func (m *iwdMonitor) monitorSignals(ctx context.Context, signalChan chan *dbus.Signal, lastState adapter.WIFIState) { for { select { case <-ctx.Done(): return case signal, ok := <-signalChan: if !ok { return } if signal.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" { state := m.ReadWIFIState() if state != lastState { lastState = state m.callback(state) } } } } } func (m *iwdMonitor) Close() error { if m.cancel != nil { m.cancel() } if m.signalChan != nil { m.conn.RemoveSignal(m.signalChan) close(m.signalChan) } if m.conn != nil { m.conn.RemoveMatchSignal( dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), dbus.WithMatchSender("net.connman.iwd"), ) return m.conn.Close() } return nil }