diff --git a/adapter/network.go b/adapter/network.go index 1b26bed68..dd53b2b49 100644 --- a/adapter/network.go +++ b/adapter/network.go @@ -10,6 +10,7 @@ import ( type NetworkManager interface { Lifecycle + Initialize(ruleSets []RuleSet) InterfaceFinder() control.InterfaceFinder UpdateInterfaces() error DefaultNetworkInterface() *NetworkInterface @@ -24,9 +25,10 @@ type NetworkManager interface { NetworkMonitor() tun.NetworkUpdateMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor PackageManager() tun.PackageManager + NeedWIFIState() bool WIFIState() WIFIState - ResetNetwork() UpdateWIFIState() + ResetNetwork() } type NetworkOptions struct { diff --git a/adapter/platform.go b/adapter/platform.go index 61dc7b440..95db93c64 100644 --- a/adapter/platform.go +++ b/adapter/platform.go @@ -32,6 +32,8 @@ type PlatformInterface interface { UsePlatformConnectionOwnerFinder() bool FindConnectionOwner(request *FindConnectionOwnerRequest) (*ConnectionOwner, error) + UsePlatformWIFIMonitor() bool + UsePlatformNotification() bool SendNotification(notification *Notification) error } diff --git a/adapter/router.go b/adapter/router.go index 522a0d9d3..3cece2ee0 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -24,7 +24,6 @@ type Router interface { PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) ConnectionRouterEx RuleSet(tag string) (RuleSet, bool) - NeedWIFIState() bool Rules() []Rule AppendTracker(tracker ConnectionTracker) ResetNetwork() diff --git a/box.go b/box.go index 06ae69459..1c168820c 100644 --- a/box.go +++ b/box.go @@ -183,7 +183,7 @@ func New(options Options) (*Box, error) { service.MustRegister[adapter.ServiceManager](ctx, serviceManager) dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions) service.MustRegister[adapter.DNSRouter](ctx, dnsRouter) - networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions) + networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions) if err != nil { return nil, E.Cause(err, "initialize network manager") } diff --git a/common/settings/wifi.go b/common/settings/wifi.go new file mode 100644 index 000000000..62bef706b --- /dev/null +++ b/common/settings/wifi.go @@ -0,0 +1,9 @@ +package settings + +import "github.com/sagernet/sing-box/adapter" + +type WIFIMonitor interface { + ReadWIFIState() adapter.WIFIState + Start() error + Close() error +} diff --git a/common/settings/wifi_linux.go b/common/settings/wifi_linux.go new file mode 100644 index 000000000..9deed3c80 --- /dev/null +++ b/common/settings/wifi_linux.go @@ -0,0 +1,46 @@ +package settings + +import ( + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" +) + +type LinuxWIFIMonitor struct { + monitor WIFIMonitor +} + +func NewWIFIMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { + monitors := []func(func(adapter.WIFIState)) (WIFIMonitor, error){ + newNetworkManagerMonitor, + newIWDMonitor, + newWpaSupplicantMonitor, + newConnManMonitor, + } + var errors []error + for _, factory := range monitors { + monitor, err := factory(callback) + if err == nil { + return &LinuxWIFIMonitor{monitor: monitor}, nil + } + errors = append(errors, err) + } + return nil, E.Cause(E.Errors(errors...), "no supported WIFI manager found") +} + +func (m *LinuxWIFIMonitor) ReadWIFIState() adapter.WIFIState { + return m.monitor.ReadWIFIState() +} + +func (m *LinuxWIFIMonitor) Start() error { + if m.monitor != nil { + return m.monitor.Start() + } + return nil +} + +func (m *LinuxWIFIMonitor) Close() error { + if m.monitor != nil { + return m.monitor.Close() + } + return nil +} diff --git a/common/settings/wifi_linux_connman.go b/common/settings/wifi_linux_connman.go new file mode 100644 index 000000000..4721e5667 --- /dev/null +++ b/common/settings/wifi_linux_connman.go @@ -0,0 +1,160 @@ +package settings + +import ( + "context" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + + "github.com/godbus/dbus/v5" +) + +type connmanMonitor struct { + conn *dbus.Conn + callback func(adapter.WIFIState) + cancel context.CancelFunc + signalChan chan *dbus.Signal +} + +func newConnManMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { + conn, err := dbus.ConnectSystemBus() + if err != nil { + return nil, err + } + cmObj := conn.Object("net.connman", "/") + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + call := cmObj.CallWithContext(ctx, "net.connman.Manager.GetServices", 0) + if call.Err != nil { + conn.Close() + return nil, call.Err + } + return &connmanMonitor{conn: conn, callback: callback}, nil +} + +func (m *connmanMonitor) ReadWIFIState() adapter.WIFIState { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + cmObj := m.conn.Object("net.connman", "/") + var services []interface{} + err := cmObj.CallWithContext(ctx, "net.connman.Manager.GetServices", 0).Store(&services) + if err != nil { + return adapter.WIFIState{} + } + + for _, service := range services { + servicePair, ok := service.([]interface{}) + if !ok || len(servicePair) != 2 { + continue + } + + serviceProps, ok := servicePair[1].(map[string]dbus.Variant) + if !ok { + continue + } + + typeVariant, hasType := serviceProps["Type"] + if !hasType { + continue + } + serviceType, ok := typeVariant.Value().(string) + if !ok || serviceType != "wifi" { + continue + } + + stateVariant, hasState := serviceProps["State"] + if !hasState { + continue + } + state, ok := stateVariant.Value().(string) + if !ok || (state != "online" && state != "ready") { + continue + } + + nameVariant, hasName := serviceProps["Name"] + if !hasName { + continue + } + ssid, ok := nameVariant.Value().(string) + if !ok || ssid == "" { + continue + } + + bssidVariant, hasBSSID := serviceProps["BSSID"] + if !hasBSSID { + return adapter.WIFIState{SSID: ssid} + } + bssid, ok := bssidVariant.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 *connmanMonitor) 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("net.connman.Service"), + dbus.WithMatchSender("net.connman"), + ) + if err != nil { + return err + } + + state := m.ReadWIFIState() + go m.monitorSignals(ctx, m.signalChan, state) + m.callback(state) + + return nil +} + +func (m *connmanMonitor) 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 == "PropertyChanged" { + state := m.ReadWIFIState() + if state != lastState { + lastState = state + m.callback(state) + } + } + } + } +} + +func (m *connmanMonitor) Close() error { + if m.cancel != nil { + m.cancel() + } + if m.signalChan != nil { + m.conn.RemoveSignal(m.signalChan) + close(m.signalChan) + } + if m.conn != nil { + return m.conn.Close() + } + return nil +} diff --git a/common/settings/wifi_linux_iwd.go b/common/settings/wifi_linux_iwd.go new file mode 100644 index 000000000..c360508cc --- /dev/null +++ b/common/settings/wifi_linux_iwd.go @@ -0,0 +1,184 @@ +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 { + return m.conn.Close() + } + return nil +} diff --git a/common/settings/wifi_linux_nm.go b/common/settings/wifi_linux_nm.go new file mode 100644 index 000000000..53e731fdf --- /dev/null +++ b/common/settings/wifi_linux_nm.go @@ -0,0 +1,157 @@ +package settings + +import ( + "context" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + + "github.com/godbus/dbus/v5" +) + +type networkManagerMonitor struct { + conn *dbus.Conn + callback func(adapter.WIFIState) + cancel context.CancelFunc + signalChan chan *dbus.Signal +} + +func newNetworkManagerMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { + conn, err := dbus.ConnectSystemBus() + if err != nil { + return nil, err + } + nmObj := conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + var state uint32 + err = nmObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager", "State").Store(&state) + if err != nil { + conn.Close() + return nil, err + } + return &networkManagerMonitor{conn: conn, callback: callback}, nil +} + +func (m *networkManagerMonitor) ReadWIFIState() adapter.WIFIState { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + nmObj := m.conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager") + + var primaryConnectionPath dbus.ObjectPath + err := nmObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager", "PrimaryConnection").Store(&primaryConnectionPath) + if err != nil || primaryConnectionPath == "/" { + return adapter.WIFIState{} + } + + connObj := m.conn.Object("org.freedesktop.NetworkManager", primaryConnectionPath) + + var devicePaths []dbus.ObjectPath + err = connObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Connection.Active", "Devices").Store(&devicePaths) + if err != nil || len(devicePaths) == 0 { + return adapter.WIFIState{} + } + + for _, devicePath := range devicePaths { + deviceObj := m.conn.Object("org.freedesktop.NetworkManager", devicePath) + + var deviceType uint32 + err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device", "DeviceType").Store(&deviceType) + if err != nil || deviceType != 2 { + continue + } + + var accessPointPath dbus.ObjectPath + err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device.Wireless", "ActiveAccessPoint").Store(&accessPointPath) + if err != nil || accessPointPath == "/" { + continue + } + + apObj := m.conn.Object("org.freedesktop.NetworkManager", accessPointPath) + + var ssidBytes []byte + err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "Ssid").Store(&ssidBytes) + if err != nil { + continue + } + + var hwAddress string + err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "HwAddress").Store(&hwAddress) + if err != nil { + continue + } + + ssid := strings.TrimSpace(string(ssidBytes)) + if ssid == "" { + continue + } + + return adapter.WIFIState{ + SSID: ssid, + BSSID: strings.ToUpper(strings.ReplaceAll(hwAddress, ":", "")), + } + } + + return adapter.WIFIState{} +} + +func (m *networkManagerMonitor) 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.WithMatchSender("org.freedesktop.NetworkManager"), + dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), + ) + if err != nil { + return err + } + + state := m.ReadWIFIState() + go m.monitorSignals(ctx, m.signalChan, state) + m.callback(state) + + return nil +} + +func (m *networkManagerMonitor) 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 *networkManagerMonitor) Close() error { + if m.cancel != nil { + m.cancel() + } + if m.signalChan != nil { + m.conn.RemoveSignal(m.signalChan) + close(m.signalChan) + } + if m.conn != nil { + return m.conn.Close() + } + return nil +} diff --git a/common/settings/wifi_linux_wpa.go b/common/settings/wifi_linux_wpa.go new file mode 100644 index 000000000..0d2516990 --- /dev/null +++ b/common/settings/wifi_linux_wpa.go @@ -0,0 +1,179 @@ +package settings + +import ( + "bufio" + "context" + "fmt" + "net" + "os" + "path/filepath" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" +) + +type wpaSupplicantMonitor struct { + socketPath string + callback func(adapter.WIFIState) + cancel context.CancelFunc +} + +func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { + socketDirs := []string{"/var/run/wpa_supplicant", "/run/wpa_supplicant"} + for _, socketDir := range socketDirs { + entries, err := os.ReadDir(socketDir) + if err != nil { + continue + } + for _, entry := range entries { + if entry.IsDir() || entry.Name() == "." || entry.Name() == ".." { + continue + } + socketPath := filepath.Join(socketDir, entry.Name()) + localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d", os.Getpid()), Net: "unixgram"} + remoteAddr := &net.UnixAddr{Name: socketPath, Net: "unixgram"} + conn, err := net.DialUnix("unixgram", localAddr, remoteAddr) + if err != nil { + continue + } + conn.Close() + return &wpaSupplicantMonitor{socketPath: socketPath, callback: callback}, nil + } + } + return nil, os.ErrNotExist +} + +func (m *wpaSupplicantMonitor) ReadWIFIState() adapter.WIFIState { + localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d", os.Getpid()), Net: "unixgram"} + remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"} + conn, err := net.DialUnix("unixgram", localAddr, remoteAddr) + if err != nil { + return adapter.WIFIState{} + } + defer conn.Close() + + conn.SetDeadline(time.Now().Add(3 * time.Second)) + + status, err := m.sendCommand(conn, "STATUS") + if err != nil { + return adapter.WIFIState{} + } + + var ssid, bssid string + var connected bool + scanner := bufio.NewScanner(strings.NewReader(status)) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "wpa_state=") { + state := strings.TrimPrefix(line, "wpa_state=") + connected = state == "COMPLETED" + } else if strings.HasPrefix(line, "ssid=") { + ssid = strings.TrimPrefix(line, "ssid=") + } else if strings.HasPrefix(line, "bssid=") { + bssid = strings.TrimPrefix(line, "bssid=") + } + } + + if !connected || ssid == "" { + return adapter.WIFIState{} + } + + return adapter.WIFIState{ + SSID: ssid, + BSSID: strings.ToUpper(strings.ReplaceAll(bssid, ":", "")), + } +} + +func (m *wpaSupplicantMonitor) sendCommand(conn *net.UnixConn, command string) (string, error) { + _, err := conn.Write([]byte(command + "\n")) + if err != nil { + return "", err + } + + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil { + return "", err + } + + response := string(buf[:n]) + if strings.HasPrefix(response, "FAIL") { + return "", os.ErrInvalid + } + + return strings.TrimSpace(response), nil +} + +func (m *wpaSupplicantMonitor) Start() error { + if m.callback == nil { + return nil + } + ctx, cancel := context.WithCancel(context.Background()) + m.cancel = cancel + + state := m.ReadWIFIState() + go m.monitorEvents(ctx, state) + m.callback(state) + + return nil +} + +func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adapter.WIFIState) { + var consecutiveErrors int + + localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-mon-%d", os.Getpid()), Net: "unixgram"} + remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"} + conn, err := net.DialUnix("unixgram", localAddr, remoteAddr) + if err != nil { + return + } + defer conn.Close() + + _, err = conn.Write([]byte("ATTACH\n")) + if err != nil { + return + } + + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil || !strings.HasPrefix(string(buf[:n]), "OK") { + return + } + + for { + select { + case <-ctx.Done(): + return + default: + } + + conn.SetReadDeadline(time.Now().Add(30 * time.Second)) + n, err := conn.Read(buf) + if err != nil { + consecutiveErrors++ + if consecutiveErrors > 10 { + return + } + time.Sleep(time.Second) + continue + } + consecutiveErrors = 0 + + msg := string(buf[:n]) + if strings.Contains(msg, "CTRL-EVENT-CONNECTED") || strings.Contains(msg, "CTRL-EVENT-DISCONNECTED") { + state := m.ReadWIFIState() + if state != lastState { + lastState = state + m.callback(state) + } + } + } +} + +func (m *wpaSupplicantMonitor) Close() error { + if m.cancel != nil { + m.cancel() + } + return nil +} diff --git a/common/settings/wifi_stub.go b/common/settings/wifi_stub.go new file mode 100644 index 000000000..98db500fd --- /dev/null +++ b/common/settings/wifi_stub.go @@ -0,0 +1,27 @@ +//go:build !linux + +package settings + +import ( + "os" + + "github.com/sagernet/sing-box/adapter" +) + +type stubWIFIMonitor struct{} + +func NewWIFIMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) { + return nil, os.ErrInvalid +} + +func (m *stubWIFIMonitor) ReadWIFIState() adapter.WIFIState { + return adapter.WIFIState{} +} + +func (m *stubWIFIMonitor) Start() error { + return nil +} + +func (m *stubWIFIMonitor) Close() error { + return nil +} diff --git a/daemon/started_service.go b/daemon/started_service.go index 7ee868bb9..2c1b532bd 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -18,7 +18,6 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/batch" E "github.com/sagernet/sing/common/exceptions" - F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/memory" "github.com/sagernet/sing/common/observable" "github.com/sagernet/sing/common/x/list" diff --git a/experimental/libbox/command_server.go b/experimental/libbox/command_server.go index 5c09a9845..5d4b68e10 100644 --- a/experimental/libbox/command_server.go +++ b/experimental/libbox/command_server.go @@ -151,7 +151,7 @@ func (s *CommandServer) NeedWIFIState() bool { if instance == nil || instance.Box() == nil { return false } - return instance.Box().Router().NeedWIFIState() + return instance.Box().Network().NeedWIFIState() } func (s *CommandServer) Pause() { diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index 946eea261..c89f3d69f 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -116,6 +116,10 @@ func (s *platformInterfaceStub) RequestPermissionForWIFIState() error { return nil } +func (s *platformInterfaceStub) UsePlatformWIFIMonitor() bool { + return false +} + func (s *platformInterfaceStub) ReadWIFIState() adapter.WIFIState { return adapter.WIFIState{} } diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 972ef5c89..e73df0274 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -144,6 +144,10 @@ func (w *platformInterfaceWrapper) RequestPermissionForWIFIState() error { return nil } +func (w *platformInterfaceWrapper) UsePlatformWIFIMonitor() bool { + return true +} + func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState { wifiState := w.iif.ReadWIFIState() if wifiState == nil { diff --git a/route/network.go b/route/network.go index 55bd6b9e9..b53142b57 100644 --- a/route/network.go +++ b/route/network.go @@ -8,11 +8,13 @@ import ( "os" "runtime" "strings" + "sync" "syscall" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/conntrack" + "github.com/sagernet/sing-box/common/settings" "github.com/sagernet/sing-box/common/taskmonitor" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" @@ -49,28 +51,31 @@ type NetworkManager struct { endpoint adapter.EndpointManager inbound adapter.InboundManager outbound adapter.OutboundManager + needWIFIState bool + wifiMonitor settings.WIFIMonitor wifiState adapter.WIFIState + wifiStateMutex sync.RWMutex started bool } -func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOptions option.RouteOptions) (*NetworkManager, error) { - defaultDomainResolver := common.PtrValueOrDefault(routeOptions.DefaultDomainResolver) - if routeOptions.AutoDetectInterface && !(C.IsLinux || C.IsDarwin || C.IsWindows) { +func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, options option.RouteOptions, dnsOptions option.DNSOptions) (*NetworkManager, error) { + defaultDomainResolver := common.PtrValueOrDefault(options.DefaultDomainResolver) + if options.AutoDetectInterface && !(C.IsLinux || C.IsDarwin || C.IsWindows) { return nil, E.New("`auto_detect_interface` is only supported on Linux, Windows and macOS") - } else if routeOptions.OverrideAndroidVPN && !C.IsAndroid { + } else if options.OverrideAndroidVPN && !C.IsAndroid { return nil, E.New("`override_android_vpn` is only supported on Android") - } else if routeOptions.DefaultInterface != "" && !(C.IsLinux || C.IsDarwin || C.IsWindows) { + } else if options.DefaultInterface != "" && !(C.IsLinux || C.IsDarwin || C.IsWindows) { return nil, E.New("`default_interface` is only supported on Linux, Windows and macOS") - } else if routeOptions.DefaultMark != 0 && !C.IsLinux { + } else if options.DefaultMark != 0 && !C.IsLinux { return nil, E.New("`default_mark` is only supported on linux") } nm := &NetworkManager{ logger: logger, interfaceFinder: control.NewDefaultInterfaceFinder(), - autoDetectInterface: routeOptions.AutoDetectInterface, + autoDetectInterface: options.AutoDetectInterface, defaultOptions: adapter.NetworkOptions{ - BindInterface: routeOptions.DefaultInterface, - RoutingMark: uint32(routeOptions.DefaultMark), + BindInterface: options.DefaultInterface, + RoutingMark: uint32(options.DefaultMark), DomainResolver: defaultDomainResolver.Server, DomainResolveOptions: adapter.DNSQueryOptions{ Strategy: C.DomainStrategy(defaultDomainResolver.Strategy), @@ -78,27 +83,28 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOp RewriteTTL: defaultDomainResolver.RewriteTTL, ClientSubnet: defaultDomainResolver.ClientSubnet.Build(netip.Prefix{}), }, - NetworkStrategy: (*C.NetworkStrategy)(routeOptions.DefaultNetworkStrategy), - NetworkType: common.Map(routeOptions.DefaultNetworkType, option.InterfaceType.Build), - FallbackNetworkType: common.Map(routeOptions.DefaultFallbackNetworkType, option.InterfaceType.Build), - FallbackDelay: time.Duration(routeOptions.DefaultFallbackDelay), + NetworkStrategy: (*C.NetworkStrategy)(options.DefaultNetworkStrategy), + NetworkType: common.Map(options.DefaultNetworkType, option.InterfaceType.Build), + FallbackNetworkType: common.Map(options.DefaultFallbackNetworkType, option.InterfaceType.Build), + FallbackDelay: time.Duration(options.DefaultFallbackDelay), }, pauseManager: service.FromContext[pause.Manager](ctx), platformInterface: service.FromContext[adapter.PlatformInterface](ctx), endpoint: service.FromContext[adapter.EndpointManager](ctx), inbound: service.FromContext[adapter.InboundManager](ctx), outbound: service.FromContext[adapter.OutboundManager](ctx), + needWIFIState: hasRule(options.Rules, isWIFIRule) || hasDNSRule(dnsOptions.Rules, isWIFIDNSRule), } - if routeOptions.DefaultNetworkStrategy != nil { - if routeOptions.DefaultInterface != "" { + if options.DefaultNetworkStrategy != nil { + if options.DefaultInterface != "" { return nil, E.New("`default_network_strategy` is conflict with `default_interface`") } - if !routeOptions.AutoDetectInterface { + if !options.AutoDetectInterface { return nil, E.New("`auto_detect_interface` is required by `default_network_strategy`") } } usePlatformDefaultInterfaceMonitor := nm.platformInterface != nil - enforceInterfaceMonitor := routeOptions.AutoDetectInterface + enforceInterfaceMonitor := options.AutoDetectInterface if !usePlatformDefaultInterfaceMonitor { networkMonitor, err := tun.NewNetworkUpdateMonitor(logger) if !((err != nil && !enforceInterfaceMonitor) || errors.Is(err, os.ErrInvalid)) { @@ -108,7 +114,7 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOp nm.networkMonitor = networkMonitor interfaceMonitor, err := tun.NewDefaultInterfaceMonitor(nm.networkMonitor, logger, tun.DefaultInterfaceMonitorOptions{ InterfaceFinder: nm.interfaceFinder, - OverrideAndroidVPN: routeOptions.OverrideAndroidVPN, + OverrideAndroidVPN: options.OverrideAndroidVPN, UnderNetworkExtension: nm.platformInterface != nil && nm.platformInterface.UnderNetworkExtension(), }) if err != nil { @@ -182,11 +188,35 @@ func (r *NetworkManager) Start(stage adapter.StartStage) error { } } case adapter.StartStatePostStart: + if r.needWIFIState && !(r.platformInterface != nil && r.platformInterface.UsePlatformWIFIMonitor()) { + wifiMonitor, err := settings.NewWIFIMonitor(r.onWIFIStateChanged) + if err != nil { + if err != os.ErrInvalid { + r.logger.Warn(E.Cause(err, "create WIFI monitor")) + } + } else { + r.wifiMonitor = wifiMonitor + err = r.wifiMonitor.Start() + if err != nil { + r.logger.Warn(E.Cause(err, "start WIFI monitor")) + } + } + } r.started = true } return nil } +func (r *NetworkManager) Initialize(ruleSets []adapter.RuleSet) { + for _, ruleSet := range ruleSets { + metadata := ruleSet.Metadata() + if metadata.ContainsWIFIRule { + r.needWIFIState = true + break + } + } +} + func (r *NetworkManager) Close() error { monitor := taskmonitor.New(r.logger, C.StopTimeout) var err error @@ -218,6 +248,13 @@ func (r *NetworkManager) Close() error { }) monitor.Finish() } + if r.wifiMonitor != nil { + monitor.Start("close WIFI monitor") + err = E.Append(err, r.wifiMonitor.Close(), func(err error) error { + return E.Cause(err, "close WIFI monitor") + }) + monitor.Finish() + } return err } @@ -375,22 +412,43 @@ func (r *NetworkManager) PackageManager() tun.PackageManager { return r.packageManager } +func (r *NetworkManager) NeedWIFIState() bool { + return r.needWIFIState +} + func (r *NetworkManager) WIFIState() adapter.WIFIState { + r.wifiStateMutex.RLock() + defer r.wifiStateMutex.RUnlock() return r.wifiState } -func (r *NetworkManager) UpdateWIFIState() { - if r.platformInterface != nil { - state := r.platformInterface.ReadWIFIState() - if state != r.wifiState { - r.wifiState = state - if state.SSID != "" { - r.logger.Info("updated WIFI state: SSID=", state.SSID, ", BSSID=", state.BSSID) - } +func (r *NetworkManager) onWIFIStateChanged(state adapter.WIFIState) { + r.wifiStateMutex.Lock() + if state != r.wifiState { + r.wifiState = state + r.wifiStateMutex.Unlock() + if state.SSID != "" { + r.logger.Info("WIFI state changed: SSID=", state.SSID, ", BSSID=", state.BSSID) + } else { + r.logger.Info("WIFI disconnected") } + } else { + r.wifiStateMutex.Unlock() } } +func (r *NetworkManager) UpdateWIFIState() { + var state adapter.WIFIState + if r.wifiMonitor != nil { + state = r.wifiMonitor.ReadWIFIState() + } else if r.platformInterface != nil && r.platformInterface.UsePlatformWIFIMonitor() { + state = r.platformInterface.ReadWIFIState() + } else { + return + } + r.onWIFIStateChanged(state) +} + func (r *NetworkManager) ResetNetwork() { conntrack.Close() diff --git a/route/router.go b/route/router.go index ef5cea108..439366c20 100644 --- a/route/router.go +++ b/route/router.go @@ -56,7 +56,6 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess, pauseManager: service.FromContext[pause.Manager](ctx), platformInterface: service.FromContext[adapter.PlatformInterface](ctx), - needWIFIState: hasRule(options.Rules, isWIFIRule) || hasDNSRule(dnsOptions.Rules, isWIFIDNSRule), } } @@ -118,10 +117,8 @@ func (r *Router) Start(stage adapter.StartStage) error { if metadata.ContainsProcessRule { needFindProcess = true } - if metadata.ContainsWIFIRule { - r.needWIFIState = true - } } + r.network.Initialize(r.ruleSets) if needFindProcess { if r.platformInterface != nil && r.platformInterface.UsePlatformConnectionOwnerFinder() { r.processSearcher = newPlatformSearcher(r.platformInterface) @@ -194,10 +191,6 @@ func (r *Router) RuleSet(tag string) (adapter.RuleSet, bool) { return ruleSet, loaded } -func (r *Router) NeedWIFIState() bool { - return r.needWIFIState -} - func (r *Router) Rules() []adapter.Rule { return r.rules }