auto-redirect: Add route address set support for nftables

This commit is contained in:
世界 2024-06-12 15:20:13 +08:00
parent 826ebd56a6
commit 1abe60755b
No known key found for this signature in database
GPG Key ID: CD109927C34A63C4
23 changed files with 932 additions and 117 deletions

2
.gitignore vendored
View File

@ -14,3 +14,5 @@
/*.xcframework/ /*.xcframework/
.DS_Store .DS_Store
/config.d/ /config.d/
/venv/

View File

@ -192,13 +192,15 @@ lib_install:
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.3 go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.3
docs: docs:
mkdocs serve venv/bin/mkdocs serve
publish_docs: publish_docs:
mkdocs gh-deploy -m "Update" --force --ignore-version --no-history venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
docs_install: docs_install:
pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*" python -m venv venv
source ./venv/bin/active && pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
clean: clean:
rm -rf bin dist sing-box rm -rf bin dist sing-box
rm -f $(shell go env GOPATH)/sing-box rm -f $(shell go env GOPATH)/sing-box

View File

@ -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)
@ -45,7 +48,9 @@ type Router interface {
DefaultInterface() string DefaultInterface() string
AutoDetectInterface() bool AutoDetectInterface() bool
AutoDetectInterfaceFunc() control.Func AutoDetectInterfaceFunc() control.Func
DefaultMark() int 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

28
box.go
View File

@ -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 { // TODO: reorganize ALL start order
if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound { 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())
} }
} }
} }
err := s.router.PostStart()
return 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 {

View File

@ -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 { if err != nil {
return err 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 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

View File

@ -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
}

View File

@ -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
}

View File

@ -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())

View File

@ -4,7 +4,18 @@ icon: material/new-box
!!! quote "Changes in sing-box 1.10.0" !!! 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: [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"
@ -27,27 +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", "address": [
"inet6_address": "fdfe:dcba:9876::1/126", "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, "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",
@ -107,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
@ -168,7 +221,7 @@ It may prevent some applications (such as VirtualBox) from working properly in c
!!! quote "" !!! quote ""
Only supported on Linux. Only supported on Linux with `auto_route` enabled.
Automatically configure iptables/nftables to redirect connections. 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**. `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 ""

View File

@ -2,9 +2,20 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "sing-box 1.10.0 中的更改" !!! 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: [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 中的更改"
@ -27,27 +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", "address": [
"inet6_address": "fdfe:dcba:9876::1/126", "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, "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",
@ -107,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
@ -181,22 +238,76 @@ tun 接口的 IPv6 前缀。
带有 `auto_redirect ``auto_route` 现在可以在路由器上按预期工作,**无需干预**。 带有 `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。
@ -312,7 +423,7 @@ TCP/IP 栈。
!!! note "" !!! note ""
在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**. 在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**.
绕过代理的主机名列表。 绕过代理的主机名列表。

View File

@ -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.

View File

@ -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 才能编译。

View File

@ -2,12 +2,76 @@
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.0 ## 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 ### `domain_suffix` behavior update
For historical reasons, sing-box's `domain_suffix` rule matches literal prefixes instead of the same as other projects. For historical reasons, sing-box's `domain_suffix` rule matches literal prefixes instead of the same as other projects.

View File

@ -2,12 +2,76 @@
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.0 ## 1.9.0
!!! warning "不稳定的"
该版本仍在开发中,迁移指南可能将在未来更改。
### `domain_suffix` 行为更新 ### `domain_suffix` 行为更新
由于历史原因sing-box 的 `domain_suffix` 规则匹配字面前缀,而不与其他项目相同。 由于历史原因sing-box 的 `domain_suffix` 规则匹配字面前缀,而不与其他项目相同。

View File

@ -3,7 +3,9 @@ package inbound
import ( import (
"context" "context"
"net" "net"
"net/netip"
"os" "os"
"runtime"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -20,28 +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 tag string
ctx context.Context ctx context.Context
router adapter.Router router adapter.Router
logger log.ContextLogger logger log.ContextLogger
inboundOptions option.InboundOptions inboundOptions option.InboundOptions
tunOptions tun.Options tunOptions tun.Options
endpointIndependentNat bool endpointIndependentNat bool
udpTimeout int64 udpTimeout int64
stack string stack string
tunIf tun.Tun tunIf tun.Tun
tunStack tun.Stack tunStack tun.Stack
platformInterface platform.Interface platformInterface platform.Interface
platformOptions option.TunPlatformOptions platformOptions option.TunPlatformOptions
autoRedirect tun.AutoRedirect 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
@ -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{ inbound := &Tun{
tag: tag, tag: tag,
ctx: ctx, ctx: ctx,
@ -78,23 +160,26 @@ 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, Inet4Address: inet4Address,
Inet6Address: options.Inet6Address, 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, Inet4RouteAddress: inet4RouteAddress,
Inet6RouteAddress: options.Inet6RouteAddress, Inet6RouteAddress: inet6RouteAddress,
Inet4RouteExcludeAddress: options.Inet4RouteExcludeAddress, Inet4RouteExcludeAddress: inet4RouteExcludeAddress,
Inet6RouteExcludeAddress: options.Inet6RouteExcludeAddress, 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()),
@ -108,15 +193,47 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
} }
disableNFTables, dErr := strconv.ParseBool(os.Getenv("DISABLE_NFTABLES")) disableNFTables, dErr := strconv.ParseBool(os.Getenv("DISABLE_NFTABLES"))
inbound.autoRedirect, err = tun.NewAutoRedirect(tun.AutoRedirectOptions{ inbound.autoRedirect, err = tun.NewAutoRedirect(tun.AutoRedirectOptions{
TunOptions: &inbound.tunOptions, TunOptions: &inbound.tunOptions,
Context: ctx, Context: ctx,
Handler: inbound, Handler: inbound,
Logger: logger, Logger: logger,
TableName: "sing-box", NetworkMonitor: router.NetworkMonitor(),
DisableNFTables: dErr == nil && disableNFTables, InterfaceFinder: router.InterfaceFinder(),
TableName: "sing-box",
DisableNFTables: dErr == nil && disableNFTables,
RouteAddressSet: &inbound.routeAddressSet,
RouteExcludeAddressSet: &inbound.routeExcludeAddressSet,
}) })
if err != nil { 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 return inbound, nil
@ -141,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")
} }
@ -215,18 +332,57 @@ func (t *Tun) Start() error {
if err != nil { if err != nil {
return err 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) t.logger.Info("started at ", t.tunOptions.Name)
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("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 { func (t *Tun) Close() error {
return common.Close( return common.Close(
t.tunStack, t.tunStack,

View File

@ -113,7 +113,7 @@ type DialerOptions struct {
Inet4BindAddress *ListenAddress `json:"inet4_bind_address,omitempty"` Inet4BindAddress *ListenAddress `json:"inet4_bind_address,omitempty"`
Inet6BindAddress *ListenAddress `json:"inet6_bind_address,omitempty"` Inet6BindAddress *ListenAddress `json:"inet6_bind_address,omitempty"`
ProtectPath string `json:"protect_path,omitempty"` ProtectPath string `json:"protect_path,omitempty"`
RoutingMark int `json:"routing_mark,omitempty"` RoutingMark uint32 `json:"routing_mark,omitempty"`
ReuseAddr bool `json:"reuse_addr,omitempty"` ReuseAddr bool `json:"reuse_addr,omitempty"`
ConnectTimeout Duration `json:"connect_timeout,omitempty"` ConnectTimeout Duration `json:"connect_timeout,omitempty"`
TCPFastOpen bool `json:"tcp_fast_open,omitempty"` TCPFastOpen bool `json:"tcp_fast_open,omitempty"`

View File

@ -10,7 +10,7 @@ type RouteOptions struct {
AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` AutoDetectInterface bool `json:"auto_detect_interface,omitempty"`
OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"` OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"`
DefaultInterface string `json:"default_interface,omitempty"` DefaultInterface string `json:"default_interface,omitempty"`
DefaultMark int `json:"default_mark,omitempty"` DefaultMark uint32 `json:"default_mark,omitempty"`
} }
type GeoIPOptions struct { type GeoIPOptions struct {

View File

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

View File

@ -82,7 +82,8 @@ type Router struct {
interfaceFinder *control.DefaultInterfaceFinder interfaceFinder *control.DefaultInterfaceFinder
autoDetectInterface bool autoDetectInterface bool
defaultInterface string defaultInterface string
defaultMark int defaultMark uint32
autoRedirectOutputMark uint32
networkMonitor tun.NetworkUpdateMonitor networkMonitor tun.NetworkUpdateMonitor
interfaceMonitor tun.DefaultInterfaceMonitor interfaceMonitor tun.DefaultInterfaceMonitor
packageManager tun.PackageManager packageManager tun.PackageManager
@ -724,10 +725,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
@ -1131,11 +1148,23 @@ 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
} }
func (r *Router) DefaultMark() int { func (r *Router) DefaultMark() uint32 {
return r.defaultMark return r.defaultMark
} }

View File

@ -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

View File

@ -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 {

View File

@ -9,16 +9,23 @@ 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"
"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(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) { 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.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 { func (s *LocalRuleSet) Name() string {
for _, rule := range s.rules { return s.tag
if rule.Match(metadata) {
return true
}
}
return false
} }
func (s *LocalRuleSet) String() string { func (s *LocalRuleSet) String() string {
@ -78,10 +80,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) 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 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
}

View File

@ -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 { func (s *RemoteRuleSet) Name() string {
for _, rule := range s.rules { return s.options.Tag
if rule.Match(metadata) {
return true
}
}
return false
} }
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
}