diff --git a/adapter/router.go b/adapter/router.go index ab2d916c..e4c3904d 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -41,6 +41,7 @@ type Router interface { NetworkMonitor() tun.NetworkUpdateMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor PackageManager() tun.PackageManager + WIFIState() WIFIState Rules() []Rule ClashServer() ClashServer @@ -78,3 +79,8 @@ type DNSRule interface { type InterfaceUpdateListener interface { InterfaceUpdated() } + +type WIFIState struct { + SSID string + BSSID string +} diff --git a/experimental/libbox/platform.go b/experimental/libbox/platform.go index b7418bd2..451a72a9 100644 --- a/experimental/libbox/platform.go +++ b/experimental/libbox/platform.go @@ -19,6 +19,7 @@ type PlatformInterface interface { UsePlatformInterfaceGetter() bool GetInterfaces() (NetworkInterfaceIterator, error) UnderNetworkExtension() bool + ReadWIFIState() *WIFIState ClearDNSCache() } @@ -38,6 +39,15 @@ type NetworkInterface struct { Addresses StringIterator } +type WIFIState struct { + SSID string + BSSID string +} + +func NewWIFIState(wifiSSID string, wifiBSSID string) *WIFIState { + return &WIFIState{wifiSSID, wifiBSSID} +} + type NetworkInterfaceIterator interface { Next() *NetworkInterface HasNext() bool diff --git a/experimental/libbox/platform/interface.go b/experimental/libbox/platform/interface.go index 8c471971..54d35fa3 100644 --- a/experimental/libbox/platform/interface.go +++ b/experimental/libbox/platform/interface.go @@ -23,6 +23,7 @@ type Interface interface { Interfaces() ([]NetworkInterface, error) UnderNetworkExtension() bool ClearDNSCache() + ReadWIFIState() adapter.WIFIState process.Searcher } diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index ce4d6d2a..3375f750 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -210,6 +210,14 @@ func (w *platformInterfaceWrapper) ClearDNSCache() { w.iif.ClearDNSCache() } +func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState { + wifiState := w.iif.ReadWIFIState() + if wifiState == nil { + return adapter.WIFIState{} + } + return (adapter.WIFIState)(*wifiState) +} + func (w *platformInterfaceWrapper) DisableColors() bool { return runtime.GOOS != "android" } diff --git a/option/rule.go b/option/rule.go index f78a752d..8caba96e 100644 --- a/option/rule.go +++ b/option/rule.go @@ -78,6 +78,8 @@ type DefaultRule struct { User Listable[string] `json:"user,omitempty"` UserID Listable[int32] `json:"user_id,omitempty"` ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` Invert bool `json:"invert,omitempty"` Outbound string `json:"outbound,omitempty"` } diff --git a/option/rule_dns.go b/option/rule_dns.go index 563d3085..ba572b9a 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -78,6 +78,8 @@ type DefaultDNSRule struct { UserID Listable[int32] `json:"user_id,omitempty"` Outbound Listable[string] `json:"outbound,omitempty"` ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` Invert bool `json:"invert,omitempty"` Server string `json:"server,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` diff --git a/route/router.go b/route/router.go index 2d2f2cde..972c4ec0 100644 --- a/route/router.go +++ b/route/router.go @@ -86,6 +86,8 @@ type Router struct { clashServer adapter.ClashServer v2rayServer adapter.V2RayServer platformInterface platform.Interface + needWIFIState bool + wifiState adapter.WIFIState } func NewRouter( @@ -116,6 +118,7 @@ func NewRouter( defaultMark: options.DefaultMark, pauseManager: pause.ManagerFromContext(ctx), platformInterface: platformInterface, + needWIFIState: hasRule(options.Rules, isWIFIRule) || hasDNSRule(dnsOptions.Rules, isWIFIDNSRule), } router.dnsClient = dns.NewClient(dns.ClientOptions{ DisableCache: dnsOptions.DNSClientOptions.DisableCache, @@ -328,6 +331,11 @@ func NewRouter( service.ContextWith[serviceNTP.TimeService](ctx, timeService) router.timeService = timeService } + if platformInterface != nil && router.interfaceMonitor != nil && router.needWIFIState { + router.interfaceMonitor.RegisterCallback(func(_ int) { + router.updateWIFIState() + }) + } return router, nil } @@ -468,6 +476,9 @@ func (r *Router) Start() error { r.geositeCache = nil r.geositeReader = nil } + if r.needWIFIState { + r.updateWIFIState() + } for i, rule := range r.rules { err := rule.Start() if err != nil { @@ -940,6 +951,10 @@ func (r *Router) Rules() []adapter.Rule { return r.rules } +func (r *Router) WIFIState() adapter.WIFIState { + return r.wifiState +} + func (r *Router) NetworkMonitor() tun.NetworkUpdateMonitor { return r.networkMonitor } @@ -1019,3 +1034,14 @@ func (r *Router) ResetNetwork() error { } return nil } + +func (r *Router) updateWIFIState() { + if r.platformInterface == nil { + return + } + state := r.platformInterface.ReadWIFIState() + if state != r.wifiState { + r.wifiState = state + r.logger.Info("updated WIFI state: SSID=", state.SSID, ", BSSID=", state.BSSID) + } +} diff --git a/route/router_geo_resources.go b/route/router_geo_resources.go index 523833dc..8715cf92 100644 --- a/route/router_geo_resources.go +++ b/route/router_geo_resources.go @@ -307,3 +307,11 @@ func isProcessDNSRule(rule option.DefaultDNSRule) bool { func notPrivateNode(code string) bool { return code != "private" } + +func isWIFIRule(rule option.DefaultRule) bool { + return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 +} + +func isWIFIDNSRule(rule option.DefaultDNSRule) bool { + return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 +} diff --git a/route/rule_default.go b/route/rule_default.go index 01322c13..2d62f97a 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -184,6 +184,16 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.WIFISSID) > 0 { + item := NewWIFISSIDItem(router, options.WIFISSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFIBSSID) > 0 { + item := NewWIFIBSSIDItem(router, options.WIFIBSSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } diff --git a/route/rule_dns.go b/route/rule_dns.go index 15e4b16f..5132f024 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -180,6 +180,16 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.WIFISSID) > 0 { + item := NewWIFISSIDItem(router, options.WIFISSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFIBSSID) > 0 { + item := NewWIFIBSSIDItem(router, options.WIFIBSSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } return rule, nil } diff --git a/route/rule_item_wifi_bssid.go b/route/rule_item_wifi_bssid.go new file mode 100644 index 00000000..3b1ff9c8 --- /dev/null +++ b/route/rule_item_wifi_bssid.go @@ -0,0 +1,39 @@ +package route + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*WIFIBSSIDItem)(nil) + +type WIFIBSSIDItem struct { + bssidList []string + bssidMap map[string]bool + router adapter.Router +} + +func NewWIFIBSSIDItem(router adapter.Router, bssidList []string) *WIFIBSSIDItem { + bssidMap := make(map[string]bool) + for _, bssid := range bssidList { + bssidMap[bssid] = true + } + return &WIFIBSSIDItem{ + bssidList, + bssidMap, + router, + } +} + +func (r *WIFIBSSIDItem) Match(metadata *adapter.InboundContext) bool { + return r.bssidMap[r.router.WIFIState().BSSID] +} + +func (r *WIFIBSSIDItem) String() string { + if len(r.bssidList) == 1 { + return F.ToString("wifi_bssid=", r.bssidList[0]) + } + return F.ToString("wifi_bssid=[", strings.Join(r.bssidList, " "), "]") +} diff --git a/route/rule_item_wifi_ssid.go b/route/rule_item_wifi_ssid.go new file mode 100644 index 00000000..62cf935e --- /dev/null +++ b/route/rule_item_wifi_ssid.go @@ -0,0 +1,39 @@ +package route + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*WIFISSIDItem)(nil) + +type WIFISSIDItem struct { + ssidList []string + ssidMap map[string]bool + router adapter.Router +} + +func NewWIFISSIDItem(router adapter.Router, ssidList []string) *WIFISSIDItem { + ssidMap := make(map[string]bool) + for _, ssid := range ssidList { + ssidMap[ssid] = true + } + return &WIFISSIDItem{ + ssidList, + ssidMap, + router, + } +} + +func (r *WIFISSIDItem) Match(metadata *adapter.InboundContext) bool { + return r.ssidMap[r.router.WIFIState().SSID] +} + +func (r *WIFISSIDItem) String() string { + if len(r.ssidList) == 1 { + return F.ToString("wifi_ssid=", r.ssidList[0]) + } + return F.ToString("wifi_ssid=[", strings.Join(r.ssidList, " "), "]") +}