diff --git a/cmd/sing-box/internal/convertor/adguard/convertor_test.go b/cmd/sing-box/internal/convertor/adguard/convertor_test.go index 6098485c..212c2170 100644 --- a/cmd/sing-box/internal/convertor/adguard/convertor_test.go +++ b/cmd/sing-box/internal/convertor/adguard/convertor_test.go @@ -1,6 +1,7 @@ package adguard import ( + "context" "strings" "testing" @@ -26,7 +27,7 @@ example.arpa `)) require.NoError(t, err) require.Len(t, rules, 1) - rule, err := rule.NewHeadlessRule(nil, rules[0]) + rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) require.NoError(t, err) matchDomain := []string{ "example.org", @@ -85,7 +86,7 @@ func TestHosts(t *testing.T) { `)) require.NoError(t, err) require.Len(t, rules, 1) - rule, err := rule.NewHeadlessRule(nil, rules[0]) + rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) require.NoError(t, err) matchDomain := []string{ "google.com", @@ -115,7 +116,7 @@ www.example.org `)) require.NoError(t, err) require.Len(t, rules, 1) - rule, err := rule.NewHeadlessRule(nil, rules[0]) + rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) require.NoError(t, err) matchDomain := []string{ "example.com", diff --git a/common/srs/binary.go b/common/srs/binary.go index dd670fc8..fbed78ad 100644 --- a/common/srs/binary.go +++ b/common/srs/binary.go @@ -38,6 +38,9 @@ const ( ruleItemWIFIBSSID ruleItemAdGuardDomain ruleItemProcessPathRegex + ruleItemNetworkType + ruleItemNetworkIsExpensive + ruleItemNetworkIsConstrained ruleItemFinal uint8 = 0xFF ) @@ -222,6 +225,12 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea return } rule.AdGuardDomainMatcher = matcher + case ruleItemNetworkType: + rule.NetworkType, err = readRuleItemString(reader) + case ruleItemNetworkIsExpensive: + rule.NetworkIsExpensive = true + case ruleItemNetworkIsConstrained: + rule.NetworkIsConstrained = true case ruleItemFinal: err = binary.Read(reader, binary.BigEndian, &rule.Invert) return @@ -336,6 +345,27 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen return err } } + if len(rule.NetworkType) > 0 { + if generateVersion < C.RuleSetVersion3 { + return E.New("network_type rule item is only supported in version 3 or later") + } + err = writeRuleItemString(writer, ruleItemNetworkType, rule.NetworkType) + if err != nil { + return err + } + } + if rule.NetworkIsExpensive { + err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsExpensive) + if err != nil { + return err + } + } + if rule.NetworkIsConstrained { + err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsConstrained) + if err != nil { + return err + } + } if len(rule.WIFISSID) > 0 { err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID) if err != nil { diff --git a/constant/rule.go b/constant/rule.go index b1f91c60..c4a77838 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -21,7 +21,8 @@ const ( const ( RuleSetVersion1 = 1 + iota RuleSetVersion2 - RuleSetVersionCurrent = RuleSetVersion2 + RuleSetVersion3 + RuleSetVersionCurrent = RuleSetVersion3 ) const ( diff --git a/option/rule.go b/option/rule.go index d5ff9925..82a53f75 100644 --- a/option/rule.go +++ b/option/rule.go @@ -95,6 +95,9 @@ type RawDefaultRule struct { User badoption.Listable[string] `json:"user,omitempty"` UserID badoption.Listable[int32] `json:"user_id,omitempty"` ClashMode string `json:"clash_mode,omitempty"` + NetworkType badoption.Listable[string] `json:"network_type,omitempty"` + NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` + NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` diff --git a/option/rule_dns.go b/option/rule_dns.go index e758488b..c49d312b 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -97,6 +97,9 @@ type RawDefaultDNSRule struct { UserID badoption.Listable[int32] `json:"user_id,omitempty"` Outbound badoption.Listable[string] `json:"outbound,omitempty"` ClashMode string `json:"clash_mode,omitempty"` + NetworkType badoption.Listable[string] `json:"network_type,omitempty"` + NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` + NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` diff --git a/option/rule_set.go b/option/rule_set.go index f0d96bb6..9ccca475 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -146,25 +146,28 @@ func (r HeadlessRule) IsValid() bool { } type DefaultHeadlessRule struct { - QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` - Network badoption.Listable[string] `json:"network,omitempty"` - Domain badoption.Listable[string] `json:"domain,omitempty"` - DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` - SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` - IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` - SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` - Port badoption.Listable[uint16] `json:"port,omitempty"` - PortRange badoption.Listable[string] `json:"port_range,omitempty"` - ProcessName badoption.Listable[string] `json:"process_name,omitempty"` - ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` - ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` - PackageName badoption.Listable[string] `json:"package_name,omitempty"` - WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` - Invert bool `json:"invert,omitempty"` + QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` + Network badoption.Listable[string] `json:"network,omitempty"` + Domain badoption.Listable[string] `json:"domain,omitempty"` + DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` + SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` + SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` + Port badoption.Listable[uint16] `json:"port,omitempty"` + PortRange badoption.Listable[string] `json:"port_range,omitempty"` + ProcessName badoption.Listable[string] `json:"process_name,omitempty"` + ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` + ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` + PackageName badoption.Listable[string] `json:"package_name,omitempty"` + NetworkType badoption.Listable[string] `json:"network_type,omitempty"` + NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` + NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` + WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` + Invert bool `json:"invert,omitempty"` DomainMatcher *domain.Matcher `json:"-"` SourceIPSet *netipx.IPSet `json:"-"` @@ -191,7 +194,7 @@ func (r LogicalHeadlessRule) IsValid() bool { } type _PlainRuleSetCompat struct { - Version int `json:"version"` + Version uint8 `json:"version"` Options PlainRuleSet `json:"-"` } @@ -200,7 +203,7 @@ type PlainRuleSetCompat _PlainRuleSetCompat func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { var v any switch r.Version { - case C.RuleSetVersion1, C.RuleSetVersion2: + case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3: v = r.Options default: return nil, E.New("unknown rule-set version: ", r.Version) @@ -215,7 +218,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { } var v any switch r.Version { - case C.RuleSetVersion1, C.RuleSetVersion2: + case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3: v = &r.Options case 0: return E.New("missing rule-set version") @@ -231,7 +234,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { func (r PlainRuleSetCompat) Upgrade() (PlainRuleSet, error) { switch r.Version { - case C.RuleSetVersion1, C.RuleSetVersion2: + case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3: default: return PlainRuleSet{}, E.New("unknown rule-set version: " + F.ToString(r.Version)) } diff --git a/route/rule/rule_default.go b/route/rule/rule_default.go index d1076e3d..5c37b415 100644 --- a/route/rule/rule_default.go +++ b/route/rule/rule_default.go @@ -224,6 +224,21 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.NetworkType) > 0 { + item := NewNetworkTypeItem(networkManager, options.NetworkType) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsExpensive { + item := NewNetworkIsExpensiveItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsConstrained { + item := NewNetworkIsConstrainedItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if len(options.WIFISSID) > 0 { item := NewWIFISSIDItem(networkManager, options.WIFISSID) rule.items = append(rule.items, item) diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go index 70e1417a..ecb68ece 100644 --- a/route/rule/rule_dns.go +++ b/route/rule/rule_dns.go @@ -221,6 +221,21 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.NetworkType) > 0 { + item := NewNetworkTypeItem(networkManager, options.NetworkType) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsExpensive { + item := NewNetworkIsExpensiveItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsConstrained { + item := NewNetworkIsConstrainedItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if len(options.WIFISSID) > 0 { item := NewWIFISSIDItem(networkManager, options.WIFISSID) rule.items = append(rule.items, item) diff --git a/route/rule/rule_headless.go b/route/rule/rule_headless.go index 99488b20..7f2dc5fc 100644 --- a/route/rule/rule_headless.go +++ b/route/rule/rule_headless.go @@ -140,18 +140,33 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } - if len(options.WIFISSID) > 0 { - if networkManager != nil { - item := NewWIFISSIDItem(networkManager, options.WIFISSID) + if networkManager != nil { + if len(options.NetworkType) > 0 { + item := NewNetworkTypeItem(networkManager, options.NetworkType) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } - } - if len(options.WIFIBSSID) > 0 { - if networkManager != nil { + if options.NetworkIsExpensive { + item := NewNetworkIsExpensiveItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsConstrained { + item := NewNetworkIsConstrainedItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFISSID) > 0 { + item := NewWIFISSIDItem(networkManager, options.WIFISSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + + } + if len(options.WIFIBSSID) > 0 { item := NewWIFIBSSIDItem(networkManager, options.WIFIBSSID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) + } } if len(options.AdGuardDomain) > 0 { diff --git a/route/rule/rule_item_network_is_constrained.go b/route/rule/rule_item_network_is_constrained.go new file mode 100644 index 00000000..e0368b75 --- /dev/null +++ b/route/rule/rule_item_network_is_constrained.go @@ -0,0 +1,29 @@ +package rule + +import ( + "github.com/sagernet/sing-box/adapter" +) + +var _ RuleItem = (*NetworkIsConstrainedItem)(nil) + +type NetworkIsConstrainedItem struct { + networkManager adapter.NetworkManager +} + +func NewNetworkIsConstrainedItem(networkManager adapter.NetworkManager) *NetworkIsConstrainedItem { + return &NetworkIsConstrainedItem{ + networkManager: networkManager, + } +} + +func (r *NetworkIsConstrainedItem) Match(metadata *adapter.InboundContext) bool { + networkInterface := r.networkManager.DefaultNetworkInterface() + if networkInterface == nil { + return false + } + return networkInterface.Constrained +} + +func (r *NetworkIsConstrainedItem) String() string { + return "network_is_expensive=true" +} diff --git a/route/rule/rule_item_network_is_expensive.go b/route/rule/rule_item_network_is_expensive.go new file mode 100644 index 00000000..83e4f96f --- /dev/null +++ b/route/rule/rule_item_network_is_expensive.go @@ -0,0 +1,29 @@ +package rule + +import ( + "github.com/sagernet/sing-box/adapter" +) + +var _ RuleItem = (*NetworkIsExpensiveItem)(nil) + +type NetworkIsExpensiveItem struct { + networkManager adapter.NetworkManager +} + +func NewNetworkIsExpensiveItem(networkManager adapter.NetworkManager) *NetworkIsExpensiveItem { + return &NetworkIsExpensiveItem{ + networkManager: networkManager, + } +} + +func (r *NetworkIsExpensiveItem) Match(metadata *adapter.InboundContext) bool { + networkInterface := r.networkManager.DefaultNetworkInterface() + if networkInterface == nil { + return false + } + return networkInterface.Expensive +} + +func (r *NetworkIsExpensiveItem) String() string { + return "network_is_expensive=true" +} diff --git a/route/rule/rule_item_network_type.go b/route/rule/rule_item_network_type.go new file mode 100644 index 00000000..8ebdb25e --- /dev/null +++ b/route/rule/rule_item_network_type.go @@ -0,0 +1,39 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*NetworkTypeItem)(nil) + +type NetworkTypeItem struct { + networkManager adapter.NetworkManager + networkType []string +} + +func NewNetworkTypeItem(networkManager adapter.NetworkManager, networkType []string) *NetworkTypeItem { + return &NetworkTypeItem{ + networkManager: networkManager, + networkType: networkType, + } +} + +func (r *NetworkTypeItem) Match(metadata *adapter.InboundContext) bool { + networkInterface := r.networkManager.DefaultNetworkInterface() + if networkInterface == nil { + return false + } + return common.Contains(r.networkType, networkInterface.Type) +} + +func (r *NetworkTypeItem) String() string { + if len(r.networkType) == 1 { + return F.ToString("network_type=", r.networkType[0]) + } else { + return F.ToString("network_type=", "["+strings.Join(F.MapToString(r.networkType), " ")+"]") + } +}