diff --git a/adapter/router.go b/adapter/router.go index c481f0c8..619c1110 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -10,15 +10,18 @@ import ( "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" mdns "github.com/miekg/dns" + "go4.org/netipx" ) type Router interface { Service PreStarter PostStarter + Cleanup() error Outbounds() []Outbound Outbound(tag string) (Outbound, bool) @@ -46,6 +49,8 @@ type Router interface { AutoDetectInterface() bool AutoDetectInterfaceFunc() control.Func DefaultMark() uint32 + RegisterAutoRedirectOutputMark(mark uint32) error + AutoRedirectOutputMark() uint32 NetworkMonitor() tun.NetworkUpdateMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor PackageManager() tun.PackageManager @@ -92,12 +97,22 @@ type DNSRule interface { } type RuleSet interface { + Name() string StartContext(ctx context.Context, startContext RuleSetStartContext) error + PostStart() error Metadata() RuleSetMetadata + ExtractIPSet() []*netipx.IPSet + IncRef() + DecRef() + Cleanup() + RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback] + UnregisterCallback(element *list.Element[RuleSetUpdateCallback]) Close() error HeadlessRule } +type RuleSetUpdateCallback func(it RuleSet) + type RuleSetMetadata struct { ContainsProcessRule bool ContainsWIFIRule bool diff --git a/box.go b/box.go index 4e7a9879..75646de5 100644 --- a/box.go +++ b/box.go @@ -303,7 +303,11 @@ func (s *Box) start() error { 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 { @@ -313,16 +317,28 @@ func (s *Box) postStart() error { 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() 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 { diff --git a/cmd/sing-box/cmd_tools_fetch.go b/cmd/sing-box/cmd_tools_fetch.go index 256c3f42..3f62424a 100644 --- a/cmd/sing-box/cmd_tools_fetch.go +++ b/cmd/sing-box/cmd_tools_fetch.go @@ -9,8 +9,10 @@ import ( "net/url" "os" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/spf13/cobra" @@ -32,7 +34,10 @@ func init() { commandTools.AddCommand(commandFetch) } -var httpClient *http.Client +var ( + httpClient *http.Client + http3Client *http.Client +) func fetch(args []string) error { instance, err := createPreStartedClient() @@ -53,8 +58,16 @@ func fetch(args []string) error { }, } defer httpClient.CloseIdleConnections() + if C.WithQUIC { + err = initializeHTTP3Client(instance) + if err != nil { + return err + } + defer http3Client.CloseIdleConnections() + } for _, urlString := range args { - parsedURL, err := url.Parse(urlString) + var parsedURL *url.URL + parsedURL, err = url.Parse(urlString) if err != nil { return err } @@ -63,16 +76,27 @@ func fetch(args []string) error { parsedURL.Scheme = "http" fallthrough 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 { + return err + } + default: + return E.New("unsupported scheme: ", parsedURL.Scheme) } } 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) if err != nil { return err diff --git a/cmd/sing-box/cmd_tools_fetch_http3.go b/cmd/sing-box/cmd_tools_fetch_http3.go new file mode 100644 index 00000000..5dc3d915 --- /dev/null +++ b/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 +} diff --git a/cmd/sing-box/cmd_tools_fetch_http3_stub.go b/cmd/sing-box/cmd_tools_fetch_http3_stub.go new file mode 100644 index 00000000..ae13f54c --- /dev/null +++ b/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 +} diff --git a/common/dialer/default.go b/common/dialer/default.go index 4fbad07d..488e8000 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -50,12 +50,26 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi dialer.Control = control.Append(dialer.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)) 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())) 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 { listener.Control = control.Append(listener.Control, control.ReuseAddr()) diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 4bd7106c..1e2bf400 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -4,7 +4,18 @@ icon: material/new-box !!! quote "Changes in sing-box 1.10.0" - :material-plus: [auto_redirect](#auto_redirect) + :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" @@ -27,27 +38,57 @@ icon: material/new-box "type": "tun", "tag": "tun-in", "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, "gso": false, "auto_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": [ "0.0.0.0/1", "128.0.0.0/1" ], + // deprecated "inet6_route_address": [ "::/1", "8000::/1" ], + "route_exclude_address": [ + "192.168.0.0/16", + "fc00::/7" + ], + // deprecated "inet4_route_exclude_address": [ "192.168.0.0/16" ], + // deprecated "inet6_route_exclude_address": [ "fc00::/7" ], + "route_address_set": [ + "geoip-cloudflare" + ], + "route_exclude_address_set": [ + "geoip-cn" + ], "endpoint_independent_nat": false, "udp_timeout": "5m", "stack": "system", @@ -107,14 +148,26 @@ icon: material/new-box 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 -==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. #### 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. #### mtu @@ -168,7 +221,7 @@ It may prevent some applications (such as VirtualBox) from working properly in c !!! quote "" - Only supported on Linux. + Only supported on Linux with `auto_route` enabled. Automatically configure iptables/nftables to redirect connections. @@ -181,22 +234,76 @@ use [VPNHotspot](https://github.com/Mygod/VPNHotspot). `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 +!!! 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. #### 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. +#### route_exclude_address + +!!! question "Since sing-box 1.10.0" + +Exclude custom routes when `auto_route` is enabled. + #### 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. #### 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. +#### 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 !!! info "" diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index ef6543c3..5b1d35af 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -2,9 +2,20 @@ icon: material/new-box --- -!!! quote "sing-box 1.10.0 中的更改" +!!! quote "Changes in sing-box 1.10.0" - :material-plus: [auto_redirect](#auto_redirect) + :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 中的更改" @@ -27,27 +38,57 @@ icon: material/new-box "type": "tun", "tag": "tun-in", "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, "gso": false, "auto_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": [ "0.0.0.0/1", "128.0.0.0/1" ], + // 已弃用 "inet6_route_address": [ "::/1", "8000::/1" ], + "route_exclude_address": [ + "192.168.0.0/16", + "fc00::/7" + ], + // 已弃用 "inet4_route_exclude_address": [ "192.168.0.0/16" ], + // 已弃用 "inet6_route_exclude_address": [ "fc00::/7" ], + "route_address_set": [ + "geoip-cloudflare" + ], + "route_exclude_address_set": [ + "geoip-cn" + ], "endpoint_independent_nat": false, "udp_timeout": "5m", "stack": "system", @@ -107,14 +148,30 @@ icon: material/new-box 虚拟设备名称,默认自动选择。 +#### address + +!!! question "自 sing-box 1.10.0 起" + +==必填== + +tun 接口的 IPv4 和 IPv6 前缀。 + #### inet4_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet4_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除. + ==必填== tun 接口的 IPv4 前缀。 #### inet6_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet6_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除. + tun 接口的 IPv6 前缀。 #### mtu @@ -181,22 +238,76 @@ tun 接口的 IPv6 前缀。 带有 `auto_redirect `的 `auto_route` 现在可以在路由器上按预期工作,**无需干预**。 +#### route_address + +!!! question "自 sing-box 1.10.0 起" + +设置到 Tun 的自定义路由。 + #### inet4_route_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet4_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除. + 启用 `auto_route` 时使用自定义路由而不是默认路由。 #### inet6_route_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet6_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除. + 启用 `auto_route` 时使用自定义路由而不是默认路由。 +#### route_exclude_address + +!!! question "自 sing-box 1.10.0 起" + +设置到 Tun 的排除自定义路由。 + #### inet4_route_exclude_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet4_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除. + 启用 `auto_route` 时排除自定义路由。 #### inet6_route_exclude_address +!!! failure "已在 sing-box 1.10.0 废弃" + + `inet6_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除. + 启用 `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 启用独立于端点的 NAT。 @@ -312,7 +423,7 @@ TCP/IP 栈。 !!! note "" - 在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**. + 在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**. 绕过代理的主机名列表。 diff --git a/docs/deprecated.md b/docs/deprecated.md index 439bf7e8..249bc492 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -6,6 +6,14 @@ icon: material/delete-alert ## 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 Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile. diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index 76e7e768..6815e9fc 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -6,6 +6,14 @@ icon: material/delete-alert ## 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 的支持 由于维护困难,sing-box 1.10.0 要求至少 Go 1.20 才能编译。 diff --git a/docs/migration.md b/docs/migration.md index b282a90f..c696a836 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -2,12 +2,76 @@ 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.0 -!!! warning "Unstable" - - This version is still under development, and the following migration guide may be changed in the future. - ### `domain_suffix` behavior update For historical reasons, sing-box's `domain_suffix` rule matches literal prefixes instead of the same as other projects. diff --git a/docs/migration.zh.md b/docs/migration.zh.md index bd63bf17..9fe40cc9 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -2,12 +2,76 @@ 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.0 -!!! warning "不稳定的" - - 该版本仍在开发中,迁移指南可能将在未来更改。 - ### `domain_suffix` 行为更新 由于历史原因,sing-box 的 `domain_suffix` 规则匹配字面前缀,而不与其他项目相同。 diff --git a/inbound/tun.go b/inbound/tun.go index a0e7f5fa..b18adc80 100644 --- a/inbound/tun.go +++ b/inbound/tun.go @@ -3,7 +3,9 @@ package inbound import ( "context" "net" + "net/netip" "os" + "runtime" "strconv" "strings" "time" @@ -20,28 +22,91 @@ import ( M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ranges" + "github.com/sagernet/sing/common/x/list" + + "go4.org/netipx" ) var _ adapter.Inbound = (*Tun)(nil) 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 - autoRedirect tun.AutoRedirect + 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) { + 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 if tunMTU == 0 { tunMTU = 9000 @@ -68,6 +133,23 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger } } + 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, ctx: ctx, @@ -78,23 +160,26 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger Name: options.InterfaceName, MTU: tunMTU, GSO: options.GSO, - Inet4Address: options.Inet4Address, - Inet6Address: options.Inet6Address, + Inet4Address: inet4Address, + Inet6Address: inet6Address, AutoRoute: options.AutoRoute, + IPRoute2TableIndex: tableIndex, + IPRoute2RuleIndex: ruleIndex, + AutoRedirectInputMark: inputMark, + AutoRedirectOutputMark: outputMark, StrictRoute: options.StrictRoute, IncludeInterface: options.IncludeInterface, 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, ExcludeUID: excludeUID, IncludeAndroidUser: options.IncludeAndroidUser, IncludePackage: options.IncludePackage, ExcludePackage: options.ExcludePackage, InterfaceMonitor: router.InterfaceMonitor(), - TableIndex: 2022, }, endpointIndependentNat: options.EndpointIndependentNat, udpTimeout: int64(udpTimeout.Seconds()), @@ -108,15 +193,47 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger } disableNFTables, dErr := strconv.ParseBool(os.Getenv("DISABLE_NFTABLES")) inbound.autoRedirect, err = tun.NewAutoRedirect(tun.AutoRedirectOptions{ - TunOptions: &inbound.tunOptions, - Context: ctx, - Handler: inbound, - Logger: logger, - TableName: "sing-box", - DisableNFTables: dErr == nil && disableNFTables, + 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") + 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 @@ -141,11 +258,11 @@ func parseRange(uidRanges []ranges.Range[uint32], rangeList []string) ([]ranges. } var start, end uint64 var err error - start, err = strconv.ParseUint(uidRange[:subIndex], 10, 32) + start, err = strconv.ParseUint(uidRange[:subIndex], 0, 32) if err != nil { 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 { return nil, E.Cause(err, "parse range end") } @@ -215,18 +332,57 @@ func (t *Tun) Start() error { if err != nil { return err } - if t.autoRedirect != nil { - monitor.Start("initiating auto redirect") - err = t.autoRedirect.Start() - monitor.Finish() - if err != nil { - return E.Cause(err, "auto redirect") - } - } t.logger.Info("started at ", t.tunOptions.Name) 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("initiating 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 { return common.Close( t.tunStack, diff --git a/option/tun.go b/option/tun.go index 91930866..cbc73e7d 100644 --- a/option/tun.go +++ b/option/tun.go @@ -3,30 +3,46 @@ package option import "net/netip" 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"` - AutoRedirect bool `json:"auto_redirect,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"` - Inet4RouteExcludeAddress Listable[netip.Prefix] `json:"inet4_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"` + 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"` + // Deprecated: merged to RouteExcludeAddress + Inet6RouteExcludeAddress Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"` } diff --git a/route/router.go b/route/router.go index 6098c5a0..b8702ce6 100644 --- a/route/router.go +++ b/route/router.go @@ -83,6 +83,7 @@ type Router struct { autoDetectInterface bool defaultInterface string defaultMark uint32 + autoRedirectOutputMark uint32 networkMonitor tun.NetworkUpdateMonitor interfaceMonitor tun.DefaultInterfaceMonitor packageManager tun.PackageManager @@ -724,10 +725,26 @@ func (r *Router) PostStart() error { 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 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) { outbound, loaded := r.outboundByTag[tag] return outbound, loaded @@ -1131,6 +1148,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 { return r.defaultInterface } diff --git a/route/rule_item_rule_set.go b/route/rule_item_rule_set.go index 482a9c7b..4ecf8c18 100644 --- a/route/rule_item_rule_set.go +++ b/route/rule_item_rule_set.go @@ -32,6 +32,7 @@ func (r *RuleSetItem) Start() error { if !loaded { return E.New("rule-set not found: ", tag) } + ruleSet.IncRef() r.setList = append(r.setList, ruleSet) } return nil diff --git a/route/rule_set.go b/route/rule_set.go index f644fb40..ff28858e 100644 --- a/route/rule_set.go +++ b/route/rule_set.go @@ -9,10 +9,13 @@ import ( "github.com/sagernet/sing-box/adapter" 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" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" 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) { @@ -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) type RuleSetStartContext struct { diff --git a/route/rule_set_local.go b/route/rule_set_local.go index 39458267..53446183 100644 --- a/route/rule_set_local.go +++ b/route/rule_set_local.go @@ -9,16 +9,23 @@ import ( "github.com/sagernet/sing-box/common/srs" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/atomic" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/x/list" + + "go4.org/netipx" ) var _ adapter.RuleSet = (*LocalRuleSet)(nil) type LocalRuleSet struct { + tag string rules []adapter.HeadlessRule metadata adapter.RuleSetMetadata + refs atomic.Int32 } func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) { @@ -58,16 +65,11 @@ func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleS metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) 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 { @@ -78,10 +80,51 @@ func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.Ru return nil } +func (s *LocalRuleSet) PostStart() error { + return nil +} + func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata { return s.metadata } -func (s *LocalRuleSet) Close() error { +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 { + s.rules = nil + return nil +} + +func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +} diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go index 8389c2f4..bf0cfe20 100644 --- a/route/rule_set_remote.go +++ b/route/rule_set_remote.go @@ -8,20 +8,26 @@ import ( "net/http" "runtime" "strings" + "sync" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/srs" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/atomic" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" + + "go4.org/netipx" ) var _ adapter.RuleSet = (*RemoteRuleSet)(nil) @@ -40,6 +46,9 @@ type RemoteRuleSet struct { lastEtag string updateTicker *time.Ticker 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 { @@ -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 { @@ -108,6 +112,10 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.R } } s.updateTicker = time.NewTicker(s.updateInterval) + return nil +} + +func (s *RemoteRuleSet) PostStart() error { go s.loopUpdate() return nil } @@ -116,6 +124,38 @@ func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata { 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 { var ( plainRuleSet option.PlainRuleSet @@ -148,6 +188,12 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error { s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) s.metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) s.rules = rules + s.callbackAccess.Lock() + callbacks := s.callbacks.Array() + s.callbackAccess.Unlock() + for _, callback := range callbacks { + callback(s) + } return nil } @@ -156,6 +202,8 @@ func (s *RemoteRuleSet) loopUpdate() { err := s.fetchOnce(s.ctx, nil) if err != nil { s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) + } else if s.refs.Load() == 0 { + s.rules = nil } } for { @@ -168,6 +216,8 @@ func (s *RemoteRuleSet) loopUpdate() { err := s.fetchOnce(s.ctx, nil) if err != nil { 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 { + s.rules = nil s.updateTicker.Stop() s.cancel() return nil } + +func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { + for _, rule := range s.rules { + if rule.Match(metadata) { + return true + } + } + return false +}