Explorar el Código

Refactor rules

世界 hace 2 años
padre
commit
aa94cfb876

+ 1 - 1
adapter/inbound.go

@@ -27,7 +27,7 @@ type InjectableInbound interface {
 type InboundContext struct {
 	Inbound     string
 	InboundType string
-	IPVersion   int
+	IPVersion   uint8
 	Network     string
 	Source      M.Socksaddr
 	Destination M.Socksaddr

+ 1 - 0
adapter/router.go

@@ -77,6 +77,7 @@ type Rule interface {
 type DNSRule interface {
 	Rule
 	DisableCache() bool
+	RewriteTTL() *uint32
 }
 
 type InterfaceUpdateListener interface {

+ 2 - 2
common/badjsonmerge/merge_test.go

@@ -21,7 +21,7 @@ func TestMergeJSON(t *testing.T) {
 				{
 					Type: C.RuleTypeDefault,
 					DefaultOptions: option.DefaultRule{
-						Network:  N.NetworkTCP,
+						Network:  []string{N.NetworkTCP},
 						Outbound: "direct",
 					},
 				},
@@ -42,7 +42,7 @@ func TestMergeJSON(t *testing.T) {
 				{
 					Type: C.RuleTypeDefault,
 					DefaultOptions: option.DefaultRule{
-						Network:  N.NetworkUDP,
+						Network:  []string{N.NetworkUDP},
 						Outbound: "direct",
 					},
 				},

+ 6 - 1
common/dialer/tfo.go

@@ -27,7 +27,12 @@ type slowOpenConn struct {
 
 func DialSlowContext(dialer *tfo.Dialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
 	if dialer.DisableTFO || N.NetworkName(network) != N.NetworkTCP {
-		return dialer.DialContext(ctx, network, destination.String(), nil)
+		switch N.NetworkName(network) {
+		case N.NetworkTCP, N.NetworkUDP:
+			return dialer.Dialer.DialContext(ctx, network, destination.String())
+		default:
+			return dialer.Dialer.DialContext(ctx, network, destination.AddrString())
+		}
 	}
 	return &slowOpenConn{
 		dialer:      dialer,

+ 6 - 10
docs/configuration/route/rule.md

@@ -9,7 +9,9 @@
           "mixed-in"
         ],
         "ip_version": 6,
-        "network": "tcp",
+        "network": [
+          "tcp"
+        ],
         "auth_user": [
           "usera",
           "userb"
@@ -244,18 +246,12 @@ Tag of the target outbound.
 
 #### mode
 
+==Required==
+
 `and` or `or`
 
 #### rules
 
-Included default rules.
-
-#### invert
-
-Invert match result.
-
-#### outbound
-
 ==Required==
 
-Tag of the target outbound.
+Included default rules.

+ 6 - 10
docs/configuration/route/rule.zh.md

@@ -9,7 +9,9 @@
           "mixed-in"
         ],
         "ip_version": 6,
-        "network": "tcp",
+        "network": [
+          "tcp"
+        ],
         "auth_user": [
           "usera",
           "userb"
@@ -242,18 +244,12 @@
 
 #### mode
 
+==必填==
+
 `and` 或 `or`
 
 #### rules
 
-包括的默认规则。
-
-#### invert
-
-反选匹配结果。
-
-#### outbound
-
 ==必填==
 
-目标出站的标签
+包括的默认规则。

+ 8 - 5
inbound/tun.go

@@ -38,10 +38,6 @@ type Tun struct {
 }
 
 func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*Tun, error) {
-	tunName := options.InterfaceName
-	if tunName == "" {
-		tunName = tun.CalculateInterfaceName("")
-	}
 	tunMTU := options.MTU
 	if tunMTU == 0 {
 		tunMTU = 9000
@@ -75,7 +71,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
 		logger:         logger,
 		inboundOptions: options.InboundOptions,
 		tunOptions: tun.Options{
-			Name:               tunName,
+			Name:               options.InterfaceName,
 			MTU:                tunMTU,
 			Inet4Address:       common.Map(options.Inet4Address, option.ListenPrefix.Build),
 			Inet6Address:       common.Map(options.Inet6Address, option.ListenPrefix.Build),
@@ -141,12 +137,17 @@ func (t *Tun) Tag() string {
 
 func (t *Tun) Start() error {
 	if C.IsAndroid && t.platformInterface == nil {
+		t.logger.Trace("building android rules")
 		t.tunOptions.BuildAndroidRules(t.router.PackageManager(), t)
 	}
+	if t.tunOptions.Name == "" {
+		t.tunOptions.Name = tun.CalculateInterfaceName("")
+	}
 	var (
 		tunInterface tun.Tun
 		err          error
 	)
+	t.logger.Trace("opening interface")
 	if t.platformInterface != nil {
 		tunInterface, err = t.platformInterface.OpenTun(&t.tunOptions, t.platformOptions)
 	} else {
@@ -155,6 +156,7 @@ func (t *Tun) Start() error {
 	if err != nil {
 		return E.Cause(err, "configure tun interface")
 	}
+	t.logger.Trace("creating stack")
 	t.tunIf = tunInterface
 	t.tunStack, err = tun.NewStack(t.stack, tun.StackOptions{
 		Context:                t.ctx,
@@ -172,6 +174,7 @@ func (t *Tun) Start() error {
 	if err != nil {
 		return err
 	}
+	t.logger.Trace("starting stack")
 	err = t.tunStack.Start()
 	if err != nil {
 		return err

+ 0 - 103
option/dns.go

@@ -1,14 +1,5 @@
 package option
 
-import (
-	"reflect"
-
-	"github.com/sagernet/sing-box/common/json"
-	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing/common"
-	E "github.com/sagernet/sing/common/exceptions"
-)
-
 type DNSOptions struct {
 	Servers        []DNSServerOptions `json:"servers,omitempty"`
 	Rules          []DNSRule          `json:"rules,omitempty"`
@@ -32,97 +23,3 @@ type DNSServerOptions struct {
 	Strategy             DomainStrategy `json:"strategy,omitempty"`
 	Detour               string         `json:"detour,omitempty"`
 }
-
-type _DNSRule struct {
-	Type           string         `json:"type,omitempty"`
-	DefaultOptions DefaultDNSRule `json:"-"`
-	LogicalOptions LogicalDNSRule `json:"-"`
-}
-
-type DNSRule _DNSRule
-
-func (r DNSRule) MarshalJSON() ([]byte, error) {
-	var v any
-	switch r.Type {
-	case C.RuleTypeDefault:
-		r.Type = ""
-		v = r.DefaultOptions
-	case C.RuleTypeLogical:
-		v = r.LogicalOptions
-	default:
-		return nil, E.New("unknown rule type: " + r.Type)
-	}
-	return MarshallObjects((_DNSRule)(r), v)
-}
-
-func (r *DNSRule) UnmarshalJSON(bytes []byte) error {
-	err := json.Unmarshal(bytes, (*_DNSRule)(r))
-	if err != nil {
-		return err
-	}
-	var v any
-	switch r.Type {
-	case "", C.RuleTypeDefault:
-		r.Type = C.RuleTypeDefault
-		v = &r.DefaultOptions
-	case C.RuleTypeLogical:
-		v = &r.LogicalOptions
-	default:
-		return E.New("unknown rule type: " + r.Type)
-	}
-	err = UnmarshallExcluded(bytes, (*_DNSRule)(r), v)
-	if err != nil {
-		return E.Cause(err, "dns route rule")
-	}
-	return nil
-}
-
-type DefaultDNSRule struct {
-	Inbound         Listable[string]       `json:"inbound,omitempty"`
-	IPVersion       int                    `json:"ip_version,omitempty"`
-	QueryType       Listable[DNSQueryType] `json:"query_type,omitempty"`
-	Network         string                 `json:"network,omitempty"`
-	AuthUser        Listable[string]       `json:"auth_user,omitempty"`
-	Protocol        Listable[string]       `json:"protocol,omitempty"`
-	Domain          Listable[string]       `json:"domain,omitempty"`
-	DomainSuffix    Listable[string]       `json:"domain_suffix,omitempty"`
-	DomainKeyword   Listable[string]       `json:"domain_keyword,omitempty"`
-	DomainRegex     Listable[string]       `json:"domain_regex,omitempty"`
-	Geosite         Listable[string]       `json:"geosite,omitempty"`
-	SourceGeoIP     Listable[string]       `json:"source_geoip,omitempty"`
-	SourceIPCIDR    Listable[string]       `json:"source_ip_cidr,omitempty"`
-	SourcePort      Listable[uint16]       `json:"source_port,omitempty"`
-	SourcePortRange Listable[string]       `json:"source_port_range,omitempty"`
-	Port            Listable[uint16]       `json:"port,omitempty"`
-	PortRange       Listable[string]       `json:"port_range,omitempty"`
-	ProcessName     Listable[string]       `json:"process_name,omitempty"`
-	ProcessPath     Listable[string]       `json:"process_path,omitempty"`
-	PackageName     Listable[string]       `json:"package_name,omitempty"`
-	User            Listable[string]       `json:"user,omitempty"`
-	UserID          Listable[int32]        `json:"user_id,omitempty"`
-	Outbound        Listable[string]       `json:"outbound,omitempty"`
-	ClashMode       string                 `json:"clash_mode,omitempty"`
-	Invert          bool                   `json:"invert,omitempty"`
-	Server          string                 `json:"server,omitempty"`
-	DisableCache    bool                   `json:"disable_cache,omitempty"`
-}
-
-func (r DefaultDNSRule) IsValid() bool {
-	var defaultValue DefaultDNSRule
-	defaultValue.Invert = r.Invert
-	defaultValue.Server = r.Server
-	defaultValue.DisableCache = r.DisableCache
-	return !reflect.DeepEqual(r, defaultValue)
-}
-
-type LogicalDNSRule struct {
-	Mode         string           `json:"mode"`
-	Rules        []DefaultDNSRule `json:"rules,omitempty"`
-	Invert       bool             `json:"invert,omitempty"`
-	Server       string           `json:"server,omitempty"`
-	DisableCache bool             `json:"disable_cache,omitempty"`
-}
-
-func (r LogicalDNSRule) IsValid() bool {
-	return len(r.Rules) > 0 && common.All(r.Rules, DefaultDNSRule.IsValid)
-}

+ 0 - 100
option/route.go

@@ -1,14 +1,5 @@
 package option
 
-import (
-	"reflect"
-
-	"github.com/sagernet/sing-box/common/json"
-	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing/common"
-	E "github.com/sagernet/sing/common/exceptions"
-)
-
 type RouteOptions struct {
 	GeoIP               *GeoIPOptions   `json:"geoip,omitempty"`
 	Geosite             *GeositeOptions `json:"geosite,omitempty"`
@@ -32,94 +23,3 @@ type GeositeOptions struct {
 	DownloadURL    string `json:"download_url,omitempty"`
 	DownloadDetour string `json:"download_detour,omitempty"`
 }
-
-type _Rule struct {
-	Type           string      `json:"type,omitempty"`
-	DefaultOptions DefaultRule `json:"-"`
-	LogicalOptions LogicalRule `json:"-"`
-}
-
-type Rule _Rule
-
-func (r Rule) MarshalJSON() ([]byte, error) {
-	var v any
-	switch r.Type {
-	case C.RuleTypeDefault:
-		r.Type = ""
-		v = r.DefaultOptions
-	case C.RuleTypeLogical:
-		v = r.LogicalOptions
-	default:
-		return nil, E.New("unknown rule type: " + r.Type)
-	}
-	return MarshallObjects((_Rule)(r), v)
-}
-
-func (r *Rule) UnmarshalJSON(bytes []byte) error {
-	err := json.Unmarshal(bytes, (*_Rule)(r))
-	if err != nil {
-		return err
-	}
-	var v any
-	switch r.Type {
-	case "", C.RuleTypeDefault:
-		r.Type = C.RuleTypeDefault
-		v = &r.DefaultOptions
-	case C.RuleTypeLogical:
-		v = &r.LogicalOptions
-	default:
-		return E.New("unknown rule type: " + r.Type)
-	}
-	err = UnmarshallExcluded(bytes, (*_Rule)(r), v)
-	if err != nil {
-		return E.Cause(err, "route rule")
-	}
-	return nil
-}
-
-type DefaultRule struct {
-	Inbound         Listable[string] `json:"inbound,omitempty"`
-	IPVersion       int              `json:"ip_version,omitempty"`
-	Network         string           `json:"network,omitempty"`
-	AuthUser        Listable[string] `json:"auth_user,omitempty"`
-	Protocol        Listable[string] `json:"protocol,omitempty"`
-	Domain          Listable[string] `json:"domain,omitempty"`
-	DomainSuffix    Listable[string] `json:"domain_suffix,omitempty"`
-	DomainKeyword   Listable[string] `json:"domain_keyword,omitempty"`
-	DomainRegex     Listable[string] `json:"domain_regex,omitempty"`
-	Geosite         Listable[string] `json:"geosite,omitempty"`
-	SourceGeoIP     Listable[string] `json:"source_geoip,omitempty"`
-	GeoIP           Listable[string] `json:"geoip,omitempty"`
-	SourceIPCIDR    Listable[string] `json:"source_ip_cidr,omitempty"`
-	IPCIDR          Listable[string] `json:"ip_cidr,omitempty"`
-	SourcePort      Listable[uint16] `json:"source_port,omitempty"`
-	SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
-	Port            Listable[uint16] `json:"port,omitempty"`
-	PortRange       Listable[string] `json:"port_range,omitempty"`
-	ProcessName     Listable[string] `json:"process_name,omitempty"`
-	ProcessPath     Listable[string] `json:"process_path,omitempty"`
-	PackageName     Listable[string] `json:"package_name,omitempty"`
-	User            Listable[string] `json:"user,omitempty"`
-	UserID          Listable[int32]  `json:"user_id,omitempty"`
-	ClashMode       string           `json:"clash_mode,omitempty"`
-	Invert          bool             `json:"invert,omitempty"`
-	Outbound        string           `json:"outbound,omitempty"`
-}
-
-func (r DefaultRule) IsValid() bool {
-	var defaultValue DefaultRule
-	defaultValue.Invert = r.Invert
-	defaultValue.Outbound = r.Outbound
-	return !reflect.DeepEqual(r, defaultValue)
-}
-
-type LogicalRule struct {
-	Mode     string        `json:"mode"`
-	Rules    []DefaultRule `json:"rules,omitempty"`
-	Invert   bool          `json:"invert,omitempty"`
-	Outbound string        `json:"outbound,omitempty"`
-}
-
-func (r LogicalRule) IsValid() bool {
-	return len(r.Rules) > 0 && common.All(r.Rules, DefaultRule.IsValid)
-}

+ 101 - 0
option/rule.go

@@ -0,0 +1,101 @@
+package option
+
+import (
+	"reflect"
+
+	"github.com/sagernet/sing-box/common/json"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing/common"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+type _Rule struct {
+	Type           string      `json:"type,omitempty"`
+	DefaultOptions DefaultRule `json:"-"`
+	LogicalOptions LogicalRule `json:"-"`
+}
+
+type Rule _Rule
+
+func (r Rule) MarshalJSON() ([]byte, error) {
+	var v any
+	switch r.Type {
+	case C.RuleTypeDefault:
+		r.Type = ""
+		v = r.DefaultOptions
+	case C.RuleTypeLogical:
+		v = r.LogicalOptions
+	default:
+		return nil, E.New("unknown rule type: " + r.Type)
+	}
+	return MarshallObjects((_Rule)(r), v)
+}
+
+func (r *Rule) UnmarshalJSON(bytes []byte) error {
+	err := json.Unmarshal(bytes, (*_Rule)(r))
+	if err != nil {
+		return err
+	}
+	var v any
+	switch r.Type {
+	case "", C.RuleTypeDefault:
+		r.Type = C.RuleTypeDefault
+		v = &r.DefaultOptions
+	case C.RuleTypeLogical:
+		v = &r.LogicalOptions
+	default:
+		return E.New("unknown rule type: " + r.Type)
+	}
+	err = UnmarshallExcluded(bytes, (*_Rule)(r), v)
+	if err != nil {
+		return E.Cause(err, "route rule")
+	}
+	return nil
+}
+
+type DefaultRule struct {
+	Inbound         Listable[string] `json:"inbound,omitempty"`
+	IPVersion       int              `json:"ip_version,omitempty"`
+	Network         Listable[string] `json:"network,omitempty"`
+	AuthUser        Listable[string] `json:"auth_user,omitempty"`
+	Protocol        Listable[string] `json:"protocol,omitempty"`
+	Domain          Listable[string] `json:"domain,omitempty"`
+	DomainSuffix    Listable[string] `json:"domain_suffix,omitempty"`
+	DomainKeyword   Listable[string] `json:"domain_keyword,omitempty"`
+	DomainRegex     Listable[string] `json:"domain_regex,omitempty"`
+	Geosite         Listable[string] `json:"geosite,omitempty"`
+	SourceGeoIP     Listable[string] `json:"source_geoip,omitempty"`
+	GeoIP           Listable[string] `json:"geoip,omitempty"`
+	SourceIPCIDR    Listable[string] `json:"source_ip_cidr,omitempty"`
+	IPCIDR          Listable[string] `json:"ip_cidr,omitempty"`
+	SourcePort      Listable[uint16] `json:"source_port,omitempty"`
+	SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
+	Port            Listable[uint16] `json:"port,omitempty"`
+	PortRange       Listable[string] `json:"port_range,omitempty"`
+	ProcessName     Listable[string] `json:"process_name,omitempty"`
+	ProcessPath     Listable[string] `json:"process_path,omitempty"`
+	PackageName     Listable[string] `json:"package_name,omitempty"`
+	User            Listable[string] `json:"user,omitempty"`
+	UserID          Listable[int32]  `json:"user_id,omitempty"`
+	ClashMode       string           `json:"clash_mode,omitempty"`
+	Invert          bool             `json:"invert,omitempty"`
+	Outbound        string           `json:"outbound,omitempty"`
+}
+
+func (r DefaultRule) IsValid() bool {
+	var defaultValue DefaultRule
+	defaultValue.Invert = r.Invert
+	defaultValue.Outbound = r.Outbound
+	return !reflect.DeepEqual(r, defaultValue)
+}
+
+type LogicalRule struct {
+	Mode     string        `json:"mode"`
+	Rules    []DefaultRule `json:"rules,omitempty"`
+	Invert   bool          `json:"invert,omitempty"`
+	Outbound string        `json:"outbound,omitempty"`
+}
+
+func (r LogicalRule) IsValid() bool {
+	return len(r.Rules) > 0 && common.All(r.Rules, DefaultRule.IsValid)
+}

+ 107 - 0
option/rule_dns.go

@@ -0,0 +1,107 @@
+package option
+
+import (
+	"reflect"
+
+	"github.com/sagernet/sing-box/common/json"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing/common"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+type _DNSRule struct {
+	Type           string         `json:"type,omitempty"`
+	DefaultOptions DefaultDNSRule `json:"-"`
+	LogicalOptions LogicalDNSRule `json:"-"`
+}
+
+type DNSRule _DNSRule
+
+func (r DNSRule) MarshalJSON() ([]byte, error) {
+	var v any
+	switch r.Type {
+	case C.RuleTypeDefault:
+		r.Type = ""
+		v = r.DefaultOptions
+	case C.RuleTypeLogical:
+		v = r.LogicalOptions
+	default:
+		return nil, E.New("unknown rule type: " + r.Type)
+	}
+	return MarshallObjects((_DNSRule)(r), v)
+}
+
+func (r *DNSRule) UnmarshalJSON(bytes []byte) error {
+	err := json.Unmarshal(bytes, (*_DNSRule)(r))
+	if err != nil {
+		return err
+	}
+	var v any
+	switch r.Type {
+	case "", C.RuleTypeDefault:
+		r.Type = C.RuleTypeDefault
+		v = &r.DefaultOptions
+	case C.RuleTypeLogical:
+		v = &r.LogicalOptions
+	default:
+		return E.New("unknown rule type: " + r.Type)
+	}
+	err = UnmarshallExcluded(bytes, (*_DNSRule)(r), v)
+	if err != nil {
+		return E.Cause(err, "dns route rule")
+	}
+	return nil
+}
+
+type DefaultDNSRule struct {
+	Inbound         Listable[string]       `json:"inbound,omitempty"`
+	IPVersion       int                    `json:"ip_version,omitempty"`
+	QueryType       Listable[DNSQueryType] `json:"query_type,omitempty"`
+	Network         Listable[string]       `json:"network,omitempty"`
+	AuthUser        Listable[string]       `json:"auth_user,omitempty"`
+	Protocol        Listable[string]       `json:"protocol,omitempty"`
+	Domain          Listable[string]       `json:"domain,omitempty"`
+	DomainSuffix    Listable[string]       `json:"domain_suffix,omitempty"`
+	DomainKeyword   Listable[string]       `json:"domain_keyword,omitempty"`
+	DomainRegex     Listable[string]       `json:"domain_regex,omitempty"`
+	Geosite         Listable[string]       `json:"geosite,omitempty"`
+	SourceGeoIP     Listable[string]       `json:"source_geoip,omitempty"`
+	SourceIPCIDR    Listable[string]       `json:"source_ip_cidr,omitempty"`
+	SourcePort      Listable[uint16]       `json:"source_port,omitempty"`
+	SourcePortRange Listable[string]       `json:"source_port_range,omitempty"`
+	Port            Listable[uint16]       `json:"port,omitempty"`
+	PortRange       Listable[string]       `json:"port_range,omitempty"`
+	ProcessName     Listable[string]       `json:"process_name,omitempty"`
+	ProcessPath     Listable[string]       `json:"process_path,omitempty"`
+	PackageName     Listable[string]       `json:"package_name,omitempty"`
+	User            Listable[string]       `json:"user,omitempty"`
+	UserID          Listable[int32]        `json:"user_id,omitempty"`
+	Outbound        Listable[string]       `json:"outbound,omitempty"`
+	ClashMode       string                 `json:"clash_mode,omitempty"`
+	Invert          bool                   `json:"invert,omitempty"`
+	Server          string                 `json:"server,omitempty"`
+	DisableCache    bool                   `json:"disable_cache,omitempty"`
+	RewriteTTL      *uint32                `json:"rewrite_ttl,omitempty"`
+}
+
+func (r DefaultDNSRule) IsValid() bool {
+	var defaultValue DefaultDNSRule
+	defaultValue.Invert = r.Invert
+	defaultValue.Server = r.Server
+	defaultValue.DisableCache = r.DisableCache
+	defaultValue.RewriteTTL = r.RewriteTTL
+	return !reflect.DeepEqual(r, defaultValue)
+}
+
+type LogicalDNSRule struct {
+	Mode         string           `json:"mode"`
+	Rules        []DefaultDNSRule `json:"rules,omitempty"`
+	Invert       bool             `json:"invert,omitempty"`
+	Server       string           `json:"server,omitempty"`
+	DisableCache bool             `json:"disable_cache,omitempty"`
+	RewriteTTL   *uint32          `json:"rewrite_ttl,omitempty"`
+}
+
+func (r LogicalDNSRule) IsValid() bool {
+	return len(r.Rules) > 0 && common.All(r.Rules, DefaultDNSRule.IsValid)
+}

+ 8 - 258
route/router.go

@@ -2,14 +2,11 @@ package route
 
 import (
 	"context"
-	"io"
 	"net"
-	"net/http"
 	"net/netip"
 	"net/url"
 	"os"
 	"os/user"
-	"path/filepath"
 	"strings"
 	"time"
 
@@ -38,7 +35,6 @@ import (
 	F "github.com/sagernet/sing/common/format"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
-	"github.com/sagernet/sing/common/rw"
 	"github.com/sagernet/sing/common/uot"
 )
 
@@ -127,6 +123,7 @@ func NewRouter(
 		}
 		router.dnsRules = append(router.dnsRules, dnsRule)
 	}
+
 	transports := make([]dns.Transport, len(dnsOptions.Servers))
 	dummyTransportMap := make(map[string]dns.Transport)
 	transportMap := make(map[string]dns.Transport)
@@ -523,27 +520,6 @@ func (r *Router) Close() error {
 	return err
 }
 
-func (r *Router) GeoIPReader() *geoip.Reader {
-	return r.geoIPReader
-}
-
-func (r *Router) LoadGeosite(code string) (adapter.Rule, error) {
-	rule, cached := r.geositeCache[code]
-	if cached {
-		return rule, nil
-	}
-	items, err := r.geositeReader.Read(code)
-	if err != nil {
-		return nil, err
-	}
-	rule, err = NewDefaultRule(r, nil, geosite.Compile(items))
-	if err != nil {
-		return nil, err
-	}
-	r.geositeCache[code] = rule
-	return rule, nil
-}
-
 func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
 	outbound, loaded := r.outboundByTag[tag]
 	return outbound, loaded
@@ -725,6 +701,13 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
 		}
 		conn = bufio.NewCachedPacketConn(conn, buffer, destination)
 	}
+	if r.dnsReverseMapping != nil && metadata.Domain == "" {
+		domain, loaded := r.dnsReverseMapping.Query(metadata.Destination.Addr)
+		if loaded {
+			metadata.Domain = domain
+			r.logger.DebugContext(ctx, "found reserve mapped domain: ", metadata.Domain)
+		}
+	}
 	if metadata.Destination.IsFqdn() && dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) != dns.DomainStrategyAsIS {
 		addresses, err := r.Lookup(adapter.WithContext(ctx, &metadata), metadata.Destination.Fqdn, dns.DomainStrategy(metadata.InboundOptions.DomainStrategy))
 		if err != nil {
@@ -898,239 +881,6 @@ func (r *Router) SetV2RayServer(server adapter.V2RayServer) {
 	r.v2rayServer = server
 }
 
-func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool {
-	for _, rule := range rules {
-		switch rule.Type {
-		case C.RuleTypeDefault:
-			if cond(rule.DefaultOptions) {
-				return true
-			}
-		case C.RuleTypeLogical:
-			for _, subRule := range rule.LogicalOptions.Rules {
-				if cond(subRule) {
-					return true
-				}
-			}
-		}
-	}
-	return false
-}
-
-func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bool) bool {
-	for _, rule := range rules {
-		switch rule.Type {
-		case C.RuleTypeDefault:
-			if cond(rule.DefaultOptions) {
-				return true
-			}
-		case C.RuleTypeLogical:
-			for _, subRule := range rule.LogicalOptions.Rules {
-				if cond(subRule) {
-					return true
-				}
-			}
-		}
-	}
-	return false
-}
-
-func isGeoIPRule(rule option.DefaultRule) bool {
-	return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) || len(rule.GeoIP) > 0 && common.Any(rule.GeoIP, notPrivateNode)
-}
-
-func isGeoIPDNSRule(rule option.DefaultDNSRule) bool {
-	return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode)
-}
-
-func isGeositeRule(rule option.DefaultRule) bool {
-	return len(rule.Geosite) > 0
-}
-
-func isGeositeDNSRule(rule option.DefaultDNSRule) bool {
-	return len(rule.Geosite) > 0
-}
-
-func isProcessRule(rule option.DefaultRule) bool {
-	return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
-}
-
-func isProcessDNSRule(rule option.DefaultDNSRule) bool {
-	return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
-}
-
-func notPrivateNode(code string) bool {
-	return code != "private"
-}
-
-func (r *Router) prepareGeoIPDatabase() error {
-	var geoPath string
-	if r.geoIPOptions.Path != "" {
-		geoPath = r.geoIPOptions.Path
-	} else {
-		geoPath = "geoip.db"
-		if foundPath, loaded := C.FindPath(geoPath); loaded {
-			geoPath = foundPath
-		}
-	}
-	geoPath = C.BasePath(geoPath)
-	if !rw.FileExists(geoPath) {
-		r.logger.Warn("geoip database not exists: ", geoPath)
-		var err error
-		for attempts := 0; attempts < 3; attempts++ {
-			err = r.downloadGeoIPDatabase(geoPath)
-			if err == nil {
-				break
-			}
-			r.logger.Error("download geoip database: ", err)
-			os.Remove(geoPath)
-			// time.Sleep(10 * time.Second)
-		}
-		if err != nil {
-			return err
-		}
-	}
-	geoReader, codes, err := geoip.Open(geoPath)
-	if err != nil {
-		return E.Cause(err, "open geoip database")
-	}
-	r.logger.Info("loaded geoip database: ", len(codes), " codes")
-	r.geoIPReader = geoReader
-	return nil
-}
-
-func (r *Router) prepareGeositeDatabase() error {
-	var geoPath string
-	if r.geositeOptions.Path != "" {
-		geoPath = r.geositeOptions.Path
-	} else {
-		geoPath = "geosite.db"
-		if foundPath, loaded := C.FindPath(geoPath); loaded {
-			geoPath = foundPath
-		}
-	}
-	geoPath = C.BasePath(geoPath)
-	if !rw.FileExists(geoPath) {
-		r.logger.Warn("geosite database not exists: ", geoPath)
-		var err error
-		for attempts := 0; attempts < 3; attempts++ {
-			err = r.downloadGeositeDatabase(geoPath)
-			if err == nil {
-				break
-			}
-			r.logger.Error("download geosite database: ", err)
-			os.Remove(geoPath)
-			// time.Sleep(10 * time.Second)
-		}
-		if err != nil {
-			return err
-		}
-	}
-	geoReader, codes, err := geosite.Open(geoPath)
-	if err == nil {
-		r.logger.Info("loaded geosite database: ", len(codes), " codes")
-		r.geositeReader = geoReader
-	} else {
-		return E.Cause(err, "open geosite database")
-	}
-	return nil
-}
-
-func (r *Router) downloadGeoIPDatabase(savePath string) error {
-	var downloadURL string
-	if r.geoIPOptions.DownloadURL != "" {
-		downloadURL = r.geoIPOptions.DownloadURL
-	} else {
-		downloadURL = "https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db"
-	}
-	r.logger.Info("downloading geoip database")
-	var detour adapter.Outbound
-	if r.geoIPOptions.DownloadDetour != "" {
-		outbound, loaded := r.Outbound(r.geoIPOptions.DownloadDetour)
-		if !loaded {
-			return E.New("detour outbound not found: ", r.geoIPOptions.DownloadDetour)
-		}
-		detour = outbound
-	} else {
-		detour = r.defaultOutboundForConnection
-	}
-
-	if parentDir := filepath.Dir(savePath); parentDir != "" {
-		os.MkdirAll(parentDir, 0o755)
-	}
-
-	saveFile, err := os.OpenFile(savePath, os.O_CREATE|os.O_WRONLY, 0o644)
-	if err != nil {
-		return E.Cause(err, "open output file: ", downloadURL)
-	}
-	defer saveFile.Close()
-
-	httpClient := &http.Client{
-		Transport: &http.Transport{
-			ForceAttemptHTTP2:   true,
-			TLSHandshakeTimeout: 5 * time.Second,
-			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
-				return detour.DialContext(ctx, network, M.ParseSocksaddr(addr))
-			},
-		},
-	}
-	defer httpClient.CloseIdleConnections()
-	response, err := httpClient.Get(downloadURL)
-	if err != nil {
-		return err
-	}
-	defer response.Body.Close()
-	_, err = io.Copy(saveFile, response.Body)
-	return err
-}
-
-func (r *Router) downloadGeositeDatabase(savePath string) error {
-	var downloadURL string
-	if r.geositeOptions.DownloadURL != "" {
-		downloadURL = r.geositeOptions.DownloadURL
-	} else {
-		downloadURL = "https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db"
-	}
-	r.logger.Info("downloading geosite database")
-	var detour adapter.Outbound
-	if r.geositeOptions.DownloadDetour != "" {
-		outbound, loaded := r.Outbound(r.geositeOptions.DownloadDetour)
-		if !loaded {
-			return E.New("detour outbound not found: ", r.geositeOptions.DownloadDetour)
-		}
-		detour = outbound
-	} else {
-		detour = r.defaultOutboundForConnection
-	}
-
-	if parentDir := filepath.Dir(savePath); parentDir != "" {
-		os.MkdirAll(parentDir, 0o755)
-	}
-
-	saveFile, err := os.OpenFile(savePath, os.O_CREATE|os.O_WRONLY, 0o644)
-	if err != nil {
-		return E.Cause(err, "open output file: ", downloadURL)
-	}
-	defer saveFile.Close()
-
-	httpClient := &http.Client{
-		Transport: &http.Transport{
-			ForceAttemptHTTP2:   true,
-			TLSHandshakeTimeout: 5 * time.Second,
-			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
-				return detour.DialContext(ctx, network, M.ParseSocksaddr(addr))
-			},
-		},
-	}
-	defer httpClient.CloseIdleConnections()
-	response, err := httpClient.Get(downloadURL)
-	if err != nil {
-		return err
-	}
-	defer response.Body.Close()
-	_, err = io.Copy(saveFile, response.Body)
-	return err
-}
-
 func (r *Router) OnPackagesUpdated(packages int, sharedUsers int) {
 	r.logger.Info("updated packages list: ", packages, " packages, ", sharedUsers, " shared users")
 }

+ 3 - 0
route/router_dns.go

@@ -47,6 +47,9 @@ func (r *Router) matchDNS(ctx context.Context) (context.Context, dns.Transport,
 			if rule.DisableCache() {
 				ctx = dns.ContextWithDisableCache(ctx, true)
 			}
+			if rewriteTTL := rule.RewriteTTL(); rewriteTTL != nil {
+				ctx = dns.ContextWithRewriteTTL(ctx, *rewriteTTL)
+			}
 			detour := rule.Outbound()
 			r.dnsLogger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour)
 			if transport, loaded := r.transportMap[detour]; loaded {

+ 283 - 0
route/router_geo_resources.go

@@ -0,0 +1,283 @@
+package route
+
+import (
+	"context"
+	"io"
+	"net"
+	"net/http"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/geoip"
+	"github.com/sagernet/sing-box/common/geosite"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	"github.com/sagernet/sing/common/rw"
+)
+
+func (r *Router) GeoIPReader() *geoip.Reader {
+	return r.geoIPReader
+}
+
+func (r *Router) LoadGeosite(code string) (adapter.Rule, error) {
+	rule, cached := r.geositeCache[code]
+	if cached {
+		return rule, nil
+	}
+	items, err := r.geositeReader.Read(code)
+	if err != nil {
+		return nil, err
+	}
+	rule, err = NewDefaultRule(r, nil, geosite.Compile(items))
+	if err != nil {
+		return nil, err
+	}
+	r.geositeCache[code] = rule
+	return rule, nil
+}
+
+func (r *Router) prepareGeoIPDatabase() error {
+	var geoPath string
+	if r.geoIPOptions.Path != "" {
+		geoPath = r.geoIPOptions.Path
+	} else {
+		geoPath = "geoip.db"
+		if foundPath, loaded := C.FindPath(geoPath); loaded {
+			geoPath = foundPath
+		}
+	}
+	geoPath = C.BasePath(geoPath)
+	if rw.FileExists(geoPath) {
+		geoReader, codes, err := geoip.Open(geoPath)
+		if err == nil {
+			r.logger.Info("loaded geoip database: ", len(codes), " codes")
+			r.geoIPReader = geoReader
+			return nil
+		}
+	}
+	if !rw.FileExists(geoPath) {
+		r.logger.Warn("geoip database not exists: ", geoPath)
+		var err error
+		for attempts := 0; attempts < 3; attempts++ {
+			err = r.downloadGeoIPDatabase(geoPath)
+			if err == nil {
+				break
+			}
+			r.logger.Error("download geoip database: ", err)
+			os.Remove(geoPath)
+			// time.Sleep(10 * time.Second)
+		}
+		if err != nil {
+			return err
+		}
+	}
+	geoReader, codes, err := geoip.Open(geoPath)
+	if err != nil {
+		return E.Cause(err, "open geoip database")
+	}
+	r.logger.Info("loaded geoip database: ", len(codes), " codes")
+	r.geoIPReader = geoReader
+	return nil
+}
+
+func (r *Router) prepareGeositeDatabase() error {
+	var geoPath string
+	if r.geositeOptions.Path != "" {
+		geoPath = r.geositeOptions.Path
+	} else {
+		geoPath = "geosite.db"
+		if foundPath, loaded := C.FindPath(geoPath); loaded {
+			geoPath = foundPath
+		}
+	}
+	geoPath = C.BasePath(geoPath)
+	if !rw.FileExists(geoPath) {
+		r.logger.Warn("geosite database not exists: ", geoPath)
+		var err error
+		for attempts := 0; attempts < 3; attempts++ {
+			err = r.downloadGeositeDatabase(geoPath)
+			if err == nil {
+				break
+			}
+			r.logger.Error("download geosite database: ", err)
+			os.Remove(geoPath)
+			// time.Sleep(10 * time.Second)
+		}
+		if err != nil {
+			return err
+		}
+	}
+	geoReader, codes, err := geosite.Open(geoPath)
+	if err == nil {
+		r.logger.Info("loaded geosite database: ", len(codes), " codes")
+		r.geositeReader = geoReader
+	} else {
+		return E.Cause(err, "open geosite database")
+	}
+	return nil
+}
+
+func (r *Router) downloadGeoIPDatabase(savePath string) error {
+	var downloadURL string
+	if r.geoIPOptions.DownloadURL != "" {
+		downloadURL = r.geoIPOptions.DownloadURL
+	} else {
+		downloadURL = "https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db"
+	}
+	r.logger.Info("downloading geoip database")
+	var detour adapter.Outbound
+	if r.geoIPOptions.DownloadDetour != "" {
+		outbound, loaded := r.Outbound(r.geoIPOptions.DownloadDetour)
+		if !loaded {
+			return E.New("detour outbound not found: ", r.geoIPOptions.DownloadDetour)
+		}
+		detour = outbound
+	} else {
+		detour = r.defaultOutboundForConnection
+	}
+
+	if parentDir := filepath.Dir(savePath); parentDir != "" {
+		os.MkdirAll(parentDir, 0o755)
+	}
+
+	saveFile, err := os.OpenFile(savePath, os.O_CREATE|os.O_WRONLY, 0o644)
+	if err != nil {
+		return E.Cause(err, "open output file: ", downloadURL)
+	}
+	defer saveFile.Close()
+
+	httpClient := &http.Client{
+		Transport: &http.Transport{
+			ForceAttemptHTTP2:   true,
+			TLSHandshakeTimeout: 5 * time.Second,
+			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+				return detour.DialContext(ctx, network, M.ParseSocksaddr(addr))
+			},
+		},
+	}
+	defer httpClient.CloseIdleConnections()
+	response, err := httpClient.Get(downloadURL)
+	if err != nil {
+		return err
+	}
+	defer response.Body.Close()
+	_, err = io.Copy(saveFile, response.Body)
+	return err
+}
+
+func (r *Router) downloadGeositeDatabase(savePath string) error {
+	var downloadURL string
+	if r.geositeOptions.DownloadURL != "" {
+		downloadURL = r.geositeOptions.DownloadURL
+	} else {
+		downloadURL = "https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db"
+	}
+	r.logger.Info("downloading geosite database")
+	var detour adapter.Outbound
+	if r.geositeOptions.DownloadDetour != "" {
+		outbound, loaded := r.Outbound(r.geositeOptions.DownloadDetour)
+		if !loaded {
+			return E.New("detour outbound not found: ", r.geositeOptions.DownloadDetour)
+		}
+		detour = outbound
+	} else {
+		detour = r.defaultOutboundForConnection
+	}
+
+	if parentDir := filepath.Dir(savePath); parentDir != "" {
+		os.MkdirAll(parentDir, 0o755)
+	}
+
+	saveFile, err := os.OpenFile(savePath, os.O_CREATE|os.O_WRONLY, 0o644)
+	if err != nil {
+		return E.Cause(err, "open output file: ", downloadURL)
+	}
+	defer saveFile.Close()
+
+	httpClient := &http.Client{
+		Transport: &http.Transport{
+			ForceAttemptHTTP2:   true,
+			TLSHandshakeTimeout: 5 * time.Second,
+			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+				return detour.DialContext(ctx, network, M.ParseSocksaddr(addr))
+			},
+		},
+	}
+	defer httpClient.CloseIdleConnections()
+	response, err := httpClient.Get(downloadURL)
+	if err != nil {
+		return err
+	}
+	defer response.Body.Close()
+	_, err = io.Copy(saveFile, response.Body)
+	return err
+}
+
+func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool {
+	for _, rule := range rules {
+		switch rule.Type {
+		case C.RuleTypeDefault:
+			if cond(rule.DefaultOptions) {
+				return true
+			}
+		case C.RuleTypeLogical:
+			for _, subRule := range rule.LogicalOptions.Rules {
+				if cond(subRule) {
+					return true
+				}
+			}
+		}
+	}
+	return false
+}
+
+func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bool) bool {
+	for _, rule := range rules {
+		switch rule.Type {
+		case C.RuleTypeDefault:
+			if cond(rule.DefaultOptions) {
+				return true
+			}
+		case C.RuleTypeLogical:
+			for _, subRule := range rule.LogicalOptions.Rules {
+				if cond(subRule) {
+					return true
+				}
+			}
+		}
+	}
+	return false
+}
+
+func isGeoIPRule(rule option.DefaultRule) bool {
+	return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) || len(rule.GeoIP) > 0 && common.Any(rule.GeoIP, notPrivateNode)
+}
+
+func isGeoIPDNSRule(rule option.DefaultDNSRule) bool {
+	return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode)
+}
+
+func isGeositeRule(rule option.DefaultRule) bool {
+	return len(rule.Geosite) > 0
+}
+
+func isGeositeDNSRule(rule option.DefaultDNSRule) bool {
+	return len(rule.Geosite) > 0
+}
+
+func isProcessRule(rule option.DefaultRule) bool {
+	return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
+}
+
+func isProcessDNSRule(rule option.DefaultDNSRule) bool {
+	return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
+}
+
+func notPrivateNode(code string) bool {
+	return code != "private"
+}

+ 203 - 0
route/rule_abstract.go

@@ -0,0 +1,203 @@
+package route
+
+import (
+	"strings"
+
+	"github.com/sagernet/sing-box/adapter"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing/common"
+	F "github.com/sagernet/sing/common/format"
+)
+
+type abstractDefaultRule struct {
+	items                   []RuleItem
+	sourceAddressItems      []RuleItem
+	sourcePortItems         []RuleItem
+	destinationAddressItems []RuleItem
+	destinationPortItems    []RuleItem
+	allItems                []RuleItem
+	invert                  bool
+	outbound                string
+}
+
+func (r *abstractDefaultRule) Type() string {
+	return C.RuleTypeDefault
+}
+
+func (r *abstractDefaultRule) Start() error {
+	for _, item := range r.allItems {
+		err := common.Start(item)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (r *abstractDefaultRule) Close() error {
+	for _, item := range r.allItems {
+		err := common.Close(item)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (r *abstractDefaultRule) UpdateGeosite() error {
+	for _, item := range r.allItems {
+		if geositeItem, isSite := item.(*GeositeItem); isSite {
+			err := geositeItem.Update()
+			if err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
+	for _, item := range r.items {
+		if !item.Match(metadata) {
+			return r.invert
+		}
+	}
+
+	if len(r.sourceAddressItems) > 0 {
+		var sourceAddressMatch bool
+		for _, item := range r.sourceAddressItems {
+			if item.Match(metadata) {
+				sourceAddressMatch = true
+				break
+			}
+		}
+		if !sourceAddressMatch {
+			return r.invert
+		}
+	}
+
+	if len(r.sourcePortItems) > 0 {
+		var sourcePortMatch bool
+		for _, item := range r.sourcePortItems {
+			if item.Match(metadata) {
+				sourcePortMatch = true
+				break
+			}
+		}
+		if !sourcePortMatch {
+			return r.invert
+		}
+	}
+
+	if len(r.destinationAddressItems) > 0 {
+		var destinationAddressMatch bool
+		for _, item := range r.destinationAddressItems {
+			if item.Match(metadata) {
+				destinationAddressMatch = true
+				break
+			}
+		}
+		if !destinationAddressMatch {
+			return r.invert
+		}
+	}
+
+	if len(r.destinationPortItems) > 0 {
+		var destinationPortMatch bool
+		for _, item := range r.destinationPortItems {
+			if item.Match(metadata) {
+				destinationPortMatch = true
+				break
+			}
+		}
+		if !destinationPortMatch {
+			return r.invert
+		}
+	}
+
+	return !r.invert
+}
+
+func (r *abstractDefaultRule) Outbound() string {
+	return r.outbound
+}
+
+func (r *abstractDefaultRule) String() string {
+	if !r.invert {
+		return strings.Join(F.MapToString(r.allItems), " ")
+	} else {
+		return "!(" + strings.Join(F.MapToString(r.allItems), " ") + ")"
+	}
+}
+
+type abstractLogicalRule struct {
+	rules    []adapter.Rule
+	mode     string
+	invert   bool
+	outbound string
+}
+
+func (r *abstractLogicalRule) Type() string {
+	return C.RuleTypeLogical
+}
+
+func (r *abstractLogicalRule) UpdateGeosite() error {
+	for _, rule := range r.rules {
+		err := rule.UpdateGeosite()
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (r *abstractLogicalRule) Start() error {
+	for _, rule := range r.rules {
+		err := rule.Start()
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (r *abstractLogicalRule) Close() error {
+	for _, rule := range r.rules {
+		err := rule.Close()
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool {
+	if r.mode == C.LogicalTypeAnd {
+		return common.All(r.rules, func(it adapter.Rule) bool {
+			return it.Match(metadata)
+		}) != r.invert
+	} else {
+		return common.Any(r.rules, func(it adapter.Rule) bool {
+			return it.Match(metadata)
+		}) != r.invert
+	}
+}
+
+func (r *abstractLogicalRule) Outbound() string {
+	return r.outbound
+}
+
+func (r *abstractLogicalRule) String() string {
+	var op string
+	switch r.mode {
+	case C.LogicalTypeAnd:
+		op = "&&"
+	case C.LogicalTypeOr:
+		op = "||"
+	}
+	if !r.invert {
+		return strings.Join(F.MapToString(r.rules), " "+op+" ")
+	} else {
+		return "!(" + strings.Join(F.MapToString(r.rules), " "+op+" ") + ")"
+	}
+}

+ 15 - 206
route/rule.go → route/rule_default.go

@@ -1,16 +1,11 @@
 package route
 
 import (
-	"strings"
-
 	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
-	F "github.com/sagernet/sing/common/format"
-	N "github.com/sagernet/sing/common/network"
 )
 
 func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule) (adapter.Rule, error) {
@@ -39,14 +34,7 @@ func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rul
 var _ adapter.Rule = (*DefaultRule)(nil)
 
 type DefaultRule struct {
-	items                   []RuleItem
-	sourceAddressItems      []RuleItem
-	sourcePortItems         []RuleItem
-	destinationAddressItems []RuleItem
-	destinationPortItems    []RuleItem
-	allItems                []RuleItem
-	invert                  bool
-	outbound                string
+	abstractDefaultRule
 }
 
 type RuleItem interface {
@@ -56,8 +44,10 @@ type RuleItem interface {
 
 func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options option.DefaultRule) (*DefaultRule, error) {
 	rule := &DefaultRule{
-		invert:   options.Invert,
-		outbound: options.Outbound,
+		abstractDefaultRule{
+			invert:   options.Invert,
+			outbound: options.Outbound,
+		},
 	}
 	if len(options.Inbound) > 0 {
 		item := NewInboundRule(options.Inbound)
@@ -74,15 +64,10 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
 			return nil, E.New("invalid ip version: ", options.IPVersion)
 		}
 	}
-	if options.Network != "" {
-		switch options.Network {
-		case N.NetworkTCP, N.NetworkUDP:
-			item := NewNetworkItem(options.Network)
-			rule.items = append(rule.items, item)
-			rule.allItems = append(rule.allItems, item)
-		default:
-			return nil, E.New("invalid network: ", options.Network)
-		}
+	if len(options.Network) > 0 {
+		item := NewNetworkItem(options.Network)
+		rule.items = append(rule.items, item)
+		rule.allItems = append(rule.allItems, item)
 	}
 	if len(options.AuthUser) > 0 {
 		item := NewAuthUserItem(options.AuthUser)
@@ -202,130 +187,19 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
 	return rule, nil
 }
 
-func (r *DefaultRule) Type() string {
-	return C.RuleTypeDefault
-}
-
-func (r *DefaultRule) Start() error {
-	for _, item := range r.allItems {
-		err := common.Start(item)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (r *DefaultRule) Close() error {
-	for _, item := range r.allItems {
-		err := common.Close(item)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (r *DefaultRule) UpdateGeosite() error {
-	for _, item := range r.allItems {
-		if geositeItem, isSite := item.(*GeositeItem); isSite {
-			err := geositeItem.Update()
-			if err != nil {
-				return err
-			}
-		}
-	}
-	return nil
-}
-
-func (r *DefaultRule) Match(metadata *adapter.InboundContext) bool {
-	for _, item := range r.items {
-		if !item.Match(metadata) {
-			return r.invert
-		}
-	}
-
-	if len(r.sourceAddressItems) > 0 {
-		var sourceAddressMatch bool
-		for _, item := range r.sourceAddressItems {
-			if item.Match(metadata) {
-				sourceAddressMatch = true
-				break
-			}
-		}
-		if !sourceAddressMatch {
-			return r.invert
-		}
-	}
-
-	if len(r.sourcePortItems) > 0 {
-		var sourcePortMatch bool
-		for _, item := range r.sourcePortItems {
-			if item.Match(metadata) {
-				sourcePortMatch = true
-				break
-			}
-		}
-		if !sourcePortMatch {
-			return r.invert
-		}
-	}
-
-	if len(r.destinationAddressItems) > 0 {
-		var destinationAddressMatch bool
-		for _, item := range r.destinationAddressItems {
-			if item.Match(metadata) {
-				destinationAddressMatch = true
-				break
-			}
-		}
-		if !destinationAddressMatch {
-			return r.invert
-		}
-	}
-
-	if len(r.destinationPortItems) > 0 {
-		var destinationPortMatch bool
-		for _, item := range r.destinationPortItems {
-			if item.Match(metadata) {
-				destinationPortMatch = true
-				break
-			}
-		}
-		if !destinationPortMatch {
-			return r.invert
-		}
-	}
-
-	return !r.invert
-}
-
-func (r *DefaultRule) Outbound() string {
-	return r.outbound
-}
-
-func (r *DefaultRule) String() string {
-	if !r.invert {
-		return strings.Join(F.MapToString(r.allItems), " ")
-	} else {
-		return "!(" + strings.Join(F.MapToString(r.allItems), " ") + ")"
-	}
-}
-
 var _ adapter.Rule = (*LogicalRule)(nil)
 
 type LogicalRule struct {
-	mode     string
-	rules    []*DefaultRule
-	invert   bool
-	outbound string
+	abstractLogicalRule
 }
 
 func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) {
 	r := &LogicalRule{
-		rules:    make([]*DefaultRule, len(options.Rules)),
-		invert:   options.Invert,
-		outbound: options.Outbound,
+		abstractLogicalRule{
+			rules:    make([]adapter.Rule, len(options.Rules)),
+			invert:   options.Invert,
+			outbound: options.Outbound,
+		},
 	}
 	switch options.Mode {
 	case C.LogicalTypeAnd:
@@ -344,68 +218,3 @@ func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options opt
 	}
 	return r, nil
 }
-
-func (r *LogicalRule) Type() string {
-	return C.RuleTypeLogical
-}
-
-func (r *LogicalRule) UpdateGeosite() error {
-	for _, rule := range r.rules {
-		err := rule.UpdateGeosite()
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (r *LogicalRule) Start() error {
-	for _, rule := range r.rules {
-		err := rule.Start()
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (r *LogicalRule) Close() error {
-	for _, rule := range r.rules {
-		err := rule.Close()
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (r *LogicalRule) Match(metadata *adapter.InboundContext) bool {
-	if r.mode == C.LogicalTypeAnd {
-		return common.All(r.rules, func(it *DefaultRule) bool {
-			return it.Match(metadata)
-		}) != r.invert
-	} else {
-		return common.Any(r.rules, func(it *DefaultRule) bool {
-			return it.Match(metadata)
-		}) != r.invert
-	}
-}
-
-func (r *LogicalRule) Outbound() string {
-	return r.outbound
-}
-
-func (r *LogicalRule) String() string {
-	var op string
-	switch r.mode {
-	case C.LogicalTypeAnd:
-		op = "&&"
-	case C.LogicalTypeOr:
-		op = "||"
-	}
-	if !r.invert {
-		return strings.Join(F.MapToString(r.rules), " "+op+" ")
-	} else {
-		return "!(" + strings.Join(F.MapToString(r.rules), " "+op+" ") + ")"
-	}
-}

+ 24 - 199
route/rule_dns.go

@@ -1,16 +1,11 @@
 package route
 
 import (
-	"strings"
-
 	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
-	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
-	F "github.com/sagernet/sing/common/format"
-	N "github.com/sagernet/sing/common/network"
 )
 
 func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule) (adapter.DNSRule, error) {
@@ -39,22 +34,19 @@ func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.
 var _ adapter.DNSRule = (*DefaultDNSRule)(nil)
 
 type DefaultDNSRule struct {
-	items                   []RuleItem
-	sourceAddressItems      []RuleItem
-	sourcePortItems         []RuleItem
-	destinationAddressItems []RuleItem
-	destinationPortItems    []RuleItem
-	allItems                []RuleItem
-	invert                  bool
-	outbound                string
-	disableCache            bool
+	abstractDefaultRule
+	disableCache bool
+	rewriteTTL   *uint32
 }
 
 func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) {
 	rule := &DefaultDNSRule{
-		invert:       options.Invert,
-		outbound:     options.Server,
+		abstractDefaultRule: abstractDefaultRule{
+			invert:   options.Invert,
+			outbound: options.Server,
+		},
 		disableCache: options.DisableCache,
+		rewriteTTL:   options.RewriteTTL,
 	}
 	if len(options.Inbound) > 0 {
 		item := NewInboundRule(options.Inbound)
@@ -76,15 +68,10 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
 		rule.items = append(rule.items, item)
 		rule.allItems = append(rule.allItems, item)
 	}
-	if options.Network != "" {
-		switch options.Network {
-		case N.NetworkTCP, N.NetworkUDP:
-			item := NewNetworkItem(options.Network)
-			rule.items = append(rule.items, item)
-			rule.allItems = append(rule.allItems, item)
-		default:
-			return nil, E.New("invalid network: ", options.Network)
-		}
+	if len(options.Network) > 0 {
+		item := NewNetworkItem(options.Network)
+		rule.items = append(rule.items, item)
+		rule.allItems = append(rule.allItems, item)
 	}
 	if len(options.AuthUser) > 0 {
 		item := NewAuthUserItem(options.AuthUser)
@@ -196,132 +183,31 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
 	return rule, nil
 }
 
-func (r *DefaultDNSRule) Type() string {
-	return C.RuleTypeDefault
-}
-
-func (r *DefaultDNSRule) Start() error {
-	for _, item := range r.allItems {
-		err := common.Start(item)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (r *DefaultDNSRule) Close() error {
-	for _, item := range r.allItems {
-		err := common.Close(item)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (r *DefaultDNSRule) UpdateGeosite() error {
-	for _, item := range r.allItems {
-		if geositeItem, isSite := item.(*GeositeItem); isSite {
-			err := geositeItem.Update()
-			if err != nil {
-				return err
-			}
-		}
-	}
-	return nil
-}
-
-func (r *DefaultDNSRule) Match(metadata *adapter.InboundContext) bool {
-	for _, item := range r.items {
-		if !item.Match(metadata) {
-			return r.invert
-		}
-	}
-
-	if len(r.sourceAddressItems) > 0 {
-		var sourceAddressMatch bool
-		for _, item := range r.sourceAddressItems {
-			if item.Match(metadata) {
-				sourceAddressMatch = true
-				break
-			}
-		}
-		if !sourceAddressMatch {
-			return r.invert
-		}
-	}
-
-	if len(r.sourcePortItems) > 0 {
-		var sourcePortMatch bool
-		for _, item := range r.sourcePortItems {
-			if item.Match(metadata) {
-				sourcePortMatch = true
-				break
-			}
-		}
-		if !sourcePortMatch {
-			return r.invert
-		}
-	}
-
-	if len(r.destinationAddressItems) > 0 {
-		var destinationAddressMatch bool
-		for _, item := range r.destinationAddressItems {
-			if item.Match(metadata) {
-				destinationAddressMatch = true
-				break
-			}
-		}
-		if !destinationAddressMatch {
-			return r.invert
-		}
-	}
-
-	if len(r.destinationPortItems) > 0 {
-		var destinationPortMatch bool
-		for _, item := range r.destinationPortItems {
-			if item.Match(metadata) {
-				destinationPortMatch = true
-				break
-			}
-		}
-		if !destinationPortMatch {
-			return r.invert
-		}
-	}
-
-	return !r.invert
-}
-
-func (r *DefaultDNSRule) Outbound() string {
-	return r.outbound
-}
-
 func (r *DefaultDNSRule) DisableCache() bool {
 	return r.disableCache
 }
 
-func (r *DefaultDNSRule) String() string {
-	return strings.Join(F.MapToString(r.allItems), " ")
+func (r *DefaultDNSRule) RewriteTTL() *uint32 {
+	return r.rewriteTTL
 }
 
 var _ adapter.DNSRule = (*LogicalDNSRule)(nil)
 
 type LogicalDNSRule struct {
-	mode         string
-	rules        []*DefaultDNSRule
-	invert       bool
-	outbound     string
+	abstractLogicalRule
 	disableCache bool
+	rewriteTTL   *uint32
 }
 
 func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) {
 	r := &LogicalDNSRule{
-		rules:        make([]*DefaultDNSRule, len(options.Rules)),
-		invert:       options.Invert,
-		outbound:     options.Server,
+		abstractLogicalRule: abstractLogicalRule{
+			rules:    make([]adapter.Rule, len(options.Rules)),
+			invert:   options.Invert,
+			outbound: options.Server,
+		},
 		disableCache: options.DisableCache,
+		rewriteTTL:   options.RewriteTTL,
 	}
 	switch options.Mode {
 	case C.LogicalTypeAnd:
@@ -341,71 +227,10 @@ func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options
 	return r, nil
 }
 
-func (r *LogicalDNSRule) Type() string {
-	return C.RuleTypeLogical
-}
-
-func (r *LogicalDNSRule) UpdateGeosite() error {
-	for _, rule := range r.rules {
-		err := rule.UpdateGeosite()
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (r *LogicalDNSRule) Start() error {
-	for _, rule := range r.rules {
-		err := rule.Start()
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (r *LogicalDNSRule) Close() error {
-	for _, rule := range r.rules {
-		err := rule.Close()
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (r *LogicalDNSRule) Match(metadata *adapter.InboundContext) bool {
-	if r.mode == C.LogicalTypeAnd {
-		return common.All(r.rules, func(it *DefaultDNSRule) bool {
-			return it.Match(metadata)
-		}) != r.invert
-	} else {
-		return common.Any(r.rules, func(it *DefaultDNSRule) bool {
-			return it.Match(metadata)
-		}) != r.invert
-	}
-}
-
-func (r *LogicalDNSRule) Outbound() string {
-	return r.outbound
-}
-
 func (r *LogicalDNSRule) DisableCache() bool {
 	return r.disableCache
 }
 
-func (r *LogicalDNSRule) String() string {
-	var op string
-	switch r.mode {
-	case C.LogicalTypeAnd:
-		op = "&&"
-	case C.LogicalTypeOr:
-		op = "||"
-	}
-	if !r.invert {
-		return strings.Join(F.MapToString(r.rules), " "+op+" ")
-	} else {
-		return "!(" + strings.Join(F.MapToString(r.rules), " "+op+" ") + ")"
-	}
+func (r *LogicalDNSRule) RewriteTTL() *uint32 {
+	return r.rewriteTTL
 }

+ 0 - 0
route/rule_auth_user.go → route/rule_item_auth_user.go


+ 0 - 0
route/rule_cidr.go → route/rule_item_cidr.go


+ 0 - 0
route/rule_clash_mode.go → route/rule_item_clash_mode.go


+ 0 - 0
route/rule_domain.go → route/rule_item_domain.go


+ 0 - 0
route/rule_domain_keyword.go → route/rule_item_domain_keyword.go


+ 0 - 0
route/rule_domain_regex.go → route/rule_item_domain_regex.go


+ 0 - 0
route/rule_geoip.go → route/rule_item_geoip.go


+ 0 - 0
route/rule_geosite.go → route/rule_item_geosite.go


+ 0 - 0
route/rule_inbound.go → route/rule_item_inbound.go


+ 0 - 0
route/rule_ipversion.go → route/rule_item_ipversion.go


+ 42 - 0
route/rule_item_network.go

@@ -0,0 +1,42 @@
+package route
+
+import (
+	"strings"
+
+	"github.com/sagernet/sing-box/adapter"
+	F "github.com/sagernet/sing/common/format"
+)
+
+var _ RuleItem = (*NetworkItem)(nil)
+
+type NetworkItem struct {
+	networks   []string
+	networkMap map[string]bool
+}
+
+func NewNetworkItem(networks []string) *NetworkItem {
+	networkMap := make(map[string]bool)
+	for _, network := range networks {
+		networkMap[network] = true
+	}
+	return &NetworkItem{
+		networks:   networks,
+		networkMap: networkMap,
+	}
+}
+
+func (r *NetworkItem) Match(metadata *adapter.InboundContext) bool {
+	return r.networkMap[metadata.Network]
+}
+
+func (r *NetworkItem) String() string {
+	description := "network="
+
+	pLen := len(r.networks)
+	if pLen == 1 {
+		description += F.ToString(r.networks[0])
+	} else {
+		description += "[" + strings.Join(F.MapToString(r.networks), " ") + "]"
+	}
+	return description
+}

+ 0 - 0
route/rule_outbound.go → route/rule_item_outbound.go


+ 0 - 0
route/rule_package_name.go → route/rule_item_package_name.go


+ 0 - 0
route/rule_port.go → route/rule_item_port.go


+ 0 - 0
route/rule_port_range.go → route/rule_item_port_range.go


+ 0 - 0
route/rule_process_name.go → route/rule_item_process_name.go


+ 0 - 0
route/rule_process_path.go → route/rule_item_process_path.go


+ 0 - 0
route/rule_protocol.go → route/rule_item_protocol.go


+ 0 - 0
route/rule_query_type.go → route/rule_item_query_type.go


+ 0 - 0
route/rule_user.go → route/rule_item_user.go


+ 0 - 0
route/rule_user_id.go → route/rule_item_user_id.go


+ 0 - 23
route/rule_network.go

@@ -1,23 +0,0 @@
-package route
-
-import (
-	"github.com/sagernet/sing-box/adapter"
-)
-
-var _ RuleItem = (*NetworkItem)(nil)
-
-type NetworkItem struct {
-	network string
-}
-
-func NewNetworkItem(network string) *NetworkItem {
-	return &NetworkItem{network}
-}
-
-func (r *NetworkItem) Match(metadata *adapter.InboundContext) bool {
-	return r.network == metadata.Network
-}
-
-func (r *NetworkItem) String() string {
-	return "network=" + r.network
-}