Skip to content

Commit ab3caab

Browse files
committed
Add Linux WI-FI state support
Support monitoring WIFI state on Linux through: - NetworkManager (D-Bus) - IWD (D-Bus) - wpa_supplicant (control socket) - ConnMan (D-Bus)
1 parent 1338c1e commit ab3caab

File tree

19 files changed

+912
-30
lines changed

19 files changed

+912
-30
lines changed

adapter/network.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
type NetworkManager interface {
1212
Lifecycle
13+
Initialize(ruleSets []RuleSet)
1314
InterfaceFinder() control.InterfaceFinder
1415
UpdateInterfaces() error
1516
DefaultNetworkInterface() *NetworkInterface
@@ -24,9 +25,10 @@ type NetworkManager interface {
2425
NetworkMonitor() tun.NetworkUpdateMonitor
2526
InterfaceMonitor() tun.DefaultInterfaceMonitor
2627
PackageManager() tun.PackageManager
28+
NeedWIFIState() bool
2729
WIFIState() WIFIState
28-
ResetNetwork()
2930
UpdateWIFIState()
31+
ResetNetwork()
3032
}
3133

3234
type NetworkOptions struct {

adapter/router.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ type Router interface {
2424
PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
2525
ConnectionRouterEx
2626
RuleSet(tag string) (RuleSet, bool)
27-
NeedWIFIState() bool
2827
Rules() []Rule
2928
AppendTracker(tracker ConnectionTracker)
3029
ResetNetwork()

box.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ func New(options Options) (*Box, error) {
184184
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
185185
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
186186
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
187-
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
187+
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
188188
if err != nil {
189189
return nil, E.Cause(err, "initialize network manager")
190190
}

common/settings/wifi.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package settings
2+
3+
import "github.com/sagernet/sing-box/adapter"
4+
5+
type WIFIMonitor interface {
6+
ReadWIFIState() adapter.WIFIState
7+
Start() error
8+
Close() error
9+
}

common/settings/wifi_linux.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package settings
2+
3+
import (
4+
"github.com/sagernet/sing-box/adapter"
5+
E "github.com/sagernet/sing/common/exceptions"
6+
)
7+
8+
type LinuxWIFIMonitor struct {
9+
monitor WIFIMonitor
10+
}
11+
12+
func NewWIFIMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
13+
monitors := []func(func(adapter.WIFIState)) (WIFIMonitor, error){
14+
newNetworkManagerMonitor,
15+
newIWDMonitor,
16+
newWpaSupplicantMonitor,
17+
newConnManMonitor,
18+
}
19+
var errors []error
20+
for _, factory := range monitors {
21+
monitor, err := factory(callback)
22+
if err == nil {
23+
return &LinuxWIFIMonitor{monitor: monitor}, nil
24+
}
25+
errors = append(errors, err)
26+
}
27+
return nil, E.Cause(E.Errors(errors...), "no supported WIFI manager found")
28+
}
29+
30+
func (m *LinuxWIFIMonitor) ReadWIFIState() adapter.WIFIState {
31+
return m.monitor.ReadWIFIState()
32+
}
33+
34+
func (m *LinuxWIFIMonitor) Start() error {
35+
if m.monitor != nil {
36+
return m.monitor.Start()
37+
}
38+
return nil
39+
}
40+
41+
func (m *LinuxWIFIMonitor) Close() error {
42+
if m.monitor != nil {
43+
return m.monitor.Close()
44+
}
45+
return nil
46+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package settings
2+
3+
import (
4+
"context"
5+
"strings"
6+
"time"
7+
8+
"github.com/sagernet/sing-box/adapter"
9+
10+
"github.com/godbus/dbus/v5"
11+
)
12+
13+
type connmanMonitor struct {
14+
conn *dbus.Conn
15+
callback func(adapter.WIFIState)
16+
cancel context.CancelFunc
17+
signalChan chan *dbus.Signal
18+
}
19+
20+
func newConnManMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
21+
conn, err := dbus.ConnectSystemBus()
22+
if err != nil {
23+
return nil, err
24+
}
25+
cmObj := conn.Object("net.connman", "/")
26+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
27+
defer cancel()
28+
call := cmObj.CallWithContext(ctx, "net.connman.Manager.GetServices", 0)
29+
if call.Err != nil {
30+
conn.Close()
31+
return nil, call.Err
32+
}
33+
return &connmanMonitor{conn: conn, callback: callback}, nil
34+
}
35+
36+
func (m *connmanMonitor) ReadWIFIState() adapter.WIFIState {
37+
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
38+
defer cancel()
39+
40+
cmObj := m.conn.Object("net.connman", "/")
41+
var services []interface{}
42+
err := cmObj.CallWithContext(ctx, "net.connman.Manager.GetServices", 0).Store(&services)
43+
if err != nil {
44+
return adapter.WIFIState{}
45+
}
46+
47+
for _, service := range services {
48+
servicePair, ok := service.([]interface{})
49+
if !ok || len(servicePair) != 2 {
50+
continue
51+
}
52+
53+
serviceProps, ok := servicePair[1].(map[string]dbus.Variant)
54+
if !ok {
55+
continue
56+
}
57+
58+
typeVariant, hasType := serviceProps["Type"]
59+
if !hasType {
60+
continue
61+
}
62+
serviceType, ok := typeVariant.Value().(string)
63+
if !ok || serviceType != "wifi" {
64+
continue
65+
}
66+
67+
stateVariant, hasState := serviceProps["State"]
68+
if !hasState {
69+
continue
70+
}
71+
state, ok := stateVariant.Value().(string)
72+
if !ok || (state != "online" && state != "ready") {
73+
continue
74+
}
75+
76+
nameVariant, hasName := serviceProps["Name"]
77+
if !hasName {
78+
continue
79+
}
80+
ssid, ok := nameVariant.Value().(string)
81+
if !ok || ssid == "" {
82+
continue
83+
}
84+
85+
bssidVariant, hasBSSID := serviceProps["BSSID"]
86+
if !hasBSSID {
87+
return adapter.WIFIState{SSID: ssid}
88+
}
89+
bssid, ok := bssidVariant.Value().(string)
90+
if !ok {
91+
return adapter.WIFIState{SSID: ssid}
92+
}
93+
94+
return adapter.WIFIState{
95+
SSID: ssid,
96+
BSSID: strings.ToUpper(strings.ReplaceAll(bssid, ":", "")),
97+
}
98+
}
99+
100+
return adapter.WIFIState{}
101+
}
102+
103+
func (m *connmanMonitor) Start() error {
104+
if m.callback == nil {
105+
return nil
106+
}
107+
ctx, cancel := context.WithCancel(context.Background())
108+
m.cancel = cancel
109+
110+
m.signalChan = make(chan *dbus.Signal, 10)
111+
m.conn.Signal(m.signalChan)
112+
113+
err := m.conn.AddMatchSignal(
114+
dbus.WithMatchInterface("net.connman.Service"),
115+
dbus.WithMatchSender("net.connman"),
116+
)
117+
if err != nil {
118+
return err
119+
}
120+
121+
state := m.ReadWIFIState()
122+
go m.monitorSignals(ctx, m.signalChan, state)
123+
m.callback(state)
124+
125+
return nil
126+
}
127+
128+
func (m *connmanMonitor) monitorSignals(ctx context.Context, signalChan chan *dbus.Signal, lastState adapter.WIFIState) {
129+
for {
130+
select {
131+
case <-ctx.Done():
132+
return
133+
case signal, ok := <-signalChan:
134+
if !ok {
135+
return
136+
}
137+
// godbus Signal.Name uses "interface.member" format (e.g. "net.connman.Service.PropertyChanged"),
138+
// not just the member name. This differs from the D-Bus signal member in the match rule.
139+
if signal.Name == "net.connman.Service.PropertyChanged" {
140+
state := m.ReadWIFIState()
141+
if state != lastState {
142+
lastState = state
143+
m.callback(state)
144+
}
145+
}
146+
}
147+
}
148+
}
149+
150+
func (m *connmanMonitor) Close() error {
151+
if m.cancel != nil {
152+
m.cancel()
153+
}
154+
if m.signalChan != nil {
155+
m.conn.RemoveSignal(m.signalChan)
156+
close(m.signalChan)
157+
}
158+
if m.conn != nil {
159+
m.conn.RemoveMatchSignal(
160+
dbus.WithMatchInterface("net.connman.Service"),
161+
dbus.WithMatchSender("net.connman"),
162+
)
163+
return m.conn.Close()
164+
}
165+
return nil
166+
}

0 commit comments

Comments
 (0)