From aabc60a7ed1be1f4849f5a4510440975ec7d7860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 14 Aug 2025 10:55:18 +0800 Subject: [PATCH] Add interface address rule items --- cmd/sing-box/cmd_rule_set_compile.go | 19 +++- common/srs/binary.go | 101 ++++++++++++++++++- common/srs/ip_cidr.go | 33 ++++++ constant/rule.go | 3 +- option/rule.go | 75 +++++++------- option/rule_dns.go | 81 ++++++++------- option/rule_set.go | 49 ++++----- route/rule/rule_default.go | 15 +++ route/rule/rule_default_interface_address.go | 56 ++++++++++ route/rule/rule_dns.go | 15 +++ route/rule/rule_headless.go | 12 ++- route/rule/rule_interface_address.go | 62 ++++++++++++ route/rule/rule_network_interface_address.go | 64 ++++++++++++ route/rule/rule_set.go | 4 +- route/rule/rule_set_local.go | 6 +- route/rule/rule_set_remote.go | 6 +- 16 files changed, 490 insertions(+), 111 deletions(-) create mode 100644 common/srs/ip_cidr.go create mode 100644 route/rule/rule_default_interface_address.go create mode 100644 route/rule/rule_interface_address.go create mode 100644 route/rule/rule_network_interface_address.go diff --git a/cmd/sing-box/cmd_rule_set_compile.go b/cmd/sing-box/cmd_rule_set_compile.go index 0c44a2a1..73655b12 100644 --- a/cmd/sing-box/cmd_rule_set_compile.go +++ b/cmd/sing-box/cmd_rule_set_compile.go @@ -6,8 +6,10 @@ import ( "strings" "github.com/sagernet/sing-box/common/srs" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing/common/json" "github.com/spf13/cobra" @@ -69,7 +71,7 @@ func compileRuleSet(sourcePath string) error { if err != nil { return err } - err = srs.Write(outputFile, plainRuleSet.Options, plainRuleSet.Version) + err = srs.Write(outputFile, plainRuleSet.Options, downgradeRuleSetVersion(plainRuleSet.Version, plainRuleSet.Options)) if err != nil { outputFile.Close() os.Remove(outputPath) @@ -78,3 +80,18 @@ func compileRuleSet(sourcePath string) error { outputFile.Close() return nil } + +func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 { + if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool { + return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 || + len(rule.DefaultInterfaceAddress) > 0 + }) { + version = C.RuleSetVersion3 + } + if version == C.RuleSetVersion3 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool { + return len(rule.NetworkType) > 0 || rule.NetworkIsExpensive || rule.NetworkIsConstrained + }) { + version = C.RuleSetVersion2 + } + return version +} diff --git a/common/srs/binary.go b/common/srs/binary.go index d7cda6eb..96b578f5 100644 --- a/common/srs/binary.go +++ b/common/srs/binary.go @@ -12,6 +12,8 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/domain" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" "github.com/sagernet/sing/common/varbin" "go4.org/netipx" @@ -41,6 +43,8 @@ const ( ruleItemNetworkType ruleItemNetworkIsExpensive ruleItemNetworkIsConstrained + ruleItemNetworkInterfaceAddress + ruleItemDefaultInterfaceAddress ruleItemFinal uint8 = 0xFF ) @@ -230,6 +234,51 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea rule.NetworkIsExpensive = true case ruleItemNetworkIsConstrained: rule.NetworkIsConstrained = true + case ruleItemNetworkInterfaceAddress: + rule.NetworkInterfaceAddress = new(badjson.TypedMap[option.InterfaceType, badoption.Listable[badoption.Prefixable]]) + var size uint64 + size, err = binary.ReadUvarint(reader) + if err != nil { + return + } + for i := uint64(0); i < size; i++ { + var key uint8 + err = binary.Read(reader, binary.BigEndian, &key) + if err != nil { + return + } + var value []badoption.Prefixable + var prefixCount uint64 + prefixCount, err = binary.ReadUvarint(reader) + if err != nil { + return + } + for j := uint64(0); j < prefixCount; j++ { + var prefix netip.Prefix + prefix, err = readPrefix(reader) + if err != nil { + return + } + value = append(value, badoption.Prefixable(prefix)) + } + rule.NetworkInterfaceAddress.Put(option.InterfaceType(key), value) + } + case ruleItemDefaultInterfaceAddress: + var value []badoption.Prefixable + var prefixCount uint64 + prefixCount, err = binary.ReadUvarint(reader) + if err != nil { + return + } + for j := uint64(0); j < prefixCount; j++ { + var prefix netip.Prefix + prefix, err = readPrefix(reader) + if err != nil { + return + } + value = append(value, badoption.Prefixable(prefix)) + } + rule.DefaultInterfaceAddress = value case ruleItemFinal: err = binary.Read(reader, binary.BigEndian, &rule.Invert) return @@ -346,7 +395,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen } if len(rule.NetworkType) > 0 { if generateVersion < C.RuleSetVersion3 { - return E.New("network_type rule item is only supported in version 3 or later") + return E.New("`network_type` rule item is only supported in version 3 or later") } err = writeRuleItemUint8(writer, ruleItemNetworkType, rule.NetworkType) if err != nil { @@ -354,17 +403,67 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen } } if rule.NetworkIsExpensive { + if generateVersion < C.RuleSetVersion3 { + return E.New("`network_is_expensive` rule item is only supported in version 3 or later") + } err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsExpensive) if err != nil { return err } } if rule.NetworkIsConstrained { + if generateVersion < C.RuleSetVersion3 { + return E.New("`network_is_constrained` rule item is only supported in version 3 or later") + } err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsConstrained) if err != nil { return err } } + if rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 { + if generateVersion < C.RuleSetVersion4 { + return E.New("`network_interface_address` rule item is only supported in version 4 or later") + } + err = writer.WriteByte(ruleItemNetworkInterfaceAddress) + if err != nil { + return err + } + _, err = varbin.WriteUvarint(writer, uint64(rule.NetworkInterfaceAddress.Size())) + if err != nil { + return err + } + for _, entry := range rule.NetworkInterfaceAddress.Entries() { + err = binary.Write(writer, binary.BigEndian, uint8(entry.Key.Build())) + if err != nil { + return err + } + for _, rawPrefix := range entry.Value { + err = writePrefix(writer, rawPrefix.Build(netip.Prefix{})) + if err != nil { + return err + } + } + } + } + if len(rule.DefaultInterfaceAddress) > 0 { + if generateVersion < C.RuleSetVersion4 { + return E.New("`default_interface_address` rule item is only supported in version 4 or later") + } + err = writer.WriteByte(ruleItemDefaultInterfaceAddress) + if err != nil { + return err + } + _, err = varbin.WriteUvarint(writer, uint64(len(rule.DefaultInterfaceAddress))) + if err != nil { + return err + } + for _, rawPrefix := range rule.DefaultInterfaceAddress { + err = writePrefix(writer, rawPrefix.Build(netip.Prefix{})) + if err != nil { + return err + } + } + } if len(rule.WIFISSID) > 0 { err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID) if err != nil { diff --git a/common/srs/ip_cidr.go b/common/srs/ip_cidr.go new file mode 100644 index 00000000..93ae84ad --- /dev/null +++ b/common/srs/ip_cidr.go @@ -0,0 +1,33 @@ +package srs + +import ( + "encoding/binary" + "net/netip" + + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/varbin" +) + +func readPrefix(reader varbin.Reader) (netip.Prefix, error) { + addrSlice, err := varbin.ReadValue[[]byte](reader, binary.BigEndian) + if err != nil { + return netip.Prefix{}, err + } + prefixBits, err := varbin.ReadValue[uint8](reader, binary.BigEndian) + if err != nil { + return netip.Prefix{}, err + } + return netip.PrefixFrom(M.AddrFromIP(addrSlice), int(prefixBits)), nil +} + +func writePrefix(writer varbin.Writer, prefix netip.Prefix) error { + err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice()) + if err != nil { + return err + } + err = binary.Write(writer, binary.BigEndian, uint8(prefix.Bits())) + if err != nil { + return err + } + return nil +} diff --git a/constant/rule.go b/constant/rule.go index 336c3b38..71441450 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -22,7 +22,8 @@ const ( RuleSetVersion1 = 1 + iota RuleSetVersion2 RuleSetVersion3 - RuleSetVersionCurrent = RuleSetVersion3 + RuleSetVersion4 + RuleSetVersionCurrent = RuleSetVersion4 ) const ( diff --git a/option/rule.go b/option/rule.go index 41bcc126..d12b679d 100644 --- a/option/rule.go +++ b/option/rule.go @@ -67,42 +67,45 @@ func (r Rule) IsValid() bool { } type RawDefaultRule struct { - Inbound badoption.Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - Network badoption.Listable[string] `json:"network,omitempty"` - AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` - Protocol badoption.Listable[string] `json:"protocol,omitempty"` - Client badoption.Listable[string] `json:"client,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"` - Geosite badoption.Listable[string] `json:"geosite,omitempty"` - SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` - GeoIP badoption.Listable[string] `json:"geoip,omitempty"` - SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` - SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` - IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` - IPIsPrivate bool `json:"ip_is_private,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"` - User badoption.Listable[string] `json:"user,omitempty"` - UserID badoption.Listable[int32] `json:"user_id,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - NetworkType badoption.Listable[InterfaceType] `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"` - RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` - Invert bool `json:"invert,omitempty"` + Inbound badoption.Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + Network badoption.Listable[string] `json:"network,omitempty"` + AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` + Protocol badoption.Listable[string] `json:"protocol,omitempty"` + Client badoption.Listable[string] `json:"client,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"` + Geosite badoption.Listable[string] `json:"geosite,omitempty"` + SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` + GeoIP badoption.Listable[string] `json:"geoip,omitempty"` + SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` + IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,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"` + User badoption.Listable[string] `json:"user,omitempty"` + UserID badoption.Listable[int32] `json:"user_id,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + NetworkType badoption.Listable[InterfaceType] `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"` + InterfaceAddress *badjson.TypedMap[string, badoption.Listable[badoption.Prefixable]] `json:"interface_address,omitempty"` + NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[badoption.Prefixable]] `json:"network_interface_address,omitempty"` + DefaultInterfaceAddress badoption.Listable[badoption.Prefixable] `json:"default_interface_address,omitempty"` + RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` + Invert bool `json:"invert,omitempty"` // Deprecated: renamed to rule_set_ip_cidr_match_source Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` diff --git a/option/rule_dns.go b/option/rule_dns.go index 87b15017..bbab993c 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -68,45 +68,48 @@ func (r DNSRule) IsValid() bool { } type RawDefaultDNSRule struct { - Inbound badoption.Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` - Network badoption.Listable[string] `json:"network,omitempty"` - AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` - Protocol badoption.Listable[string] `json:"protocol,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"` - Geosite badoption.Listable[string] `json:"geosite,omitempty"` - SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` - GeoIP badoption.Listable[string] `json:"geoip,omitempty"` - IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` - IPIsPrivate bool `json:"ip_is_private,omitempty"` - IPAcceptAny bool `json:"ip_accept_any,omitempty"` - SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` - SourceIPIsPrivate bool `json:"source_ip_is_private,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"` - User badoption.Listable[string] `json:"user,omitempty"` - UserID badoption.Listable[int32] `json:"user_id,omitempty"` - Outbound badoption.Listable[string] `json:"outbound,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - NetworkType badoption.Listable[InterfaceType] `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"` - RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` - RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` - Invert bool `json:"invert,omitempty"` + Inbound badoption.Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` + Network badoption.Listable[string] `json:"network,omitempty"` + AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` + Protocol badoption.Listable[string] `json:"protocol,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"` + Geosite badoption.Listable[string] `json:"geosite,omitempty"` + SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` + GeoIP badoption.Listable[string] `json:"geoip,omitempty"` + IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,omitempty"` + IPAcceptAny bool `json:"ip_accept_any,omitempty"` + SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,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"` + User badoption.Listable[string] `json:"user,omitempty"` + UserID badoption.Listable[int32] `json:"user_id,omitempty"` + Outbound badoption.Listable[string] `json:"outbound,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + NetworkType badoption.Listable[InterfaceType] `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"` + InterfaceAddress *badjson.TypedMap[string, badoption.Listable[badoption.Prefixable]] `json:"interface_address,omitempty"` + NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[badoption.Prefixable]] `json:"network_interface_address,omitempty"` + DefaultInterfaceAddress badoption.Listable[badoption.Prefixable] `json:"default_interface_address,omitempty"` + RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` + RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` + Invert bool `json:"invert,omitempty"` // Deprecated: renamed to rule_set_ip_cidr_match_source Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` diff --git a/option/rule_set.go b/option/rule_set.go index 610d7ba2..2775d743 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -182,28 +182,31 @@ 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"` - NetworkType badoption.Listable[InterfaceType] `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"` + 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[InterfaceType] `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"` + NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[badoption.Prefixable]] `json:"network_interface_address,omitempty"` + DefaultInterfaceAddress badoption.Listable[badoption.Prefixable] `json:"default_interface_address,omitempty"` + + Invert bool `json:"invert,omitempty"` DomainMatcher *domain.Matcher `json:"-"` SourceIPSet *netipx.IPSet `json:"-"` @@ -240,7 +243,7 @@ type PlainRuleSetCompat _PlainRuleSetCompat func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { var v any switch r.Version { - case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3: + case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4: v = r.Options default: return nil, E.New("unknown rule-set version: ", r.Version) diff --git a/route/rule/rule_default.go b/route/rule/rule_default.go index 08e21e5f..e0677b97 100644 --- a/route/rule/rule_default.go +++ b/route/rule/rule_default.go @@ -246,6 +246,21 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if options.InterfaceAddress != nil && options.InterfaceAddress.Size() > 0 { + item := NewInterfaceAddressItem(networkManager, options.InterfaceAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkInterfaceAddress != nil && options.NetworkInterfaceAddress.Size() > 0 { + item := NewNetworkInterfaceAddressItem(networkManager, options.NetworkInterfaceAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DefaultInterfaceAddress) > 0 { + item := NewDefaultInterfaceAddressItem(networkManager, options.DefaultInterfaceAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if len(options.RuleSet) > 0 { var matchSource bool if options.RuleSetIPCIDRMatchSource { diff --git a/route/rule/rule_default_interface_address.go b/route/rule/rule_default_interface_address.go new file mode 100644 index 00000000..940d3a06 --- /dev/null +++ b/route/rule/rule_default_interface_address.go @@ -0,0 +1,56 @@ +package rule + +import ( + "net/netip" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" +) + +var _ RuleItem = (*DefaultInterfaceAddressItem)(nil) + +type DefaultInterfaceAddressItem struct { + interfaceMonitor tun.DefaultInterfaceMonitor + interfaceAddresses []netip.Prefix +} + +func NewDefaultInterfaceAddressItem(networkManager adapter.NetworkManager, interfaceAddresses badoption.Listable[badoption.Prefixable]) *DefaultInterfaceAddressItem { + item := &DefaultInterfaceAddressItem{ + interfaceMonitor: networkManager.InterfaceMonitor(), + interfaceAddresses: make([]netip.Prefix, 0, len(interfaceAddresses)), + } + for _, prefixable := range interfaceAddresses { + item.interfaceAddresses = append(item.interfaceAddresses, prefixable.Build(netip.Prefix{})) + } + return item +} + +func (r *DefaultInterfaceAddressItem) Match(metadata *adapter.InboundContext) bool { + defaultInterface := r.interfaceMonitor.DefaultInterface() + if defaultInterface == nil { + return false + } + for _, address := range r.interfaceAddresses { + if common.All(defaultInterface.Addresses, func(it netip.Prefix) bool { + return !address.Overlaps(it) + }) { + return false + } + } + return true +} + +func (r *DefaultInterfaceAddressItem) String() string { + addressLen := len(r.interfaceAddresses) + switch { + case addressLen == 1: + return "default_interface_address=" + r.interfaceAddresses[0].String() + case addressLen > 3: + return "default_interface_address=[" + strings.Join(common.Map(r.interfaceAddresses[:3], netip.Prefix.String), " ") + "...]" + default: + return "default_interface_address=[" + strings.Join(common.Map(r.interfaceAddresses, netip.Prefix.String), " ") + "]" + } +} diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go index 30442abf..d9570cae 100644 --- a/route/rule/rule_dns.go +++ b/route/rule/rule_dns.go @@ -247,6 +247,21 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if options.InterfaceAddress != nil && options.InterfaceAddress.Size() > 0 { + item := NewInterfaceAddressItem(networkManager, options.InterfaceAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkInterfaceAddress != nil && options.NetworkInterfaceAddress.Size() > 0 { + item := NewNetworkInterfaceAddressItem(networkManager, options.NetworkInterfaceAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DefaultInterfaceAddress) > 0 { + item := NewDefaultInterfaceAddressItem(networkManager, options.DefaultInterfaceAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if len(options.RuleSet) > 0 { var matchSource bool if options.RuleSetIPCIDRMatchSource { diff --git a/route/rule/rule_headless.go b/route/rule/rule_headless.go index ba17ca37..689e6e3e 100644 --- a/route/rule/rule_headless.go +++ b/route/rule/rule_headless.go @@ -164,13 +164,21 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR 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 options.NetworkInterfaceAddress != nil && options.NetworkInterfaceAddress.Size() > 0 { + item := NewNetworkInterfaceAddressItem(networkManager, options.NetworkInterfaceAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.DefaultInterfaceAddress) > 0 { + item := NewDefaultInterfaceAddressItem(networkManager, options.DefaultInterfaceAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) } } if len(options.AdGuardDomain) > 0 { diff --git a/route/rule/rule_interface_address.go b/route/rule/rule_interface_address.go new file mode 100644 index 00000000..53ece683 --- /dev/null +++ b/route/rule/rule_interface_address.go @@ -0,0 +1,62 @@ +package rule + +import ( + "net/netip" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/control" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" +) + +var _ RuleItem = (*InterfaceAddressItem)(nil) + +type InterfaceAddressItem struct { + networkManager adapter.NetworkManager + interfaceAddresses map[string][]netip.Prefix + description string +} + +func NewInterfaceAddressItem(networkManager adapter.NetworkManager, interfaceAddresses *badjson.TypedMap[string, badoption.Listable[badoption.Prefixable]]) *InterfaceAddressItem { + item := &InterfaceAddressItem{ + networkManager: networkManager, + interfaceAddresses: make(map[string][]netip.Prefix, interfaceAddresses.Size()), + } + var entryDescriptions []string + for _, entry := range interfaceAddresses.Entries() { + prefixes := make([]netip.Prefix, 0, len(entry.Value)) + for _, prefixable := range entry.Value { + prefixes = append(prefixes, prefixable.Build(netip.Prefix{})) + } + item.interfaceAddresses[entry.Key] = prefixes + entryDescriptions = append(entryDescriptions, entry.Key+"="+strings.Join(common.Map(prefixes, netip.Prefix.String), ",")) + } + item.description = "interface_address=[" + strings.Join(entryDescriptions, " ") + "]" + return item +} + +func (r *InterfaceAddressItem) Match(metadata *adapter.InboundContext) bool { + interfaces := r.networkManager.InterfaceFinder().Interfaces() + for ifName, addresses := range r.interfaceAddresses { + iface := common.Find(interfaces, func(it control.Interface) bool { + return it.Name == ifName + }) + if iface.Name == "" { + return false + } + if common.All(addresses, func(address netip.Prefix) bool { + return common.All(iface.Addresses, func(it netip.Prefix) bool { + return !address.Overlaps(it) + }) + }) { + return false + } + } + return true +} + +func (r *InterfaceAddressItem) String() string { + return r.description +} diff --git a/route/rule/rule_network_interface_address.go b/route/rule/rule_network_interface_address.go new file mode 100644 index 00000000..c365be3b --- /dev/null +++ b/route/rule/rule_network_interface_address.go @@ -0,0 +1,64 @@ +package rule + +import ( + "net/netip" + "strings" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" +) + +var _ RuleItem = (*NetworkInterfaceAddressItem)(nil) + +type NetworkInterfaceAddressItem struct { + networkManager adapter.NetworkManager + interfaceAddresses map[C.InterfaceType][]netip.Prefix + description string +} + +func NewNetworkInterfaceAddressItem(networkManager adapter.NetworkManager, interfaceAddresses *badjson.TypedMap[option.InterfaceType, badoption.Listable[badoption.Prefixable]]) *NetworkInterfaceAddressItem { + item := &NetworkInterfaceAddressItem{ + networkManager: networkManager, + interfaceAddresses: make(map[C.InterfaceType][]netip.Prefix, interfaceAddresses.Size()), + } + var entryDescriptions []string + for _, entry := range interfaceAddresses.Entries() { + prefixes := make([]netip.Prefix, 0, len(entry.Value)) + for _, prefixable := range entry.Value { + prefixes = append(prefixes, prefixable.Build(netip.Prefix{})) + } + item.interfaceAddresses[entry.Key.Build()] = prefixes + entryDescriptions = append(entryDescriptions, entry.Key.Build().String()+"="+strings.Join(common.Map(prefixes, netip.Prefix.String), ",")) + } + item.description = "network_interface_address=[" + strings.Join(entryDescriptions, " ") + "]" + return item +} + +func (r *NetworkInterfaceAddressItem) Match(metadata *adapter.InboundContext) bool { + interfaces := r.networkManager.NetworkInterfaces() +match: + for ifType, addresses := range r.interfaceAddresses { + for _, networkInterface := range interfaces { + if networkInterface.Type != ifType { + continue + } + if common.Any(networkInterface.Addresses, func(it netip.Prefix) bool { + return common.Any(addresses, func(prefix netip.Prefix) bool { + return prefix.Overlaps(it) + }) + }) { + continue match + } + } + return false + } + return true +} + +func (r *NetworkInterfaceAddressItem) String() string { + return r.description +} diff --git a/route/rule/rule_set.go b/route/rule/rule_set.go index 5e639a47..39068dbf 100644 --- a/route/rule/rule_set.go +++ b/route/rule/rule_set.go @@ -42,7 +42,7 @@ func extractIPSetFromRule(rawRule adapter.HeadlessRule) []*netipx.IPSet { } } -func hasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool { +func HasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool { for _, rule := range rules { switch rule.Type { case C.RuleTypeDefault: @@ -50,7 +50,7 @@ func hasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultH return true } case C.RuleTypeLogical: - if hasHeadlessRule(rule.LogicalOptions.Rules, cond) { + if HasHeadlessRule(rule.LogicalOptions.Rules, cond) { return true } } diff --git a/route/rule/rule_set_local.go b/route/rule/rule_set_local.go index 4f2fabcc..442302e2 100644 --- a/route/rule/rule_set_local.go +++ b/route/rule/rule_set_local.go @@ -138,9 +138,9 @@ func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error { } } var metadata adapter.RuleSetMetadata - metadata.ContainsProcessRule = hasHeadlessRule(headlessRules, isProcessHeadlessRule) - metadata.ContainsWIFIRule = hasHeadlessRule(headlessRules, isWIFIHeadlessRule) - metadata.ContainsIPCIDRRule = hasHeadlessRule(headlessRules, isIPCIDRHeadlessRule) + metadata.ContainsProcessRule = HasHeadlessRule(headlessRules, isProcessHeadlessRule) + metadata.ContainsWIFIRule = HasHeadlessRule(headlessRules, isWIFIHeadlessRule) + metadata.ContainsIPCIDRRule = HasHeadlessRule(headlessRules, isIPCIDRHeadlessRule) s.rules = rules s.metadata = metadata s.callbackAccess.Lock() diff --git a/route/rule/rule_set_remote.go b/route/rule/rule_set_remote.go index 16a95bb6..f31ec143 100644 --- a/route/rule/rule_set_remote.go +++ b/route/rule/rule_set_remote.go @@ -185,9 +185,9 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error { return E.Cause(err, "parse rule_set.rules.[", i, "]") } } - s.metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) - s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) - s.metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) + s.metadata.ContainsProcessRule = HasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) + s.metadata.ContainsWIFIRule = HasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) + s.metadata.ContainsIPCIDRRule = HasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) s.rules = rules s.callbackAccess.Lock() callbacks := s.callbacks.Array()