Browse Source

Add interface address rule items

世界 2 months ago
parent
commit
7bd1cc1dd5

+ 18 - 1
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
+}

+ 100 - 1
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 {

+ 33 - 0
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
+}

+ 2 - 1
constant/rule.go

@@ -22,7 +22,8 @@ const (
 	RuleSetVersion1 = 1 + iota
 	RuleSetVersion2
 	RuleSetVersion3
-	RuleSetVersionCurrent = RuleSetVersion3
+	RuleSetVersion4
+	RuleSetVersionCurrent = RuleSetVersion4
 )
 
 const (

+ 39 - 36
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"`

+ 42 - 39
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"`

+ 26 - 23
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)

+ 15 - 0
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 {

+ 56 - 0
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), " ") + "]"
+	}
+}

+ 15 - 0
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 {

+ 10 - 2
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 {

+ 62 - 0
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
+}

+ 64 - 0
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
+}

+ 2 - 2
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
 			}
 		}

+ 3 - 3
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.access.Lock()
 	s.rules = rules
 	s.metadata = metadata

+ 3 - 3
route/rule/rule_set_remote.go

@@ -190,9 +190,9 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
 		}
 	}
 	s.access.Lock()
-	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
 	callbacks := s.callbacks.Array()
 	s.access.Unlock()