浏览代码

Add auto-redirect & Improve auto-route

世界 1 年之前
父节点
当前提交
db3a0c636d

+ 1 - 1
Dockerfile

@@ -21,7 +21,7 @@ FROM --platform=$TARGETPLATFORM alpine AS dist
 LABEL maintainer="nekohasekai <[email protected]>"
 LABEL maintainer="nekohasekai <[email protected]>"
 RUN set -ex \
 RUN set -ex \
     && apk upgrade \
     && apk upgrade \
-    && apk add bash tzdata ca-certificates \
+    && apk add bash tzdata ca-certificates nftables \
     && rm -rf /var/cache/apk/*
     && rm -rf /var/cache/apk/*
 COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
 COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
 ENTRYPOINT ["sing-box"]
 ENTRYPOINT ["sing-box"]

+ 15 - 0
adapter/router.go

@@ -10,15 +10,18 @@ import (
 	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing/common/control"
 	"github.com/sagernet/sing/common/control"
 	N "github.com/sagernet/sing/common/network"
 	N "github.com/sagernet/sing/common/network"
+	"github.com/sagernet/sing/common/x/list"
 	"github.com/sagernet/sing/service"
 	"github.com/sagernet/sing/service"
 
 
 	mdns "github.com/miekg/dns"
 	mdns "github.com/miekg/dns"
+	"go4.org/netipx"
 )
 )
 
 
 type Router interface {
 type Router interface {
 	Service
 	Service
 	PreStarter
 	PreStarter
 	PostStarter
 	PostStarter
+	Cleanup() error
 
 
 	Outbounds() []Outbound
 	Outbounds() []Outbound
 	Outbound(tag string) (Outbound, bool)
 	Outbound(tag string) (Outbound, bool)
@@ -46,6 +49,8 @@ type Router interface {
 	AutoDetectInterface() bool
 	AutoDetectInterface() bool
 	AutoDetectInterfaceFunc() control.Func
 	AutoDetectInterfaceFunc() control.Func
 	DefaultMark() uint32
 	DefaultMark() uint32
+	RegisterAutoRedirectOutputMark(mark uint32) error
+	AutoRedirectOutputMark() uint32
 	NetworkMonitor() tun.NetworkUpdateMonitor
 	NetworkMonitor() tun.NetworkUpdateMonitor
 	InterfaceMonitor() tun.DefaultInterfaceMonitor
 	InterfaceMonitor() tun.DefaultInterfaceMonitor
 	PackageManager() tun.PackageManager
 	PackageManager() tun.PackageManager
@@ -92,12 +97,22 @@ type DNSRule interface {
 }
 }
 
 
 type RuleSet interface {
 type RuleSet interface {
+	Name() string
 	StartContext(ctx context.Context, startContext RuleSetStartContext) error
 	StartContext(ctx context.Context, startContext RuleSetStartContext) error
+	PostStart() error
 	Metadata() RuleSetMetadata
 	Metadata() RuleSetMetadata
+	ExtractIPSet() []*netipx.IPSet
+	IncRef()
+	DecRef()
+	Cleanup()
+	RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback]
+	UnregisterCallback(element *list.Element[RuleSetUpdateCallback])
 	Close() error
 	Close() error
 	HeadlessRule
 	HeadlessRule
 }
 }
 
 
+type RuleSetUpdateCallback func(it RuleSet)
+
 type RuleSetMetadata struct {
 type RuleSetMetadata struct {
 	ContainsProcessRule bool
 	ContainsProcessRule bool
 	ContainsWIFIRule    bool
 	ContainsWIFIRule    bool

+ 22 - 6
box.go

@@ -303,7 +303,11 @@ func (s *Box) start() error {
 			return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
 			return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
 		}
 		}
 	}
 	}
-	return s.postStart()
+	err = s.postStart()
+	if err != nil {
+		return err
+	}
+	return s.router.Cleanup()
 }
 }
 
 
 func (s *Box) postStart() error {
 func (s *Box) postStart() error {
@@ -313,16 +317,28 @@ func (s *Box) postStart() error {
 			return E.Cause(err, "start ", serviceName)
 			return E.Cause(err, "start ", serviceName)
 		}
 		}
 	}
 	}
-	for _, outbound := range s.outbounds {
-		if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound {
+	// TODO: reorganize ALL start order
+	for _, out := range s.outbounds {
+		if lateOutbound, isLateOutbound := out.(adapter.PostStarter); isLateOutbound {
 			err := lateOutbound.PostStart()
 			err := lateOutbound.PostStart()
 			if err != nil {
 			if err != nil {
-				return E.Cause(err, "post-start outbound/", outbound.Tag())
+				return E.Cause(err, "post-start outbound/", out.Tag())
 			}
 			}
 		}
 		}
 	}
 	}
-
-	return s.router.PostStart()
+	err := s.router.PostStart()
+	if err != nil {
+		return err
+	}
+	for _, in := range s.inbounds {
+		if lateInbound, isLateInbound := in.(adapter.PostStarter); isLateInbound {
+			err = lateInbound.PostStart()
+			if err != nil {
+				return E.Cause(err, "post-start inbound/", in.Tag())
+			}
+		}
+	}
+	return nil
 }
 }
 
 
 func (s *Box) Close() error {
 func (s *Box) Close() error {

+ 28 - 4
cmd/sing-box/cmd_tools_fetch.go

@@ -9,8 +9,10 @@ import (
 	"net/url"
 	"net/url"
 	"os"
 	"os"
 
 
+	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing/common/bufio"
 	"github.com/sagernet/sing/common/bufio"
+	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
 	M "github.com/sagernet/sing/common/metadata"
 
 
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
@@ -32,7 +34,10 @@ func init() {
 	commandTools.AddCommand(commandFetch)
 	commandTools.AddCommand(commandFetch)
 }
 }
 
 
-var httpClient *http.Client
+var (
+	httpClient  *http.Client
+	http3Client *http.Client
+)
 
 
 func fetch(args []string) error {
 func fetch(args []string) error {
 	instance, err := createPreStartedClient()
 	instance, err := createPreStartedClient()
@@ -53,8 +58,16 @@ func fetch(args []string) error {
 		},
 		},
 	}
 	}
 	defer httpClient.CloseIdleConnections()
 	defer httpClient.CloseIdleConnections()
+	if C.WithQUIC {
+		err = initializeHTTP3Client(instance)
+		if err != nil {
+			return err
+		}
+		defer http3Client.CloseIdleConnections()
+	}
 	for _, urlString := range args {
 	for _, urlString := range args {
-		parsedURL, err := url.Parse(urlString)
+		var parsedURL *url.URL
+		parsedURL, err = url.Parse(urlString)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
@@ -63,16 +76,27 @@ func fetch(args []string) error {
 			parsedURL.Scheme = "http"
 			parsedURL.Scheme = "http"
 			fallthrough
 			fallthrough
 		case "http", "https":
 		case "http", "https":
-			err = fetchHTTP(parsedURL)
+			err = fetchHTTP(httpClient, parsedURL)
+			if err != nil {
+				return err
+			}
+		case "http3":
+			if !C.WithQUIC {
+				return C.ErrQUICNotIncluded
+			}
+			parsedURL.Scheme = "https"
+			err = fetchHTTP(http3Client, parsedURL)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
+		default:
+			return E.New("unsupported scheme: ", parsedURL.Scheme)
 		}
 		}
 	}
 	}
 	return nil
 	return nil
 }
 }
 
 
-func fetchHTTP(parsedURL *url.URL) error {
+func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error {
 	request, err := http.NewRequest("GET", parsedURL.String(), nil)
 	request, err := http.NewRequest("GET", parsedURL.String(), nil)
 	if err != nil {
 	if err != nil {
 		return err
 		return err

+ 36 - 0
cmd/sing-box/cmd_tools_fetch_http3.go

@@ -0,0 +1,36 @@
+//go:build with_quic
+
+package main
+
+import (
+	"context"
+	"crypto/tls"
+	"net/http"
+
+	"github.com/sagernet/quic-go"
+	"github.com/sagernet/quic-go/http3"
+	box "github.com/sagernet/sing-box"
+	"github.com/sagernet/sing/common/bufio"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+)
+
+func initializeHTTP3Client(instance *box.Box) error {
+	dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound)
+	if err != nil {
+		return err
+	}
+	http3Client = &http.Client{
+		Transport: &http3.RoundTripper{
+			Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
+				destination := M.ParseSocksaddr(addr)
+				udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
+				if dErr != nil {
+					return nil, dErr
+				}
+				return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg)
+			},
+		},
+	}
+	return nil
+}

+ 18 - 0
cmd/sing-box/cmd_tools_fetch_http3_stub.go

@@ -0,0 +1,18 @@
+//go:build !with_quic
+
+package main
+
+import (
+	"net/url"
+	"os"
+
+	box "github.com/sagernet/sing-box"
+)
+
+func initializeHTTP3Client(instance *box.Box) error {
+	return os.ErrInvalid
+}
+
+func fetchHTTP3(parsedURL *url.URL) error {
+	return os.ErrInvalid
+}

+ 16 - 2
common/dialer/default.go

@@ -50,12 +50,26 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
 		dialer.Control = control.Append(dialer.Control, bindFunc)
 		dialer.Control = control.Append(dialer.Control, bindFunc)
 		listener.Control = control.Append(listener.Control, bindFunc)
 		listener.Control = control.Append(listener.Control, bindFunc)
 	}
 	}
-	if options.RoutingMark != 0 {
+	var autoRedirectOutputMark uint32
+	if router != nil {
+		autoRedirectOutputMark = router.AutoRedirectOutputMark()
+	}
+	if autoRedirectOutputMark > 0 {
+		dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark))
+		listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
+	}
+	if options.RoutingMark > 0 {
 		dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))
 		dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))
 		listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark))
 		listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark))
-	} else if router != nil && router.DefaultMark() != 0 {
+		if autoRedirectOutputMark > 0 {
+			return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `routing_mark`")
+		}
+	} else if router != nil && router.DefaultMark() > 0 {
 		dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark()))
 		dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark()))
 		listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark()))
 		listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark()))
+		if autoRedirectOutputMark > 0 {
+			return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `default_mark`")
+		}
 	}
 	}
 	if options.ReuseAddr {
 	if options.ReuseAddr {
 		listener.Control = control.Append(listener.Control, control.ReuseAddr())
 		listener.Control = control.Append(listener.Control, control.ReuseAddr())

+ 140 - 4
docs/configuration/inbound/tun.md

@@ -2,6 +2,21 @@
 icon: material/new-box
 icon: material/new-box
 ---
 ---
 
 
+!!! quote "Changes in sing-box 1.10.0"
+
+    :material-plus: [address](#address)  
+    :material-delete-clock: [inet4_address](#inet4_address)  
+    :material-delete-clock: [inet6_address](#inet6_address)  
+    :material-plus: [route_address](#route_address)  
+    :material-delete-clock: [inet4_route_address](#inet4_route_address)  
+    :material-delete-clock: [inet6_route_address](#inet6_route_address)  
+    :material-plus: [route_exclude_address](#route_address)  
+    :material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address)  
+    :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address)  
+    :material-plus: [auto_redirect](#auto_redirect)  
+    :material-plus: [route_address_set](#route_address_set)  
+    :material-plus: [route_exclude_address_set](#route_address_set)
+
 !!! quote "Changes in sing-box 1.9.0"
 !!! quote "Changes in sing-box 1.9.0"
 
 
     :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain)  
     :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain)  
@@ -23,26 +38,57 @@ icon: material/new-box
   "type": "tun",
   "type": "tun",
   "tag": "tun-in",
   "tag": "tun-in",
   "interface_name": "tun0",
   "interface_name": "tun0",
-  "inet4_address": "172.19.0.1/30",
-  "inet6_address": "fdfe:dcba:9876::1/126",
+  "address": [
+    "172.18.0.1/30",
+    "fdfe:dcba:9876::1/126"
+  ],
+  // deprecated
+  "inet4_address": [
+    "172.19.0.1/30"
+  ],
+  // deprecated
+  "inet6_address": [
+    "fdfe:dcba:9876::1/126"
+  ],
   "mtu": 9000,
   "mtu": 9000,
   "gso": false,
   "gso": false,
   "auto_route": true,
   "auto_route": true,
   "strict_route": true,
   "strict_route": true,
+  "auto_redirect": false,
+  "route_address": [
+    "0.0.0.0/1",
+    "128.0.0.0/1",
+    "::/1",
+    "8000::/1"
+  ],
+  // deprecated
   "inet4_route_address": [
   "inet4_route_address": [
     "0.0.0.0/1",
     "0.0.0.0/1",
     "128.0.0.0/1"
     "128.0.0.0/1"
   ],
   ],
+  // deprecated
   "inet6_route_address": [
   "inet6_route_address": [
     "::/1",
     "::/1",
     "8000::/1"
     "8000::/1"
   ],
   ],
+  "route_exclude_address": [
+    "192.168.0.0/16",
+    "fc00::/7"
+  ],
+  // deprecated
   "inet4_route_exclude_address": [
   "inet4_route_exclude_address": [
     "192.168.0.0/16"
     "192.168.0.0/16"
   ],
   ],
+  // deprecated
   "inet6_route_exclude_address": [
   "inet6_route_exclude_address": [
     "fc00::/7"
     "fc00::/7"
   ],
   ],
+  "route_address_set": [
+    "geoip-cloudflare"
+  ],
+  "route_exclude_address_set": [
+    "geoip-cn"
+  ],
   "endpoint_independent_nat": false,
   "endpoint_independent_nat": false,
   "udp_timeout": "5m",
   "udp_timeout": "5m",
   "stack": "system",
   "stack": "system",
@@ -102,14 +148,26 @@ icon: material/new-box
 
 
 Virtual device name, automatically selected if empty.
 Virtual device name, automatically selected if empty.
 
 
+#### address
+
+!!! question "Since sing-box 1.10.0"
+
+IPv4 and IPv6 prefix for the tun interface.
+
 #### inet4_address
 #### inet4_address
 
 
-==Required==
+!!! failure "Deprecated in sing-box 1.10.0"
+
+    `inet4_address` is merged to `address` and will be removed in sing-box 1.11.0.
 
 
 IPv4 prefix for the tun interface.
 IPv4 prefix for the tun interface.
 
 
 #### inet6_address
 #### inet6_address
 
 
+!!! failure "Deprecated in sing-box 1.10.0"
+
+    `inet6_address` is merged to `address` and will be removed in sing-box 1.11.0.
+
 IPv6 prefix for the tun interface.
 IPv6 prefix for the tun interface.
 
 
 #### mtu
 #### mtu
@@ -145,9 +203,10 @@ Enforce strict routing rules when `auto_route` is enabled:
 *In Linux*:
 *In Linux*:
 
 
 * Let unsupported network unreachable
 * Let unsupported network unreachable
+* Make ICMP traffic route to tun instead of upstream interfaces
 * Route all connections to tun
 * Route all connections to tun
 
 
-It prevents address leaks and makes DNS hijacking work on Android.
+It prevents IP address leaks and makes DNS hijacking work on Android.
 
 
 *In Windows*:
 *In Windows*:
 
 
@@ -156,22 +215,95 @@ It prevents address leaks and makes DNS hijacking work on Android.
 
 
 It may prevent some applications (such as VirtualBox) from working properly in certain situations.
 It may prevent some applications (such as VirtualBox) from working properly in certain situations.
 
 
+#### auto_redirect
+
+!!! question "Since sing-box 1.10.0"
+
+!!! quote ""
+
+    Only supported on Linux with `auto_route` enabled.
+
+Automatically configure iptables/nftables to redirect connections.
+
+*In Android*:
+
+Only local connections are forwarded. To share your VPN connection over hotspot or repeater,
+use [VPNHotspot](https://github.com/Mygod/VPNHotspot).
+
+*In Linux*:
+
+`auto_route` with `auto_redirect` now works as expected on routers **without intervention**.
+
+#### route_address
+
+!!! question "Since sing-box 1.10.0"
+
+Use custom routes instead of default when `auto_route` is enabled.
+
 #### inet4_route_address
 #### inet4_route_address
 
 
+!!! failure "Deprecated in sing-box 1.10.0"
+
+   `inet4_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) instead.
+
 Use custom routes instead of default when `auto_route` is enabled.
 Use custom routes instead of default when `auto_route` is enabled.
 
 
 #### inet6_route_address
 #### inet6_route_address
 
 
+!!! failure "Deprecated in sing-box 1.10.0"
+
+   `inet6_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) instead.
+
 Use custom routes instead of default when `auto_route` is enabled.
 Use custom routes instead of default when `auto_route` is enabled.
 
 
+#### route_exclude_address
+
+!!! question "Since sing-box 1.10.0"
+
+Exclude custom routes when `auto_route` is enabled.
+
 #### inet4_route_exclude_address
 #### inet4_route_exclude_address
 
 
+!!! failure "Deprecated in sing-box 1.10.0"
+
+   `inet4_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_exclude_address](#route_exclude_address) instead.
+
 Exclude custom routes when `auto_route` is enabled.
 Exclude custom routes when `auto_route` is enabled.
 
 
 #### inet6_route_exclude_address
 #### inet6_route_exclude_address
 
 
+!!! failure "Deprecated in sing-box 1.10.0"
+
+   `inet6_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_exclude_address](#route_exclude_address) instead.
+
 Exclude custom routes when `auto_route` is enabled.
 Exclude custom routes when `auto_route` is enabled.
 
 
+#### route_address_set
+
+!!! question "Since sing-box 1.10.0"
+
+!!! quote ""
+
+    Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled.
+
+Add the destination IP CIDR rules in the specified rule-sets to the firewall.
+Unmatched traffic will bypass the sing-box routes.
+
+Conflict with `route.default_mark` and `[dialOptions].routing_mark`.
+
+#### route_exclude_address_set
+
+!!! question "Since sing-box 1.10.0"
+
+!!! quote ""
+
+    Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled.
+
+Add the destination IP CIDR rules in the specified rule-sets to the firewall.
+Matched traffic will bypass the sing-box routes.
+
+Conflict with `route.default_mark` and `[dialOptions].routing_mark`.
+
 #### endpoint_independent_nat
 #### endpoint_independent_nat
 
 
 !!! info ""
 !!! info ""
@@ -214,6 +346,10 @@ Conflict with `exclude_interface`.
 
 
 #### exclude_interface
 #### exclude_interface
 
 
+!!! warning ""
+
+    When `strict_route` enabled, return traffic to excluded interfaces will not be automatically excluded, so add them as well (example: `br-lan` and `pppoe-wan`).
+
 Exclude interfaces in route.
 Exclude interfaces in route.
 
 
 Conflict with `include_interface`.
 Conflict with `include_interface`.

+ 143 - 4
docs/configuration/inbound/tun.zh.md

@@ -2,6 +2,21 @@
 icon: material/new-box
 icon: material/new-box
 ---
 ---
 
 
+!!! quote "Changes in sing-box 1.10.0"
+
+    :material-plus: [address](#address)  
+    :material-delete-clock: [inet4_address](#inet4_address)  
+    :material-delete-clock: [inet6_address](#inet6_address)  
+    :material-plus: [route_address](#route_address)  
+    :material-delete-clock: [inet4_route_address](#inet4_route_address)  
+    :material-delete-clock: [inet6_route_address](#inet6_route_address)  
+    :material-plus: [route_exclude_address](#route_address)  
+    :material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address)  
+    :material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address)  
+    :material-plus: [auto_redirect](#auto_redirect)  
+    :material-plus: [route_address_set](#route_address_set)  
+    :material-plus: [route_exclude_address_set](#route_address_set)
+
 !!! quote "sing-box 1.9.0 中的更改"
 !!! quote "sing-box 1.9.0 中的更改"
 
 
     :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain)  
     :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain)  
@@ -23,26 +38,57 @@ icon: material/new-box
   "type": "tun",
   "type": "tun",
   "tag": "tun-in",
   "tag": "tun-in",
   "interface_name": "tun0",
   "interface_name": "tun0",
-  "inet4_address": "172.19.0.1/30",
-  "inet6_address": "fdfe:dcba:9876::1/126",
+  "address": [
+    "172.18.0.1/30",
+    "fdfe:dcba:9876::1/126"
+  ],
+  // 已弃用
+  "inet4_address": [
+    "172.19.0.1/30"
+  ],
+  // 已弃用
+  "inet6_address": [
+    "fdfe:dcba:9876::1/126"
+  ],
   "mtu": 9000,
   "mtu": 9000,
   "gso": false,
   "gso": false,
   "auto_route": true,
   "auto_route": true,
   "strict_route": true,
   "strict_route": true,
+  "auto_redirect": false,
+  "route_address": [
+    "0.0.0.0/1",
+    "128.0.0.0/1",
+    "::/1",
+    "8000::/1"
+  ],
+  // 已弃用
   "inet4_route_address": [
   "inet4_route_address": [
     "0.0.0.0/1",
     "0.0.0.0/1",
     "128.0.0.0/1"
     "128.0.0.0/1"
   ],
   ],
+  // 已弃用
   "inet6_route_address": [
   "inet6_route_address": [
     "::/1",
     "::/1",
     "8000::/1"
     "8000::/1"
   ],
   ],
+  "route_exclude_address": [
+    "192.168.0.0/16",
+    "fc00::/7"
+  ],
+  // 已弃用
   "inet4_route_exclude_address": [
   "inet4_route_exclude_address": [
     "192.168.0.0/16"
     "192.168.0.0/16"
   ],
   ],
+  // 已弃用
   "inet6_route_exclude_address": [
   "inet6_route_exclude_address": [
     "fc00::/7"
     "fc00::/7"
   ],
   ],
+  "route_address_set": [
+    "geoip-cloudflare"
+  ],
+  "route_exclude_address_set": [
+    "geoip-cn"
+  ],
   "endpoint_independent_nat": false,
   "endpoint_independent_nat": false,
   "udp_timeout": "5m",
   "udp_timeout": "5m",
   "stack": "system",
   "stack": "system",
@@ -102,14 +148,30 @@ icon: material/new-box
 
 
 虚拟设备名称,默认自动选择。
 虚拟设备名称,默认自动选择。
 
 
+#### address
+
+!!! question "自 sing-box 1.10.0 起"
+
+==必填==
+
+tun 接口的 IPv4 和 IPv6 前缀。
+
 #### inet4_address
 #### inet4_address
 
 
+!!! failure "已在 sing-box 1.10.0 废弃"
+
+    `inet4_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除.
+
 ==必填==
 ==必填==
 
 
 tun 接口的 IPv4 前缀。
 tun 接口的 IPv4 前缀。
 
 
 #### inet6_address
 #### inet6_address
 
 
+!!! failure "已在 sing-box 1.10.0 废弃"
+
+    `inet6_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除.
+
 tun 接口的 IPv6 前缀。
 tun 接口的 IPv6 前缀。
 
 
 #### mtu
 #### mtu
@@ -145,9 +207,10 @@ tun 接口的 IPv6 前缀。
 *在 Linux 中*:
 *在 Linux 中*:
 
 
 * 让不支持的网络无法到达
 * 让不支持的网络无法到达
+* 使 ICMP 流量路由到 tun 而不是上游接口
 * 将所有连接路由到 tun
 * 将所有连接路由到 tun
 
 
-它可以防止地址泄漏,并使 DNS 劫持在 Android 上工作。
+它可以防止 IP 地址泄漏,并使 DNS 劫持在 Android 上工作。
 
 
 *在 Windows 中*:
 *在 Windows 中*:
 
 
@@ -157,22 +220,94 @@ tun 接口的 IPv6 前缀。
 
 
 它可能会使某些应用程序(如 VirtualBox)在某些情况下无法正常工作。
 它可能会使某些应用程序(如 VirtualBox)在某些情况下无法正常工作。
 
 
+#### auto_redirect
+
+!!! question "自 sing-box 1.10.0 起"
+
+!!! quote ""
+
+    仅支持 Linux。
+
+自动配置 iptables 以重定向 TCP 连接。
+
+*在 Android 中*:
+
+仅转发本地 IPv4 连接。 要通过热点或中继共享您的 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。
+
+*在 Linux 中*:
+
+带有 `auto_redirect `的 `auto_route` 现在可以在路由器上按预期工作,**无需干预**。
+
+#### route_address
+
+!!! question "自 sing-box 1.10.0 起"
+
+设置到 Tun 的自定义路由。
+
 #### inet4_route_address
 #### inet4_route_address
 
 
+!!! failure "已在 sing-box 1.10.0 废弃"
+
+    `inet4_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除.
+
 启用 `auto_route` 时使用自定义路由而不是默认路由。
 启用 `auto_route` 时使用自定义路由而不是默认路由。
 
 
 #### inet6_route_address
 #### inet6_route_address
 
 
+!!! failure "已在 sing-box 1.10.0 废弃"
+
+    `inet6_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除.
+
 启用 `auto_route` 时使用自定义路由而不是默认路由。
 启用 `auto_route` 时使用自定义路由而不是默认路由。
 
 
+#### route_exclude_address
+
+!!! question "自 sing-box 1.10.0 起"
+
+设置到 Tun 的排除自定义路由。
+
 #### inet4_route_exclude_address
 #### inet4_route_exclude_address
 
 
+!!! failure "已在 sing-box 1.10.0 废弃"
+
+    `inet4_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除.
+
 启用 `auto_route` 时排除自定义路由。
 启用 `auto_route` 时排除自定义路由。
 
 
 #### inet6_route_exclude_address
 #### inet6_route_exclude_address
 
 
+!!! failure "已在 sing-box 1.10.0 废弃"
+
+    `inet6_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除.
+
 启用 `auto_route` 时排除自定义路由。
 启用 `auto_route` 时排除自定义路由。
 
 
+#### route_address_set
+
+!!! question "自 sing-box 1.10.0 起"
+
+!!! quote ""
+
+    仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。 
+
+将指定规则集中的目标 IP CIDR 规则添加到防火墙。
+不匹配的流量将绕过 sing-box 路由。
+
+与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。
+
+#### route_exclude_address_set
+
+!!! question "自 sing-box 1.10.0 起"
+
+!!! quote ""
+
+    仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。
+
+将指定规则集中的目标 IP CIDR 规则添加到防火墙。
+匹配的流量将绕过 sing-box 路由。
+
+与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。
+
 #### endpoint_independent_nat
 #### endpoint_independent_nat
 
 
 启用独立于端点的 NAT。
 启用独立于端点的 NAT。
@@ -211,6 +346,10 @@ TCP/IP 栈。
 
 
 #### exclude_interface
 #### exclude_interface
 
 
+!!! warning ""
+
+    当 `strict_route` 启用,到被排除接口的回程流量将不会被自动排除,因此也要添加它们(例:`br-lan` 与 `pppoe-wan`)。
+
 排除路由的接口。
 排除路由的接口。
 
 
 与 `include_interface` 冲突。
 与 `include_interface` 冲突。
@@ -284,7 +423,7 @@ TCP/IP 栈。
 
 
 !!! note ""
 !!! note ""
 
 
-  在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**.
+    在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**.
 
 
 绕过代理的主机名列表。
 绕过代理的主机名列表。
 
 

+ 8 - 0
docs/deprecated.md

@@ -6,6 +6,14 @@ icon: material/delete-alert
 
 
 ## 1.10.0
 ## 1.10.0
 
 
+#### TUN address fields are merged
+
+`inet4_address` and `inet6_address` are merged into `address`,
+`inet4_route_address` and `inet6_route_address` are merged into `route_address`,
+`inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`.
+
+Old fields are deprecated and will be removed in sing-box 1.11.0.
+
 #### Drop support for go1.18 and go1.19
 #### Drop support for go1.18 and go1.19
 
 
 Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile.
 Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile.

+ 8 - 0
docs/deprecated.zh.md

@@ -6,6 +6,14 @@ icon: material/delete-alert
 
 
 ## 1.10.0
 ## 1.10.0
 
 
+#### TUN 地址字段已合并
+
+`inet4_address` 和 `inet6_address` 已合并为 `address`,
+`inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`,
+`inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。
+
+旧字段已废弃,且将在 sing-box 1.11.0 中移除。
+
 #### 移除对 go1.18 和 go1.19 的支持
 #### 移除对 go1.18 和 go1.19 的支持
 
 
 由于维护困难,sing-box 1.10.0 要求至少 Go 1.20 才能编译。
 由于维护困难,sing-box 1.10.0 要求至少 Go 1.20 才能编译。

+ 68 - 0
docs/migration.md

@@ -2,6 +2,74 @@
 icon: material/arrange-bring-forward
 icon: material/arrange-bring-forward
 ---
 ---
 
 
+## 1.10.0
+
+### TUN address fields are merged
+
+`inet4_address` and `inet6_address` are merged into `address`,
+`inet4_route_address` and `inet6_route_address` are merged into `route_address`,
+`inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`.
+
+Old fields are deprecated and will be removed in sing-box 1.11.0.
+
+!!! info "References"
+
+    [TUN](/configuration/inbound/tun/)
+
+=== ":material-card-remove: Deprecated"
+
+    ```json
+    {
+      "inbounds": [
+        {
+          "type": "tun",
+          "inet4_address": "172.19.0.1/30",
+          "inet6_address": "fdfe:dcba:9876::1/126",
+          "inet4_route_address": [
+            "0.0.0.0/1",
+            "128.0.0.0/1"
+          ],
+          "inet6_route_address": [
+            "::/1",
+            "8000::/1"
+          ],
+          "inet4_route_exclude_address": [
+            "192.168.0.0/16"
+          ],
+          "inet6_route_exclude_address": [
+            "fc00::/7"
+          ]
+        }
+      ]
+    }
+    ```
+
+=== ":material-card-multiple: New"
+
+    ```json
+    {
+      "inbounds": [
+        {
+          "type": "tun",
+          "address": [
+            "172.19.0.1/30",
+            "fdfe:dcba:9876::1/126"
+          ],
+          "route_address": [
+            "0.0.0.0/1",
+            "128.0.0.0/1",
+            "::/1",
+            "8000::/1"
+          ],
+          "route_exclude_address": [
+            "192.168.0.0/16",
+            "fc00::/7"
+          ]
+        }
+      ]
+    }
+    ```
+
 ## 1.9.5
 ## 1.9.5
 
 
 ### Bundle Identifier updates in Apple platform clients
 ### Bundle Identifier updates in Apple platform clients

+ 68 - 0
docs/migration.zh.md

@@ -2,6 +2,74 @@
 icon: material/arrange-bring-forward
 icon: material/arrange-bring-forward
 ---
 ---
 
 
+## 1.10.0
+
+### TUN 地址字段已合并
+
+`inet4_address` 和 `inet6_address` 已合并为 `address`,
+`inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`,
+`inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。
+
+旧字段已废弃,且将在 sing-box 1.11.0 中移除。
+
+!!! info "参考"
+
+    [TUN](/zh/configuration/inbound/tun/)
+
+=== ":material-card-remove: 弃用的"
+
+    ```json
+    {
+      "inbounds": [
+        {
+          "type": "tun",
+          "inet4_address": "172.19.0.1/30",
+          "inet6_address": "fdfe:dcba:9876::1/126",
+          "inet4_route_address": [
+            "0.0.0.0/1",
+            "128.0.0.0/1"
+          ],
+          "inet6_route_address": [
+            "::/1",
+            "8000::/1"
+          ],
+          "inet4_route_exclude_address": [
+            "192.168.0.0/16"
+          ],
+          "inet6_route_exclude_address": [
+            "fc00::/7"
+          ]
+        }
+      ]
+    }
+    ```
+
+=== ":material-card-multiple: 新的"
+
+    ```json
+    {
+      "inbounds": [
+        {
+          "type": "tun",
+          "address": [
+            "172.19.0.1/30",
+            "fdfe:dcba:9876::1/126"
+          ],
+          "route_address": [
+            "0.0.0.0/1",
+            "128.0.0.0/1",
+            "::/1",
+            "8000::/1"
+          ],
+          "route_exclude_address": [
+            "192.168.0.0/16",
+            "fc00::/7"
+          ]
+        }
+      ]
+    }
+    ```
+
 ## 1.9.5
 ## 1.9.5
 
 
 ### Apple 平台客户端的 Bundle Identifier 更新
 ### Apple 平台客户端的 Bundle Identifier 更新

+ 12 - 7
go.mod

@@ -34,7 +34,7 @@ require (
 	github.com/sagernet/sing-shadowsocks v0.2.7
 	github.com/sagernet/sing-shadowsocks v0.2.7
 	github.com/sagernet/sing-shadowsocks2 v0.2.0
 	github.com/sagernet/sing-shadowsocks2 v0.2.0
 	github.com/sagernet/sing-shadowtls v0.1.4
 	github.com/sagernet/sing-shadowtls v0.1.4
-	github.com/sagernet/sing-tun v0.3.3
+	github.com/sagernet/sing-tun v0.4.0-beta.16
 	github.com/sagernet/sing-vmess v0.1.12
 	github.com/sagernet/sing-vmess v0.1.12
 	github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
 	github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
 	github.com/sagernet/utls v1.5.4
 	github.com/sagernet/utls v1.5.4
@@ -44,8 +44,8 @@ require (
 	github.com/stretchr/testify v1.9.0
 	github.com/stretchr/testify v1.9.0
 	go.uber.org/zap v1.27.0
 	go.uber.org/zap v1.27.0
 	go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
 	go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
-	golang.org/x/crypto v0.23.0
-	golang.org/x/net v0.25.0
+	golang.org/x/crypto v0.24.0
+	golang.org/x/net v0.26.0
 	golang.org/x/sys v0.25.0
 	golang.org/x/sys v0.25.0
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
 	google.golang.org/grpc v1.63.2
 	google.golang.org/grpc v1.63.2
@@ -65,6 +65,7 @@ require (
 	github.com/gobwas/httphead v0.1.0 // indirect
 	github.com/gobwas/httphead v0.1.0 // indirect
 	github.com/gobwas/pool v0.2.1 // indirect
 	github.com/gobwas/pool v0.2.1 // indirect
 	github.com/google/btree v1.1.2 // indirect
 	github.com/google/btree v1.1.2 // indirect
+	github.com/google/go-cmp v0.6.0 // indirect
 	github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
 	github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
 	github.com/hashicorp/yamux v0.1.1 // indirect
 	github.com/hashicorp/yamux v0.1.1 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -72,24 +73,28 @@ require (
 	github.com/klauspost/compress v1.17.4 // indirect
 	github.com/klauspost/compress v1.17.4 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.5 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.5 // indirect
 	github.com/libdns/libdns v0.2.2 // indirect
 	github.com/libdns/libdns v0.2.2 // indirect
+	github.com/mdlayher/netlink v1.7.2 // indirect
+	github.com/mdlayher/socket v0.4.1 // indirect
 	github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
 	github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
 	github.com/onsi/ginkgo/v2 v2.9.7 // indirect
 	github.com/onsi/ginkgo/v2 v2.9.7 // indirect
 	github.com/pierrec/lz4/v4 v4.1.14 // indirect
 	github.com/pierrec/lz4/v4 v4.1.14 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/quic-go/qpack v0.4.0 // indirect
 	github.com/quic-go/qpack v0.4.0 // indirect
 	github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
 	github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
-	github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba // indirect
+	github.com/sagernet/fswatch v0.1.1 // indirect
+	github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
+	github.com/sagernet/nftables v0.3.0-beta.4 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
 	github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
-	github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
+	github.com/vishvananda/netns v0.0.4 // indirect
 	github.com/zeebo/blake3 v0.2.3 // indirect
 	github.com/zeebo/blake3 v0.2.3 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
+	golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
 	golang.org/x/mod v0.18.0 // indirect
 	golang.org/x/mod v0.18.0 // indirect
 	golang.org/x/sync v0.8.0 // indirect
 	golang.org/x/sync v0.8.0 // indirect
 	golang.org/x/text v0.18.0 // indirect
 	golang.org/x/text v0.18.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
-	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
+	golang.org/x/tools v0.22.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
 	gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
 	gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect

+ 24 - 16
go.sum

@@ -40,6 +40,7 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
 github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
 github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
 github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
 github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
 github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
 github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
 github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
 github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
 github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
@@ -69,6 +70,10 @@ github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
 github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
 github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
 github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
 github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
 github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
 github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
+github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
+github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
+github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
+github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
 github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d h1:j9LtzkYstLFoNvXW824QQeN7Y26uPL5249kzWKbzO9U=
 github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d h1:j9LtzkYstLFoNvXW824QQeN7Y26uPL5249kzWKbzO9U=
 github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts=
 github.com/metacubex/tfo-go v0.0.0-20240821025650-e9be0afd5e7d/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts=
 github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
 github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
@@ -97,12 +102,16 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk
 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
 github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 h1:YbmpqPQEMdlk9oFSKYWRqVuu9qzNiOayIonKmv1gCXY=
 github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 h1:YbmpqPQEMdlk9oFSKYWRqVuu9qzNiOayIonKmv1gCXY=
 github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1/go.mod h1:J2yAxTFPDjrDPhuAi9aWFz2L3ox9it4qAluBBbN0H5k=
 github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1/go.mod h1:J2yAxTFPDjrDPhuAi9aWFz2L3ox9it4qAluBBbN0H5k=
+github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
+github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
 github.com/sagernet/gomobile v0.1.4 h1:WzX9ka+iHdupMgy2Vdich+OAt7TM8C2cZbIbzNjBrJY=
 github.com/sagernet/gomobile v0.1.4 h1:WzX9ka+iHdupMgy2Vdich+OAt7TM8C2cZbIbzNjBrJY=
 github.com/sagernet/gomobile v0.1.4/go.mod h1:Pqq2+ZVvs10U7xK+UwJgwYWUykewi8H6vlslAO73n9E=
 github.com/sagernet/gomobile v0.1.4/go.mod h1:Pqq2+ZVvs10U7xK+UwJgwYWUykewi8H6vlslAO73n9E=
 github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f h1:NkhuupzH5ch7b/Y/6ZHJWrnNLoiNnSJaow6DPb8VW2I=
 github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f h1:NkhuupzH5ch7b/Y/6ZHJWrnNLoiNnSJaow6DPb8VW2I=
 github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f/go.mod h1:KXmw+ouSJNOsuRpg4wgwwCQuunrGz4yoAqQjsLjc6N0=
 github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f/go.mod h1:KXmw+ouSJNOsuRpg4wgwwCQuunrGz4yoAqQjsLjc6N0=
-github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba h1:EY5AS7CCtfmARNv2zXUOrsEMPFDGYxaw65JzA2p51Vk=
-github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
+github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
+github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
+github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
+github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
 github.com/sagernet/quic-go v0.47.0-beta.2 h1:1tCGWFOSaXIeuQaHrwOMJIYvlupjTcaVInGQw5ArULU=
 github.com/sagernet/quic-go v0.47.0-beta.2 h1:1tCGWFOSaXIeuQaHrwOMJIYvlupjTcaVInGQw5ArULU=
 github.com/sagernet/quic-go v0.47.0-beta.2/go.mod h1:bLVKvElSEMNv7pu7SZHscW02TYigzQ5lQu3Nh4wNh8Q=
 github.com/sagernet/quic-go v0.47.0-beta.2/go.mod h1:bLVKvElSEMNv7pu7SZHscW02TYigzQ5lQu3Nh4wNh8Q=
 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
@@ -122,8 +131,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wK
 github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
 github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
 github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
 github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
 github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
 github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
-github.com/sagernet/sing-tun v0.3.3 h1:LZnQNmfGcNG2KPTPkLgc+Lo7k606QJVkPp2DnjriwUk=
-github.com/sagernet/sing-tun v0.3.3/go.mod h1:DxLIyhjWU/HwGYoX0vNGg2c5QgTQIakphU1MuERR5tQ=
+github.com/sagernet/sing-tun v0.4.0-beta.16 h1:05VdL5BZiKLQsDNrpdXMseSO1NwPfl9Y4o76PqAd9sY=
+github.com/sagernet/sing-tun v0.4.0-beta.16/go.mod h1:81JwnnYw8X9W9XvmZetSTTiPgIE3SbAbnc+EHKwPJ5U=
 github.com/sagernet/sing-vmess v0.1.12 h1:2gFD8JJb+eTFMoa8FIVMnknEi+vCSfaiTXTfEYAYAPg=
 github.com/sagernet/sing-vmess v0.1.12 h1:2gFD8JJb+eTFMoa8FIVMnknEi+vCSfaiTXTfEYAYAPg=
 github.com/sagernet/sing-vmess v0.1.12/go.mod h1:luTSsfyBGAc9VhtCqwjR+dt1QgqBhuYBCONB/POhF8I=
 github.com/sagernet/sing-vmess v0.1.12/go.mod h1:luTSsfyBGAc9VhtCqwjR+dt1QgqBhuYBCONB/POhF8I=
 github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
 github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
@@ -146,8 +155,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA=
 github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA=
 github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
 github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
-github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
-github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
+github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
+github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
 github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
 github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
 github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
 github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
 github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
 github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
@@ -163,20 +172,19 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
 golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
 golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
-golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
-golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
-golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
-golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
+golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
+golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
+golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
+golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
 golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
 golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
 golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
-golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
+golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
 golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -187,7 +195,7 @@ golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
 golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
 golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
+golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
 golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
@@ -195,8 +203,8 @@ golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
 golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
 golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
+golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=

+ 216 - 27
inbound/tun.go

@@ -3,6 +3,9 @@ package inbound
 import (
 import (
 	"context"
 	"context"
 	"net"
 	"net"
+	"net/netip"
+	"os"
+	"runtime"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"time"
 	"time"
@@ -19,27 +22,91 @@ import (
 	M "github.com/sagernet/sing/common/metadata"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/common/ranges"
 	"github.com/sagernet/sing/common/ranges"
+	"github.com/sagernet/sing/common/x/list"
+
+	"go4.org/netipx"
 )
 )
 
 
 var _ adapter.Inbound = (*Tun)(nil)
 var _ adapter.Inbound = (*Tun)(nil)
 
 
 type Tun struct {
 type Tun struct {
-	tag                    string
-	ctx                    context.Context
-	router                 adapter.Router
-	logger                 log.ContextLogger
-	inboundOptions         option.InboundOptions
-	tunOptions             tun.Options
-	endpointIndependentNat bool
-	udpTimeout             int64
-	stack                  string
-	tunIf                  tun.Tun
-	tunStack               tun.Stack
-	platformInterface      platform.Interface
-	platformOptions        option.TunPlatformOptions
+	tag                         string
+	ctx                         context.Context
+	router                      adapter.Router
+	logger                      log.ContextLogger
+	inboundOptions              option.InboundOptions
+	tunOptions                  tun.Options
+	endpointIndependentNat      bool
+	udpTimeout                  int64
+	stack                       string
+	tunIf                       tun.Tun
+	tunStack                    tun.Stack
+	platformInterface           platform.Interface
+	platformOptions             option.TunPlatformOptions
+	autoRedirect                tun.AutoRedirect
+	routeRuleSet                []adapter.RuleSet
+	routeRuleSetCallback        []*list.Element[adapter.RuleSetUpdateCallback]
+	routeExcludeRuleSet         []adapter.RuleSet
+	routeExcludeRuleSetCallback []*list.Element[adapter.RuleSetUpdateCallback]
+	routeAddressSet             []*netipx.IPSet
+	routeExcludeAddressSet      []*netipx.IPSet
 }
 }
 
 
 func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*Tun, error) {
 func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*Tun, error) {
+	address := options.Address
+	//nolint:staticcheck
+	//goland:noinspection GoDeprecation
+	if len(options.Inet4Address) > 0 {
+		address = append(address, options.Inet4Address...)
+	}
+	//nolint:staticcheck
+	//goland:noinspection GoDeprecation
+	if len(options.Inet6Address) > 0 {
+		address = append(address, options.Inet6Address...)
+	}
+	inet4Address := common.Filter(address, func(it netip.Prefix) bool {
+		return it.Addr().Is4()
+	})
+	inet6Address := common.Filter(address, func(it netip.Prefix) bool {
+		return it.Addr().Is6()
+	})
+
+	routeAddress := options.RouteAddress
+	//nolint:staticcheck
+	//goland:noinspection GoDeprecation
+	if len(options.Inet4RouteAddress) > 0 {
+		routeAddress = append(routeAddress, options.Inet4RouteAddress...)
+	}
+	//nolint:staticcheck
+	//goland:noinspection GoDeprecation
+	if len(options.Inet6RouteAddress) > 0 {
+		routeAddress = append(routeAddress, options.Inet6RouteAddress...)
+	}
+	inet4RouteAddress := common.Filter(routeAddress, func(it netip.Prefix) bool {
+		return it.Addr().Is4()
+	})
+	inet6RouteAddress := common.Filter(routeAddress, func(it netip.Prefix) bool {
+		return it.Addr().Is6()
+	})
+
+	routeExcludeAddress := options.RouteExcludeAddress
+	//nolint:staticcheck
+	//goland:noinspection GoDeprecation
+	if len(options.Inet4RouteExcludeAddress) > 0 {
+		routeExcludeAddress = append(routeExcludeAddress, options.Inet4RouteExcludeAddress...)
+	}
+	//nolint:staticcheck
+	//goland:noinspection GoDeprecation
+	if len(options.Inet6RouteExcludeAddress) > 0 {
+		routeExcludeAddress = append(routeExcludeAddress, options.Inet6RouteExcludeAddress...)
+	}
+	inet4RouteExcludeAddress := common.Filter(routeExcludeAddress, func(it netip.Prefix) bool {
+		return it.Addr().Is4()
+	})
+	inet6RouteExcludeAddress := common.Filter(routeExcludeAddress, func(it netip.Prefix) bool {
+		return it.Addr().Is6()
+	})
+
 	tunMTU := options.MTU
 	tunMTU := options.MTU
 	if tunMTU == 0 {
 	if tunMTU == 0 {
 		tunMTU = 9000
 		tunMTU = 9000
@@ -50,9 +117,9 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
 	} else {
 	} else {
 		udpTimeout = C.UDPTimeout
 		udpTimeout = C.UDPTimeout
 	}
 	}
+	var err error
 	includeUID := uidToRange(options.IncludeUID)
 	includeUID := uidToRange(options.IncludeUID)
 	if len(options.IncludeUIDRange) > 0 {
 	if len(options.IncludeUIDRange) > 0 {
-		var err error
 		includeUID, err = parseRange(includeUID, options.IncludeUIDRange)
 		includeUID, err = parseRange(includeUID, options.IncludeUIDRange)
 		if err != nil {
 		if err != nil {
 			return nil, E.Cause(err, "parse include_uid_range")
 			return nil, E.Cause(err, "parse include_uid_range")
@@ -60,13 +127,30 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
 	}
 	}
 	excludeUID := uidToRange(options.ExcludeUID)
 	excludeUID := uidToRange(options.ExcludeUID)
 	if len(options.ExcludeUIDRange) > 0 {
 	if len(options.ExcludeUIDRange) > 0 {
-		var err error
 		excludeUID, err = parseRange(excludeUID, options.ExcludeUIDRange)
 		excludeUID, err = parseRange(excludeUID, options.ExcludeUIDRange)
 		if err != nil {
 		if err != nil {
 			return nil, E.Cause(err, "parse exclude_uid_range")
 			return nil, E.Cause(err, "parse exclude_uid_range")
 		}
 		}
 	}
 	}
-	return &Tun{
+
+	tableIndex := options.IPRoute2TableIndex
+	if tableIndex == 0 {
+		tableIndex = tun.DefaultIPRoute2TableIndex
+	}
+	ruleIndex := options.IPRoute2RuleIndex
+	if ruleIndex == 0 {
+		ruleIndex = tun.DefaultIPRoute2RuleIndex
+	}
+	inputMark := options.AutoRedirectInputMark
+	if inputMark == 0 {
+		inputMark = tun.DefaultAutoRedirectInputMark
+	}
+	outputMark := options.AutoRedirectOutputMark
+	if outputMark == 0 {
+		outputMark = tun.DefaultAutoRedirectOutputMark
+	}
+
+	inbound := &Tun{
 		tag:            tag,
 		tag:            tag,
 		ctx:            ctx,
 		ctx:            ctx,
 		router:         router,
 		router:         router,
@@ -76,30 +160,83 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
 			Name:                     options.InterfaceName,
 			Name:                     options.InterfaceName,
 			MTU:                      tunMTU,
 			MTU:                      tunMTU,
 			GSO:                      options.GSO,
 			GSO:                      options.GSO,
-			Inet4Address:             options.Inet4Address,
-			Inet6Address:             options.Inet6Address,
+			Inet4Address:             inet4Address,
+			Inet6Address:             inet6Address,
 			AutoRoute:                options.AutoRoute,
 			AutoRoute:                options.AutoRoute,
+			IPRoute2TableIndex:       tableIndex,
+			IPRoute2RuleIndex:        ruleIndex,
+			AutoRedirectInputMark:    inputMark,
+			AutoRedirectOutputMark:   outputMark,
 			StrictRoute:              options.StrictRoute,
 			StrictRoute:              options.StrictRoute,
 			IncludeInterface:         options.IncludeInterface,
 			IncludeInterface:         options.IncludeInterface,
 			ExcludeInterface:         options.ExcludeInterface,
 			ExcludeInterface:         options.ExcludeInterface,
-			Inet4RouteAddress:        options.Inet4RouteAddress,
-			Inet6RouteAddress:        options.Inet6RouteAddress,
-			Inet4RouteExcludeAddress: options.Inet4RouteExcludeAddress,
-			Inet6RouteExcludeAddress: options.Inet6RouteExcludeAddress,
+			Inet4RouteAddress:        inet4RouteAddress,
+			Inet6RouteAddress:        inet6RouteAddress,
+			Inet4RouteExcludeAddress: inet4RouteExcludeAddress,
+			Inet6RouteExcludeAddress: inet6RouteExcludeAddress,
 			IncludeUID:               includeUID,
 			IncludeUID:               includeUID,
 			ExcludeUID:               excludeUID,
 			ExcludeUID:               excludeUID,
 			IncludeAndroidUser:       options.IncludeAndroidUser,
 			IncludeAndroidUser:       options.IncludeAndroidUser,
 			IncludePackage:           options.IncludePackage,
 			IncludePackage:           options.IncludePackage,
 			ExcludePackage:           options.ExcludePackage,
 			ExcludePackage:           options.ExcludePackage,
 			InterfaceMonitor:         router.InterfaceMonitor(),
 			InterfaceMonitor:         router.InterfaceMonitor(),
-			TableIndex:               2022,
 		},
 		},
 		endpointIndependentNat: options.EndpointIndependentNat,
 		endpointIndependentNat: options.EndpointIndependentNat,
 		udpTimeout:             int64(udpTimeout.Seconds()),
 		udpTimeout:             int64(udpTimeout.Seconds()),
 		stack:                  options.Stack,
 		stack:                  options.Stack,
 		platformInterface:      platformInterface,
 		platformInterface:      platformInterface,
 		platformOptions:        common.PtrValueOrDefault(options.Platform),
 		platformOptions:        common.PtrValueOrDefault(options.Platform),
-	}, nil
+	}
+	if options.AutoRedirect {
+		if !options.AutoRoute {
+			return nil, E.New("`auto_route` is required by `auto_redirect`")
+		}
+		disableNFTables, dErr := strconv.ParseBool(os.Getenv("DISABLE_NFTABLES"))
+		inbound.autoRedirect, err = tun.NewAutoRedirect(tun.AutoRedirectOptions{
+			TunOptions:             &inbound.tunOptions,
+			Context:                ctx,
+			Handler:                inbound,
+			Logger:                 logger,
+			NetworkMonitor:         router.NetworkMonitor(),
+			InterfaceFinder:        router.InterfaceFinder(),
+			TableName:              "sing-box",
+			DisableNFTables:        dErr == nil && disableNFTables,
+			RouteAddressSet:        &inbound.routeAddressSet,
+			RouteExcludeAddressSet: &inbound.routeExcludeAddressSet,
+		})
+		if err != nil {
+			return nil, E.Cause(err, "initialize auto-redirect")
+		}
+		if runtime.GOOS != "android" {
+			var markMode bool
+			for _, routeAddressSet := range options.RouteAddressSet {
+				ruleSet, loaded := router.RuleSet(routeAddressSet)
+				if !loaded {
+					return nil, E.New("parse route_address_set: rule-set not found: ", routeAddressSet)
+				}
+				ruleSet.IncRef()
+				inbound.routeRuleSet = append(inbound.routeRuleSet, ruleSet)
+				markMode = true
+			}
+			for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet {
+				ruleSet, loaded := router.RuleSet(routeExcludeAddressSet)
+				if !loaded {
+					return nil, E.New("parse route_exclude_address_set: rule-set not found: ", routeExcludeAddressSet)
+				}
+				ruleSet.IncRef()
+				inbound.routeExcludeRuleSet = append(inbound.routeExcludeRuleSet, ruleSet)
+				markMode = true
+			}
+			if markMode {
+				inbound.tunOptions.AutoRedirectMarkMode = true
+				err = router.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark)
+				if err != nil {
+					return nil, err
+				}
+			}
+		}
+	}
+	return inbound, nil
 }
 }
 
 
 func uidToRange(uidList option.Listable[uint32]) []ranges.Range[uint32] {
 func uidToRange(uidList option.Listable[uint32]) []ranges.Range[uint32] {
@@ -121,11 +258,11 @@ func parseRange(uidRanges []ranges.Range[uint32], rangeList []string) ([]ranges.
 		}
 		}
 		var start, end uint64
 		var start, end uint64
 		var err error
 		var err error
-		start, err = strconv.ParseUint(uidRange[:subIndex], 10, 32)
+		start, err = strconv.ParseUint(uidRange[:subIndex], 0, 32)
 		if err != nil {
 		if err != nil {
 			return nil, E.Cause(err, "parse range start")
 			return nil, E.Cause(err, "parse range start")
 		}
 		}
-		end, err = strconv.ParseUint(uidRange[subIndex+1:], 10, 32)
+		end, err = strconv.ParseUint(uidRange[subIndex+1:], 0, 32)
 		if err != nil {
 		if err != nil {
 			return nil, E.Cause(err, "parse range end")
 			return nil, E.Cause(err, "parse range end")
 		}
 		}
@@ -200,10 +337,58 @@ func (t *Tun) Start() error {
 	return nil
 	return nil
 }
 }
 
 
+func (t *Tun) PostStart() error {
+	monitor := taskmonitor.New(t.logger, C.StartTimeout)
+	if t.autoRedirect != nil {
+		t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet)
+		for _, routeRuleSet := range t.routeRuleSet {
+			ipSets := routeRuleSet.ExtractIPSet()
+			if len(ipSets) == 0 {
+				t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeRuleSet.Name())
+			}
+			t.routeAddressSet = append(t.routeAddressSet, ipSets...)
+		}
+		t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet)
+		for _, routeExcludeRuleSet := range t.routeExcludeRuleSet {
+			ipSets := routeExcludeRuleSet.ExtractIPSet()
+			if len(ipSets) == 0 {
+				t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeExcludeRuleSet.Name())
+			}
+			t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ipSets...)
+		}
+		monitor.Start("initialize auto-redirect")
+		err := t.autoRedirect.Start()
+		monitor.Finish()
+		if err != nil {
+			return E.Cause(err, "auto-redirect")
+		}
+		for _, routeRuleSet := range t.routeRuleSet {
+			t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet))
+			routeRuleSet.DecRef()
+		}
+		for _, routeExcludeRuleSet := range t.routeExcludeRuleSet {
+			t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet))
+			routeExcludeRuleSet.DecRef()
+		}
+		t.routeAddressSet = nil
+		t.routeExcludeAddressSet = nil
+	}
+	return nil
+}
+
+func (t *Tun) updateRouteAddressSet(it adapter.RuleSet) {
+	t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet)
+	t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet)
+	t.autoRedirect.UpdateRouteAddressSet()
+	t.routeAddressSet = nil
+	t.routeExcludeAddressSet = nil
+}
+
 func (t *Tun) Close() error {
 func (t *Tun) Close() error {
 	return common.Close(
 	return common.Close(
 		t.tunStack,
 		t.tunStack,
 		t.tunIf,
 		t.tunIf,
+		t.autoRedirect,
 	)
 	)
 }
 }
 
 
@@ -215,7 +400,11 @@ func (t *Tun) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata
 	metadata.Source = upstreamMetadata.Source
 	metadata.Source = upstreamMetadata.Source
 	metadata.Destination = upstreamMetadata.Destination
 	metadata.Destination = upstreamMetadata.Destination
 	metadata.InboundOptions = t.inboundOptions
 	metadata.InboundOptions = t.inboundOptions
-	t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
+	if upstreamMetadata.Protocol != "" {
+		t.logger.InfoContext(ctx, "inbound ", upstreamMetadata.Protocol, " connection from ", metadata.Source)
+	} else {
+		t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
+	}
 	t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
 	t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
 	err := t.router.RouteConnection(ctx, conn, metadata)
 	err := t.router.RouteConnection(ctx, conn, metadata)
 	if err != nil {
 	if err != nil {

+ 40 - 23
option/tun.go

@@ -3,29 +3,46 @@ package option
 import "net/netip"
 import "net/netip"
 
 
 type TunInboundOptions struct {
 type TunInboundOptions struct {
-	InterfaceName            string                 `json:"interface_name,omitempty"`
-	MTU                      uint32                 `json:"mtu,omitempty"`
-	GSO                      bool                   `json:"gso,omitempty"`
-	Inet4Address             Listable[netip.Prefix] `json:"inet4_address,omitempty"`
-	Inet6Address             Listable[netip.Prefix] `json:"inet6_address,omitempty"`
-	AutoRoute                bool                   `json:"auto_route,omitempty"`
-	StrictRoute              bool                   `json:"strict_route,omitempty"`
-	Inet4RouteAddress        Listable[netip.Prefix] `json:"inet4_route_address,omitempty"`
-	Inet6RouteAddress        Listable[netip.Prefix] `json:"inet6_route_address,omitempty"`
+	InterfaceName          string                 `json:"interface_name,omitempty"`
+	MTU                    uint32                 `json:"mtu,omitempty"`
+	GSO                    bool                   `json:"gso,omitempty"`
+	Address                Listable[netip.Prefix] `json:"address,omitempty"`
+	AutoRoute              bool                   `json:"auto_route,omitempty"`
+	IPRoute2TableIndex     int                    `json:"iproute2_table_index,omitempty"`
+	IPRoute2RuleIndex      int                    `json:"iproute2_rule_index,omitempty"`
+	AutoRedirect           bool                   `json:"auto_redirect,omitempty"`
+	AutoRedirectInputMark  uint32                 `json:"auto_redirect_input_mark,omitempty"`
+	AutoRedirectOutputMark uint32                 `json:"auto_redirect_output_mark,omitempty"`
+	StrictRoute            bool                   `json:"strict_route,omitempty"`
+	RouteAddress           Listable[netip.Prefix] `json:"route_address,omitempty"`
+	RouteAddressSet        Listable[string]       `json:"route_address_set,omitempty"`
+	RouteExcludeAddress    Listable[netip.Prefix] `json:"route_exclude_address,omitempty"`
+	RouteExcludeAddressSet Listable[string]       `json:"route_exclude_address_set,omitempty"`
+	IncludeInterface       Listable[string]       `json:"include_interface,omitempty"`
+	ExcludeInterface       Listable[string]       `json:"exclude_interface,omitempty"`
+	IncludeUID             Listable[uint32]       `json:"include_uid,omitempty"`
+	IncludeUIDRange        Listable[string]       `json:"include_uid_range,omitempty"`
+	ExcludeUID             Listable[uint32]       `json:"exclude_uid,omitempty"`
+	ExcludeUIDRange        Listable[string]       `json:"exclude_uid_range,omitempty"`
+	IncludeAndroidUser     Listable[int]          `json:"include_android_user,omitempty"`
+	IncludePackage         Listable[string]       `json:"include_package,omitempty"`
+	ExcludePackage         Listable[string]       `json:"exclude_package,omitempty"`
+	EndpointIndependentNat bool                   `json:"endpoint_independent_nat,omitempty"`
+	UDPTimeout             UDPTimeoutCompat       `json:"udp_timeout,omitempty"`
+	Stack                  string                 `json:"stack,omitempty"`
+	Platform               *TunPlatformOptions    `json:"platform,omitempty"`
+	InboundOptions
+
+	// Deprecated: merged to Address
+	Inet4Address Listable[netip.Prefix] `json:"inet4_address,omitempty"`
+	// Deprecated: merged to Address
+	Inet6Address Listable[netip.Prefix] `json:"inet6_address,omitempty"`
+	// Deprecated: merged to RouteAddress
+	Inet4RouteAddress Listable[netip.Prefix] `json:"inet4_route_address,omitempty"`
+	// Deprecated: merged to RouteAddress
+	Inet6RouteAddress Listable[netip.Prefix] `json:"inet6_route_address,omitempty"`
+	// Deprecated: merged to RouteExcludeAddress
 	Inet4RouteExcludeAddress Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty"`
 	Inet4RouteExcludeAddress Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty"`
+	// Deprecated: merged to RouteExcludeAddress
 	Inet6RouteExcludeAddress Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"`
 	Inet6RouteExcludeAddress Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"`
-	IncludeInterface         Listable[string]       `json:"include_interface,omitempty"`
-	ExcludeInterface         Listable[string]       `json:"exclude_interface,omitempty"`
-	IncludeUID               Listable[uint32]       `json:"include_uid,omitempty"`
-	IncludeUIDRange          Listable[string]       `json:"include_uid_range,omitempty"`
-	ExcludeUID               Listable[uint32]       `json:"exclude_uid,omitempty"`
-	ExcludeUIDRange          Listable[string]       `json:"exclude_uid_range,omitempty"`
-	IncludeAndroidUser       Listable[int]          `json:"include_android_user,omitempty"`
-	IncludePackage           Listable[string]       `json:"include_package,omitempty"`
-	ExcludePackage           Listable[string]       `json:"exclude_package,omitempty"`
-	EndpointIndependentNat   bool                   `json:"endpoint_independent_nat,omitempty"`
-	UDPTimeout               UDPTimeoutCompat       `json:"udp_timeout,omitempty"`
-	Stack                    string                 `json:"stack,omitempty"`
-	Platform                 *TunPlatformOptions    `json:"platform,omitempty"`
-	InboundOptions
 }
 }

+ 42 - 10
route/router.go

@@ -83,6 +83,7 @@ type Router struct {
 	autoDetectInterface                bool
 	autoDetectInterface                bool
 	defaultInterface                   string
 	defaultInterface                   string
 	defaultMark                        uint32
 	defaultMark                        uint32
+	autoRedirectOutputMark             uint32
 	networkMonitor                     tun.NetworkUpdateMonitor
 	networkMonitor                     tun.NetworkUpdateMonitor
 	interfaceMonitor                   tun.DefaultInterfaceMonitor
 	interfaceMonitor                   tun.DefaultInterfaceMonitor
 	packageManager                     tun.PackageManager
 	packageManager                     tun.PackageManager
@@ -533,7 +534,10 @@ func (r *Router) Start() error {
 
 
 	if C.IsAndroid && r.platformInterface == nil {
 	if C.IsAndroid && r.platformInterface == nil {
 		monitor.Start("initialize package manager")
 		monitor.Start("initialize package manager")
-		packageManager, err := tun.NewPackageManager(r)
+		packageManager, err := tun.NewPackageManager(tun.PackageManagerOptions{
+			Callback: r,
+			Logger:   r.logger,
+		})
 		monitor.Finish()
 		monitor.Finish()
 		if err != nil {
 		if err != nil {
 			return E.Cause(err, "create package manager")
 			return E.Cause(err, "create package manager")
@@ -736,10 +740,26 @@ func (r *Router) PostStart() error {
 			return E.Cause(err, "initialize rule[", i, "]")
 			return E.Cause(err, "initialize rule[", i, "]")
 		}
 		}
 	}
 	}
+	for _, ruleSet := range r.ruleSets {
+		monitor.Start("post start rule_set[", ruleSet.Name(), "]")
+		err := ruleSet.PostStart()
+		monitor.Finish()
+		if err != nil {
+			return E.Cause(err, "post start rule_set[", ruleSet.Name(), "]")
+		}
+	}
 	r.started = true
 	r.started = true
 	return nil
 	return nil
 }
 }
 
 
+func (r *Router) Cleanup() error {
+	for _, ruleSet := range r.ruleSetMap {
+		ruleSet.Cleanup()
+	}
+	runtime.GC()
+	return nil
+}
+
 func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
 func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
 	outbound, loaded := r.outboundByTag[tag]
 	outbound, loaded := r.outboundByTag[tag]
 	return outbound, loaded
 	return outbound, loaded
@@ -993,15 +1013,15 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
 			}
 			}
 			if metadata.InboundOptions.SniffEnabled {
 			if metadata.InboundOptions.SniffEnabled {
 				sniffMetadata, _ := sniff.PeekPacket(
 				sniffMetadata, _ := sniff.PeekPacket(
-				ctx,
-				buffer.Bytes(),
-				sniff.DomainNameQuery,
-				sniff.QUICClientHello,
-				sniff.STUNMessage,
-				sniff.UTP,
-				sniff.UDPTracker,
-				sniff.DTLSRecord,
-			)
+					ctx,
+					buffer.Bytes(),
+					sniff.DomainNameQuery,
+					sniff.QUICClientHello,
+					sniff.STUNMessage,
+					sniff.UTP,
+					sniff.UDPTracker,
+					sniff.DTLSRecord,
+				)
 				if sniffMetadata != nil {
 				if sniffMetadata != nil {
 					metadata.Protocol = sniffMetadata.Protocol
 					metadata.Protocol = sniffMetadata.Protocol
 					metadata.Domain = sniffMetadata.Domain
 					metadata.Domain = sniffMetadata.Domain
@@ -1167,6 +1187,18 @@ func (r *Router) AutoDetectInterfaceFunc() control.Func {
 	}
 	}
 }
 }
 
 
+func (r *Router) RegisterAutoRedirectOutputMark(mark uint32) error {
+	if r.autoRedirectOutputMark > 0 {
+		return E.New("only one auto-redirect can be configured")
+	}
+	r.autoRedirectOutputMark = mark
+	return nil
+}
+
+func (r *Router) AutoRedirectOutputMark() uint32 {
+	return r.autoRedirectOutputMark
+}
+
 func (r *Router) DefaultInterface() string {
 func (r *Router) DefaultInterface() string {
 	return r.defaultInterface
 	return r.defaultInterface
 }
 }

+ 1 - 0
route/rule_item_rule_set.go

@@ -32,6 +32,7 @@ func (r *RuleSetItem) Start() error {
 		if !loaded {
 		if !loaded {
 			return E.New("rule-set not found: ", tag)
 			return E.New("rule-set not found: ", tag)
 		}
 		}
+		ruleSet.IncRef()
 		r.setList = append(r.setList, ruleSet)
 		r.setList = append(r.setList, ruleSet)
 	}
 	}
 	return nil
 	return nil

+ 21 - 0
route/rule_set.go

@@ -9,10 +9,13 @@ import (
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/logger"
 	"github.com/sagernet/sing/common/logger"
 	M "github.com/sagernet/sing/common/metadata"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 	N "github.com/sagernet/sing/common/network"
+
+	"go4.org/netipx"
 )
 )
 
 
 func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) {
 func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) {
@@ -26,6 +29,24 @@ func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.Contex
 	}
 	}
 }
 }
 
 
+func extractIPSetFromRule(rawRule adapter.HeadlessRule) []*netipx.IPSet {
+	switch rule := rawRule.(type) {
+	case *DefaultHeadlessRule:
+		return common.FlatMap(rule.destinationIPCIDRItems, func(rawItem RuleItem) []*netipx.IPSet {
+			switch item := rawItem.(type) {
+			case *IPCIDRItem:
+				return []*netipx.IPSet{item.ipSet}
+			default:
+				return nil
+			}
+		})
+	case *LogicalHeadlessRule:
+		return common.FlatMap(rule.rules, extractIPSetFromRule)
+	default:
+		panic("unexpected rule type")
+	}
+}
+
 var _ adapter.RuleSetStartContext = (*RuleSetStartContext)(nil)
 var _ adapter.RuleSetStartContext = (*RuleSetStartContext)(nil)
 
 
 type RuleSetStartContext struct {
 type RuleSetStartContext struct {

+ 51 - 8
route/rule_set_local.go

@@ -9,17 +9,24 @@ import (
 	"github.com/sagernet/sing-box/common/srs"
 	"github.com/sagernet/sing-box/common/srs"
 	C "github.com/sagernet/sing-box/constant"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/atomic"
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 	F "github.com/sagernet/sing/common/format"
 	F "github.com/sagernet/sing/common/format"
 	"github.com/sagernet/sing/common/json"
 	"github.com/sagernet/sing/common/json"
+	"github.com/sagernet/sing/common/x/list"
 	"github.com/sagernet/sing/service/filemanager"
 	"github.com/sagernet/sing/service/filemanager"
+
+	"go4.org/netipx"
 )
 )
 
 
 var _ adapter.RuleSet = (*LocalRuleSet)(nil)
 var _ adapter.RuleSet = (*LocalRuleSet)(nil)
 
 
 type LocalRuleSet struct {
 type LocalRuleSet struct {
+	tag      string
 	rules    []adapter.HeadlessRule
 	rules    []adapter.HeadlessRule
 	metadata adapter.RuleSetMetadata
 	metadata adapter.RuleSetMetadata
+	refs     atomic.Int32
 }
 }
 
 
 func NewLocalRuleSet(ctx context.Context, router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) {
 func NewLocalRuleSet(ctx context.Context, router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) {
@@ -59,16 +66,11 @@ func NewLocalRuleSet(ctx context.Context, router adapter.Router, options option.
 	metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
 	metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
 	metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
 	metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
 	metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
 	metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
-	return &LocalRuleSet{rules, metadata}, nil
+	return &LocalRuleSet{tag: options.Tag, rules: rules, metadata: metadata}, nil
 }
 }
 
 
-func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool {
-	for _, rule := range s.rules {
-		if rule.Match(metadata) {
-			return true
-		}
-	}
-	return false
+func (s *LocalRuleSet) Name() string {
+	return s.tag
 }
 }
 
 
 func (s *LocalRuleSet) String() string {
 func (s *LocalRuleSet) String() string {
@@ -79,10 +81,51 @@ func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.Ru
 	return nil
 	return nil
 }
 }
 
 
+func (s *LocalRuleSet) PostStart() error {
+	return nil
+}
+
 func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata {
 func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata {
 	return s.metadata
 	return s.metadata
 }
 }
 
 
+func (s *LocalRuleSet) ExtractIPSet() []*netipx.IPSet {
+	return common.FlatMap(s.rules, extractIPSetFromRule)
+}
+
+func (s *LocalRuleSet) IncRef() {
+	s.refs.Add(1)
+}
+
+func (s *LocalRuleSet) DecRef() {
+	if s.refs.Add(-1) < 0 {
+		panic("rule-set: negative refs")
+	}
+}
+
+func (s *LocalRuleSet) Cleanup() {
+	if s.refs.Load() == 0 {
+		s.rules = nil
+	}
+}
+
+func (s *LocalRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
+	return nil
+}
+
+func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) {
+}
+
 func (s *LocalRuleSet) Close() error {
 func (s *LocalRuleSet) Close() error {
+	s.rules = nil
 	return nil
 	return nil
 }
 }
+
+func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool {
+	for _, rule := range s.rules {
+		if rule.Match(metadata) {
+			return true
+		}
+	}
+	return false
+}

+ 67 - 7
route/rule_set_remote.go

@@ -8,20 +8,26 @@ import (
 	"net/http"
 	"net/http"
 	"runtime"
 	"runtime"
 	"strings"
 	"strings"
+	"sync"
 	"time"
 	"time"
 
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/common/srs"
 	"github.com/sagernet/sing-box/common/srs"
 	C "github.com/sagernet/sing-box/constant"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/atomic"
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 	F "github.com/sagernet/sing/common/format"
 	F "github.com/sagernet/sing/common/format"
 	"github.com/sagernet/sing/common/json"
 	"github.com/sagernet/sing/common/json"
 	"github.com/sagernet/sing/common/logger"
 	"github.com/sagernet/sing/common/logger"
 	M "github.com/sagernet/sing/common/metadata"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 	N "github.com/sagernet/sing/common/network"
+	"github.com/sagernet/sing/common/x/list"
 	"github.com/sagernet/sing/service"
 	"github.com/sagernet/sing/service"
 	"github.com/sagernet/sing/service/pause"
 	"github.com/sagernet/sing/service/pause"
+
+	"go4.org/netipx"
 )
 )
 
 
 var _ adapter.RuleSet = (*RemoteRuleSet)(nil)
 var _ adapter.RuleSet = (*RemoteRuleSet)(nil)
@@ -40,6 +46,9 @@ type RemoteRuleSet struct {
 	lastEtag       string
 	lastEtag       string
 	updateTicker   *time.Ticker
 	updateTicker   *time.Ticker
 	pauseManager   pause.Manager
 	pauseManager   pause.Manager
+	callbackAccess sync.Mutex
+	callbacks      list.List[adapter.RuleSetUpdateCallback]
+	refs           atomic.Int32
 }
 }
 
 
 func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet {
 func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet {
@@ -61,13 +70,8 @@ func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.
 	}
 	}
 }
 }
 
 
-func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool {
-	for _, rule := range s.rules {
-		if rule.Match(metadata) {
-			return true
-		}
-	}
-	return false
+func (s *RemoteRuleSet) Name() string {
+	return s.options.Tag
 }
 }
 
 
 func (s *RemoteRuleSet) String() string {
 func (s *RemoteRuleSet) String() string {
@@ -108,6 +112,10 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.R
 		}
 		}
 	}
 	}
 	s.updateTicker = time.NewTicker(s.updateInterval)
 	s.updateTicker = time.NewTicker(s.updateInterval)
+	return nil
+}
+
+func (s *RemoteRuleSet) PostStart() error {
 	go s.loopUpdate()
 	go s.loopUpdate()
 	return nil
 	return nil
 }
 }
@@ -116,6 +124,38 @@ func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata {
 	return s.metadata
 	return s.metadata
 }
 }
 
 
+func (s *RemoteRuleSet) ExtractIPSet() []*netipx.IPSet {
+	return common.FlatMap(s.rules, extractIPSetFromRule)
+}
+
+func (s *RemoteRuleSet) IncRef() {
+	s.refs.Add(1)
+}
+
+func (s *RemoteRuleSet) DecRef() {
+	if s.refs.Add(-1) < 0 {
+		panic("rule-set: negative refs")
+	}
+}
+
+func (s *RemoteRuleSet) Cleanup() {
+	if s.refs.Load() == 0 {
+		s.rules = nil
+	}
+}
+
+func (s *RemoteRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
+	s.callbackAccess.Lock()
+	defer s.callbackAccess.Unlock()
+	return s.callbacks.PushBack(callback)
+}
+
+func (s *RemoteRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) {
+	s.callbackAccess.Lock()
+	defer s.callbackAccess.Unlock()
+	s.callbacks.Remove(element)
+}
+
 func (s *RemoteRuleSet) loadBytes(content []byte) error {
 func (s *RemoteRuleSet) loadBytes(content []byte) error {
 	var (
 	var (
 		plainRuleSet option.PlainRuleSet
 		plainRuleSet option.PlainRuleSet
@@ -148,6 +188,12 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
 	s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
 	s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
 	s.metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
 	s.metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
 	s.rules = rules
 	s.rules = rules
+	s.callbackAccess.Lock()
+	callbacks := s.callbacks.Array()
+	s.callbackAccess.Unlock()
+	for _, callback := range callbacks {
+		callback(s)
+	}
 	return nil
 	return nil
 }
 }
 
 
@@ -156,6 +202,8 @@ func (s *RemoteRuleSet) loopUpdate() {
 		err := s.fetchOnce(s.ctx, nil)
 		err := s.fetchOnce(s.ctx, nil)
 		if err != nil {
 		if err != nil {
 			s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
 			s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
+		} else if s.refs.Load() == 0 {
+			s.rules = nil
 		}
 		}
 	}
 	}
 	for {
 	for {
@@ -168,6 +216,8 @@ func (s *RemoteRuleSet) loopUpdate() {
 			err := s.fetchOnce(s.ctx, nil)
 			err := s.fetchOnce(s.ctx, nil)
 			if err != nil {
 			if err != nil {
 				s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
 				s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
+			} else if s.refs.Load() == 0 {
+				s.rules = nil
 			}
 			}
 		}
 		}
 	}
 	}
@@ -253,7 +303,17 @@ func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext adapter.Rule
 }
 }
 
 
 func (s *RemoteRuleSet) Close() error {
 func (s *RemoteRuleSet) Close() error {
+	s.rules = nil
 	s.updateTicker.Stop()
 	s.updateTicker.Stop()
 	s.cancel()
 	s.cancel()
 	return nil
 	return nil
 }
 }
+
+func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool {
+	for _, rule := range s.rules {
+		if rule.Match(metadata) {
+			return true
+		}
+	}
+	return false
+}