From dccc3a9fc0640cb25e8174962ec71b5d8fb3dbab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 26 Apr 2024 20:24:36 +0800 Subject: [PATCH 01/29] Fix linux workflow --- .github/workflows/linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 13228efd4c..581155c2ae 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -33,5 +33,5 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} FURY_TOKEN: ${{ secrets.FURY_TOKEN }} - NFPM_KEY_PATH: ${{ env.Home }}/.gnupg/sagernet.key + NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} From 0aa43d6c01e0b689fefa6b492d06e248db80c777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 26 Apr 2024 19:46:41 +0800 Subject: [PATCH 02/29] documentation: Fix strict_route description --- docs/configuration/inbound/tun.md | 2 +- docs/configuration/inbound/tun.zh.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 002c690a4b..6e6c2ae027 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -140,7 +140,7 @@ Enforce strict routing rules when `auto_route` is enabled: * Let unsupported network unreachable * Route all connections to tun -It prevents address leaks and makes DNS hijacking work on Android, but your device will not be accessible by others. +It prevents address leaks and makes DNS hijacking work on Android. *In Windows*: diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index 6a80063487..71c66704ba 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -140,7 +140,7 @@ tun 接口的 IPv6 前缀。 * 让不支持的网络无法到达 * 将所有连接路由到 tun -它可以防止地址泄漏,并使 DNS 劫持在 Android 上工作,但你的设备将无法其他设备被访问。 +它可以防止地址泄漏,并使 DNS 劫持在 Android 上工作。 *在 Windows 中*: From 50197a3f49b26b729ffa61a3143af6e17f637fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 26 Apr 2024 20:37:28 +0800 Subject: [PATCH 03/29] documentation: Update TestFlight --- docs/clients/apple/index.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/clients/apple/index.md b/docs/clients/apple/index.md index 36a4edd9f4..cd090776ef 100644 --- a/docs/clients/apple/index.md +++ b/docs/clients/apple/index.md @@ -15,7 +15,12 @@ platform-specific function implementation, such as TUN transparent proxy impleme ## :material-download: Download * [App Store](https://apps.apple.com/us/app/sing-box/id6451272673) -* [TestFlight (Beta)](https://testflight.apple.com/join/AcqO44FH) +* ~~TestFlight (Beta)~~ + +TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai) +(one-time sponsorships are accepted). +Once you donate, you can get an invitation by sending us your Apple ID [via email](mailto:contact@sagernet.org), +or join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot). ## :material-file-download: Download (macOS standalone version) From 09f104cb3cacc4230e2760728544a2c1c795d231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 27 Dec 2023 18:05:52 +0800 Subject: [PATCH 04/29] badtls: Support uTLS and TLS ECH for read waiter --- common/badtls/read_wait.go | 78 +++++++++++++++++++++++---------- common/badtls/read_wait_ech.go | 31 +++++++++++++ common/badtls/read_wait_utls.go | 31 +++++++++++++ 3 files changed, 117 insertions(+), 23 deletions(-) create mode 100644 common/badtls/read_wait_ech.go create mode 100644 common/badtls/read_wait_utls.go diff --git a/common/badtls/read_wait.go b/common/badtls/read_wait.go index fdae8a1c35..334bcfa81e 100644 --- a/common/badtls/read_wait.go +++ b/common/badtls/read_wait.go @@ -4,6 +4,8 @@ package badtls import ( "bytes" + "context" + "net" "os" "reflect" "sync" @@ -18,20 +20,32 @@ import ( var _ N.ReadWaiter = (*ReadWaitConn)(nil) type ReadWaitConn struct { - *tls.STDConn - halfAccess *sync.Mutex - rawInput *bytes.Buffer - input *bytes.Reader - hand *bytes.Buffer - readWaitOptions N.ReadWaitOptions + tls.Conn + halfAccess *sync.Mutex + rawInput *bytes.Buffer + input *bytes.Reader + hand *bytes.Buffer + readWaitOptions N.ReadWaitOptions + tlsReadRecord func() error + tlsHandlePostHandshakeMessage func() error } func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) { - stdConn, isSTDConn := conn.(*tls.STDConn) - if !isSTDConn { + var ( + loaded bool + tlsReadRecord func() error + tlsHandlePostHandshakeMessage func() error + ) + for _, tlsCreator := range tlsRegistry { + loaded, tlsReadRecord, tlsHandlePostHandshakeMessage = tlsCreator(conn) + if loaded { + break + } + } + if !loaded { return nil, os.ErrInvalid } - rawConn := reflect.Indirect(reflect.ValueOf(stdConn)) + rawConn := reflect.Indirect(reflect.ValueOf(conn)) rawHalfConn := rawConn.FieldByName("in") if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct { return nil, E.New("badtls: invalid half conn") @@ -57,11 +71,13 @@ func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) { } hand := (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr())) return &ReadWaitConn{ - STDConn: stdConn, - halfAccess: halfAccess, - rawInput: rawInput, - input: input, - hand: hand, + Conn: conn, + halfAccess: halfAccess, + rawInput: rawInput, + input: input, + hand: hand, + tlsReadRecord: tlsReadRecord, + tlsHandlePostHandshakeMessage: tlsHandlePostHandshakeMessage, }, nil } @@ -71,19 +87,19 @@ func (c *ReadWaitConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy } func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) { - err = c.Handshake() + err = c.HandshakeContext(context.Background()) if err != nil { return } c.halfAccess.Lock() defer c.halfAccess.Unlock() for c.input.Len() == 0 { - err = tlsReadRecord(c.STDConn) + err = c.tlsReadRecord() if err != nil { return } for c.hand.Len() > 0 { - err = tlsHandlePostHandshakeMessage(c.STDConn) + err = c.tlsHandlePostHandshakeMessage() if err != nil { return } @@ -100,7 +116,7 @@ func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) { if n != 0 && c.input.Len() == 0 && c.rawInput.Len() > 0 && // recordType(c.rawInput.Bytes()[0]) == recordTypeAlert { c.rawInput.Bytes()[0] == 21 { - _ = tlsReadRecord(c.STDConn) + _ = c.tlsReadRecord() // return n, err // will be io.EOF on closeNotify } @@ -109,11 +125,27 @@ func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) { } func (c *ReadWaitConn) Upstream() any { - return c.STDConn + return c.Conn +} + +var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) + +func init() { + tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) { + tlsConn, loaded := conn.(*tls.STDConn) + if !loaded { + return + } + return true, func() error { + return stdTLSReadRecord(tlsConn) + }, func() error { + return stdTLSHandlePostHandshakeMessage(tlsConn) + } + }) } -//go:linkname tlsReadRecord crypto/tls.(*Conn).readRecord -func tlsReadRecord(c *tls.STDConn) error +//go:linkname stdTLSReadRecord crypto/tls.(*Conn).readRecord +func stdTLSReadRecord(c *tls.STDConn) error -//go:linkname tlsHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage -func tlsHandlePostHandshakeMessage(c *tls.STDConn) error +//go:linkname stdTLSHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage +func stdTLSHandlePostHandshakeMessage(c *tls.STDConn) error diff --git a/common/badtls/read_wait_ech.go b/common/badtls/read_wait_ech.go new file mode 100644 index 0000000000..6a0d5b5ff8 --- /dev/null +++ b/common/badtls/read_wait_ech.go @@ -0,0 +1,31 @@ +//go:build go1.21 && !without_badtls && with_ech + +package badtls + +import ( + "net" + _ "unsafe" + + "github.com/sagernet/cloudflare-tls" + "github.com/sagernet/sing/common" +) + +func init() { + tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) { + tlsConn, loaded := common.Cast[*tls.Conn](conn) + if !loaded { + return + } + return true, func() error { + return echReadRecord(tlsConn) + }, func() error { + return echHandlePostHandshakeMessage(tlsConn) + } + }) +} + +//go:linkname echReadRecord github.com/sagernet/cloudflare-tls.(*Conn).readRecord +func echReadRecord(c *tls.Conn) error + +//go:linkname echHandlePostHandshakeMessage github.com/sagernet/cloudflare-tls.(*Conn).handlePostHandshakeMessage +func echHandlePostHandshakeMessage(c *tls.Conn) error diff --git a/common/badtls/read_wait_utls.go b/common/badtls/read_wait_utls.go new file mode 100644 index 0000000000..ebdb2251a9 --- /dev/null +++ b/common/badtls/read_wait_utls.go @@ -0,0 +1,31 @@ +//go:build go1.21 && !without_badtls && with_utls + +package badtls + +import ( + "net" + _ "unsafe" + + "github.com/sagernet/sing/common" + "github.com/sagernet/utls" +) + +func init() { + tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) { + tlsConn, loaded := common.Cast[*tls.UConn](conn) + if !loaded { + return + } + return true, func() error { + return utlsReadRecord(tlsConn.Conn) + }, func() error { + return utlsHandlePostHandshakeMessage(tlsConn.Conn) + } + }) +} + +//go:linkname utlsReadRecord github.com/sagernet/utls.(*Conn).readRecord +func utlsReadRecord(c *tls.Conn) error + +//go:linkname utlsHandlePostHandshakeMessage github.com/sagernet/utls.(*Conn).handlePostHandshakeMessage +func utlsHandlePostHandshakeMessage(c *tls.Conn) error From f31c6182bb933b34716d6cfb746eb3b2973fafe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 5 Jan 2024 14:06:15 +0800 Subject: [PATCH 05/29] Remove `PROCESS_NAME_NATIVE` dwFlag in process query output The `process_path` rule of sing-box is inherited from Clash, the original code uses the local system's path format (e.g. `\Device\HarddiskVolume1\folder\program.exe`), but when the device has multiple disks, the HarddiskVolume serial number is not stable. This change make QueryFullProcessImageNameW output a Win32 path (such as `C:\folder\program.exe`), which will disrupt the existing `process_path` use cases in Windows. --- common/process/searcher_windows.go | 2 +- docs/migration.md | 15 +++++++++++++++ docs/migration.zh.md | 15 +++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/common/process/searcher_windows.go b/common/process/searcher_windows.go index f13b440e28..5b3d59b5ab 100644 --- a/common/process/searcher_windows.go +++ b/common/process/searcher_windows.go @@ -223,7 +223,7 @@ func getExecPathFromPID(pid uint32) (string, error) { r1, _, err := syscall.SyscallN( procQueryFullProcessImageNameW.Addr(), uintptr(h), - uintptr(1), + uintptr(0), uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&size)), ) diff --git a/docs/migration.md b/docs/migration.md index 44ddd8337d..b6ac0d8ab7 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -2,6 +2,21 @@ icon: material/arrange-bring-forward --- +## 1.9.0 + +!!! warning "Unstable" + + This version is still under development, and the following migration guide may be changed in the future. + +### `process_path` format update on Windows + +The `process_path` rule of sing-box is inherited from Clash, +the original code uses the local system's path format (e.g. `\Device\HarddiskVolume1\folder\program.exe`), +but when the device has multiple disks, the HarddiskVolume serial number is not stable. + +sing-box 1.9.0 make QueryFullProcessImageNameW output a Win32 path (such as `C:\folder\program.exe`), +which will disrupt the existing `process_path` use cases in Windows. + ## 1.8.0 ### :material-close-box: Migrate cache file from Clash API to independent options diff --git a/docs/migration.zh.md b/docs/migration.zh.md index 0422833d04..9998349f13 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -2,6 +2,21 @@ icon: material/arrange-bring-forward --- +## 1.9.0 + +!!! warning "不稳定的" + + 该版本仍在开发中,迁移指南可能将在未来更改。 + +### 对 Windows 上 `process_path` 格式的更新 + +sing-box 的 `process_path` 规则继承自Clash, +原始代码使用本地系统的路径格式(例如 `\Device\HarddiskVolume1\folder\program.exe`), +但是当设备有多个硬盘时,该 HarddiskVolume 系列号并不稳定。 + +sing-box 1.9.0 使 QueryFullProcessImageNameW 输出 Win32 路径(如 `C:\folder\program.exe`), +这将会破坏现有的 Windows `process_path` 用例。 + ## 1.8.0 ### :material-close-box: 将缓存文件从 Clash API 迁移到独立选项 From 096aef3f1bd221ee60fd1c84c48e62d08f627c63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 5 Jan 2024 14:19:53 +0800 Subject: [PATCH 06/29] Improve domain suffix match behavior For historical reasons, sing-box's `domain_suffix` rule matches literal prefixes instead of the same as other projects. This change modifies the behavior of `domain_suffix`: If the rule value is prefixed with `.`, the behavior is unchanged, otherwise it matches `(domain|.+\.domain)` instead. --- docs/migration.md | 7 +++++++ docs/migration.zh.md | 6 ++++++ go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/migration.md b/docs/migration.md index b6ac0d8ab7..b282a90fc7 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -8,6 +8,13 @@ icon: material/arrange-bring-forward 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. + +sing-box 1.9.0 modifies the behavior of `domain_suffix`: If the rule value is prefixed with `.`, +the behavior is unchanged, otherwise it matches `(domain|.+\.domain)` instead. + ### `process_path` format update on Windows The `process_path` rule of sing-box is inherited from Clash, diff --git a/docs/migration.zh.md b/docs/migration.zh.md index 9998349f13..bd63bf1767 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -8,6 +8,12 @@ icon: material/arrange-bring-forward 该版本仍在开发中,迁移指南可能将在未来更改。 +### `domain_suffix` 行为更新 + +由于历史原因,sing-box 的 `domain_suffix` 规则匹配字面前缀,而不与其他项目相同。 + +sing-box 1.9.0 修改了 `domain_suffix` 的行为:如果规则值以 `.` 为前缀则行为不变,否则改为匹配 `(domain|.+\.domain)`。 + ### 对 Windows 上 `process_path` 格式的更新 sing-box 的 `process_path` 规则继承自Clash, diff --git a/go.mod b/go.mod index f3a8717fde..67fa36aa3f 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e github.com/sagernet/quic-go v0.40.1 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 - github.com/sagernet/sing v0.3.8 + github.com/sagernet/sing v0.4.0-beta.18 github.com/sagernet/sing-dns v0.1.14 github.com/sagernet/sing-mux v0.2.0 github.com/sagernet/sing-quic v0.1.12 diff --git a/go.sum b/go.sum index b0a52310f8..3f73e42435 100644 --- a/go.sum +++ b/go.sum @@ -106,8 +106,8 @@ github.com/sagernet/quic-go v0.40.1/go.mod h1:CcKTpzTAISxrM4PA5M20/wYuz9Tj6Tx4Dw github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= -github.com/sagernet/sing v0.3.8 h1:gm4JKalPhydMYX2zFOTnnd4TXtM/16WFRqSjMepYQQk= -github.com/sagernet/sing v0.3.8/go.mod h1:+60H3Cm91RnL9dpVGWDPHt0zTQImO9Vfqt9a4rSambI= +github.com/sagernet/sing v0.4.0-beta.18 h1:oK+pvyXnFwxwvQkeUqgxIeATiMHcrH5doLKKDGNmQkU= +github.com/sagernet/sing v0.4.0-beta.18/go.mod h1:PFQKbElc2Pke7faBLv8oEba5ehtKO21Ho+TkYemTI3Y= github.com/sagernet/sing-dns v0.1.14 h1:kxE/Ik3jMXmD3sXsdt9MgrNzLFWt64mghV+MQqzyf40= github.com/sagernet/sing-dns v0.1.14/go.mod h1:AA+vZMNovuPN5i/sPnfF6756Nq94nzb5nXodMWbta5w= github.com/sagernet/sing-mux v0.2.0 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo= From a5e9da42f82f75103cf3a9bd1d08bd238d3531a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 22 Feb 2024 21:20:23 +0800 Subject: [PATCH 07/29] Handle Windows power events --- route/router.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/route/router.go b/route/router.go index 3f5d2f40bd..fa602989c5 100644 --- a/route/router.go +++ b/route/router.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "os/user" + "runtime" "strings" "time" @@ -42,6 +43,7 @@ import ( serviceNTP "github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/task" "github.com/sagernet/sing/common/uot" + "github.com/sagernet/sing/common/winpowrprof" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" ) @@ -85,6 +87,7 @@ type Router struct { networkMonitor tun.NetworkUpdateMonitor interfaceMonitor tun.DefaultInterfaceMonitor packageManager tun.PackageManager + powerListener winpowrprof.EventListener processSearcher process.Searcher timeService *ntp.Service pauseManager pause.Manager @@ -321,6 +324,14 @@ func NewRouter( router.interfaceMonitor = interfaceMonitor } + if runtime.GOOS == "windows" { + powerListener, err := winpowrprof.NewEventListener(router.notifyWindowsPowerEvent) + if err != nil { + return nil, E.Cause(err, "initialize power listener") + } + router.powerListener = powerListener + } + if ntpOptions.Enabled { timeService, err := ntp.NewService(ctx, router, logFactory.NewLogger("ntp"), ntpOptions) if err != nil { @@ -560,6 +571,16 @@ func (r *Router) Start() error { } } } + + if r.powerListener != nil { + monitor.Start("start power listener") + err := r.powerListener.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "start power listener") + } + } + if (needWIFIStateFromRuleSet || r.needWIFIState) && r.platformInterface != nil { monitor.Start("initialize WIFI state") r.needWIFIState = true @@ -657,6 +678,13 @@ func (r *Router) Close() error { }) monitor.Finish() } + if r.powerListener != nil { + monitor.Start("close power listener") + err = E.Append(err, r.powerListener.Close(), func(err error) error { + return E.Cause(err, "close power listener") + }) + monitor.Finish() + } if r.timeService != nil { monitor.Start("close time service") err = E.Append(err, r.timeService.Close(), func(err error) error { @@ -1189,3 +1217,19 @@ func (r *Router) updateWIFIState() { } } } + +func (r *Router) notifyWindowsPowerEvent(event int) { + switch event { + case winpowrprof.EVENT_SUSPEND: + r.pauseManager.DevicePause() + _ = r.ResetNetwork() + case winpowrprof.EVENT_RESUME: + if !r.pauseManager.IsDevicePaused() { + return + } + fallthrough + case winpowrprof.EVENT_RESUME_AUTOMATIC: + r.pauseManager.DeviceWake() + _ = r.ResetNetwork() + } +} From 97024dde323ba5218acd7a01c2dc1b48404c407a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 26 Mar 2024 20:58:47 +0800 Subject: [PATCH 08/29] Migrate ntp service to library --- ntp/service.go | 112 ------------------------------------------------ option/ntp.go | 3 +- route/router.go | 19 +++++--- 3 files changed, 14 insertions(+), 120 deletions(-) delete mode 100644 ntp/service.go diff --git a/ntp/service.go b/ntp/service.go deleted file mode 100644 index 70a41c0eda..0000000000 --- a/ntp/service.go +++ /dev/null @@ -1,112 +0,0 @@ -package ntp - -import ( - "context" - "os" - "time" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/dialer" - "github.com/sagernet/sing-box/common/settings" - 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" - "github.com/sagernet/sing/common/ntp" -) - -var _ ntp.TimeService = (*Service)(nil) - -type Service struct { - ctx context.Context - cancel common.ContextCancelCauseFunc - server M.Socksaddr - writeToSystem bool - dialer N.Dialer - logger logger.Logger - ticker *time.Ticker - clockOffset time.Duration -} - -func NewService(ctx context.Context, router adapter.Router, logger logger.Logger, options option.NTPOptions) (*Service, error) { - ctx, cancel := common.ContextWithCancelCause(ctx) - server := M.ParseSocksaddrHostPort(options.Server, options.ServerPort) - if server.Port == 0 { - server.Port = 123 - } - var interval time.Duration - if options.Interval > 0 { - interval = time.Duration(options.Interval) - } else { - interval = 30 * time.Minute - } - outboundDialer, err := dialer.New(router, options.DialerOptions) - if err != nil { - return nil, err - } - return &Service{ - ctx: ctx, - cancel: cancel, - server: server, - writeToSystem: options.WriteToSystem, - dialer: outboundDialer, - logger: logger, - ticker: time.NewTicker(interval), - }, nil -} - -func (s *Service) Start() error { - err := s.update() - if err != nil { - return E.Cause(err, "initialize time") - } - s.logger.Info("updated time: ", s.TimeFunc()().Local().Format(C.TimeLayout)) - go s.loopUpdate() - return nil -} - -func (s *Service) Close() error { - s.ticker.Stop() - s.cancel(os.ErrClosed) - return nil -} - -func (s *Service) TimeFunc() func() time.Time { - return func() time.Time { - return time.Now().Add(s.clockOffset) - } -} - -func (s *Service) loopUpdate() { - for { - select { - case <-s.ctx.Done(): - return - case <-s.ticker.C: - } - err := s.update() - if err == nil { - s.logger.Debug("updated time: ", s.TimeFunc()().Local().Format(C.TimeLayout)) - } else { - s.logger.Warn("update time: ", err) - } - } -} - -func (s *Service) update() error { - response, err := ntp.Exchange(s.ctx, s.dialer, s.server) - if err != nil { - return err - } - s.clockOffset = response.ClockOffset - if s.writeToSystem { - writeErr := settings.SetSystemTime(s.TimeFunc()()) - if writeErr != nil { - s.logger.Warn("write time to system: ", writeErr) - } - } - return nil -} diff --git a/option/ntp.go b/option/ntp.go index 000a658c2f..0bd2489ac2 100644 --- a/option/ntp.go +++ b/option/ntp.go @@ -2,9 +2,8 @@ package option type NTPOptions struct { Enabled bool `json:"enabled,omitempty"` - Server string `json:"server,omitempty"` - ServerPort uint16 `json:"server_port,omitempty"` Interval Duration `json:"interval,omitempty"` WriteToSystem bool `json:"write_to_system,omitempty"` + ServerOptions DialerOptions } diff --git a/route/router.go b/route/router.go index fa602989c5..15d8ef1061 100644 --- a/route/router.go +++ b/route/router.go @@ -23,7 +23,6 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/ntp" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing-box/transport/fakeip" @@ -40,7 +39,7 @@ import ( F "github.com/sagernet/sing/common/format" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" - serviceNTP "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/task" "github.com/sagernet/sing/common/uot" "github.com/sagernet/sing/common/winpowrprof" @@ -333,11 +332,19 @@ func NewRouter( } if ntpOptions.Enabled { - timeService, err := ntp.NewService(ctx, router, logFactory.NewLogger("ntp"), ntpOptions) + ntpDialer, err := dialer.New(router, ntpOptions.DialerOptions) if err != nil { - return nil, err - } - service.ContextWith[serviceNTP.TimeService](ctx, timeService) + return nil, E.Cause(err, "create NTP service") + } + timeService := ntp.NewService(ntp.Options{ + Context: ctx, + Dialer: ntpDialer, + Logger: logFactory.NewLogger("ntp"), + Server: ntpOptions.ServerOptions.Build(), + Interval: time.Duration(ntpOptions.Interval), + WriteToSystem: ntpOptions.WriteToSystem, + }) + service.MustRegister[ntp.TimeService](ctx, timeService) router.timeService = timeService } return router, nil From 48e8009d3cefdb3ccc8e5cf09fe96c489038710e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 8 Apr 2024 18:00:48 +0800 Subject: [PATCH 09/29] Set the default TCP keep alive period --- common/dialer/default.go | 3 +++ constant/timeout.go | 2 ++ inbound/default_tcp.go | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/common/dialer/default.go b/common/dialer/default.go index 0234b1b97f..91af85c524 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -63,6 +63,9 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi } else { dialer.Timeout = C.TCPTimeout } + // TODO: Add an option to customize the keep alive period + dialer.KeepAlive = C.TCPKeepAliveInitial + dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(C.TCPKeepAliveInitial, C.TCPKeepAliveInterval)) var udpFragment bool if options.UDPFragment != nil { udpFragment = *options.UDPFragment diff --git a/constant/timeout.go b/constant/timeout.go index 0d7a0b7d2d..b270a0500d 100644 --- a/constant/timeout.go +++ b/constant/timeout.go @@ -3,6 +3,8 @@ package constant import "time" const ( + TCPKeepAliveInitial = 10 * time.Minute + TCPKeepAliveInterval = 75 * time.Second TCPTimeout = 5 * time.Second ReadPayloadTimeout = 300 * time.Millisecond DNSTimeout = 10 * time.Second diff --git a/inbound/default_tcp.go b/inbound/default_tcp.go index 698801834a..d680c6951a 100644 --- a/inbound/default_tcp.go +++ b/inbound/default_tcp.go @@ -5,7 +5,9 @@ import ( "net" "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" @@ -16,6 +18,9 @@ func (a *myInboundAdapter) ListenTCP() (net.Listener, error) { bindAddr := M.SocksaddrFrom(a.listenOptions.Listen.Build(), a.listenOptions.ListenPort) var tcpListener net.Listener var listenConfig net.ListenConfig + // TODO: Add an option to customize the keep alive period + listenConfig.KeepAlive = C.TCPKeepAliveInitial + listenConfig.Control = control.Append(listenConfig.Control, control.SetKeepAlivePeriod(C.TCPKeepAliveInitial, C.TCPKeepAliveInterval)) if a.listenOptions.TCPMultiPath { if !go121Available { return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.") From ca15de28baceeae143bcd60cebd3f96ec0d7d8d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 10 Apr 2024 20:27:43 +0800 Subject: [PATCH 10/29] Remove unused fakeip packet conn --- transport/fakeip/packet.go | 55 --------------------------------- transport/fakeip/packet_wait.go | 37 ---------------------- 2 files changed, 92 deletions(-) delete mode 100644 transport/fakeip/packet.go delete mode 100644 transport/fakeip/packet_wait.go diff --git a/transport/fakeip/packet.go b/transport/fakeip/packet.go deleted file mode 100644 index 620acb929d..0000000000 --- a/transport/fakeip/packet.go +++ /dev/null @@ -1,55 +0,0 @@ -package fakeip - -import ( - "github.com/sagernet/sing/common/buf" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -var _ N.PacketConn = (*NATPacketConn)(nil) - -type NATPacketConn struct { - N.PacketConn - origin M.Socksaddr - destination M.Socksaddr -} - -func NewNATPacketConn(conn N.PacketConn, origin M.Socksaddr, destination M.Socksaddr) *NATPacketConn { - return &NATPacketConn{ - PacketConn: conn, - origin: socksaddrWithoutPort(origin), - destination: socksaddrWithoutPort(destination), - } -} - -func (c *NATPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { - destination, err = c.PacketConn.ReadPacket(buffer) - if socksaddrWithoutPort(destination) == c.origin { - destination = M.Socksaddr{ - Addr: c.destination.Addr, - Fqdn: c.destination.Fqdn, - Port: destination.Port, - } - } - return -} - -func (c *NATPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { - if socksaddrWithoutPort(destination) == c.destination { - destination = M.Socksaddr{ - Addr: c.origin.Addr, - Fqdn: c.origin.Fqdn, - Port: destination.Port, - } - } - return c.PacketConn.WritePacket(buffer, destination) -} - -func (c *NATPacketConn) Upstream() any { - return c.PacketConn -} - -func socksaddrWithoutPort(destination M.Socksaddr) M.Socksaddr { - destination.Port = 0 - return destination -} diff --git a/transport/fakeip/packet_wait.go b/transport/fakeip/packet_wait.go deleted file mode 100644 index 9fa4a5bd78..0000000000 --- a/transport/fakeip/packet_wait.go +++ /dev/null @@ -1,37 +0,0 @@ -package fakeip - -import ( - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -func (c *NATPacketConn) CreatePacketReadWaiter() (N.PacketReadWaiter, bool) { - waiter, created := bufio.CreatePacketReadWaiter(c.PacketConn) - if !created { - return nil, false - } - return &waitNATPacketConn{c, waiter}, true -} - -type waitNATPacketConn struct { - *NATPacketConn - readWaiter N.PacketReadWaiter -} - -func (c *waitNATPacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - return c.readWaiter.InitializeReadWaiter(options) -} - -func (c *waitNATPacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) { - buffer, destination, err = c.readWaiter.WaitReadPacket() - if err == nil && socksaddrWithoutPort(destination) == c.origin { - destination = M.Socksaddr{ - Addr: c.destination.Addr, - Fqdn: c.destination.Fqdn, - Port: destination.Port, - } - } - return -} From 444656af0487e1796dad3598a3a40c4efe4392c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 12 Apr 2024 09:24:49 +0800 Subject: [PATCH 11/29] Improve loopback detector --- experimental/libbox/config.go | 3 +- experimental/libbox/platform/interface.go | 10 +---- experimental/libbox/service.go | 6 +-- outbound/direct.go | 2 +- outbound/direct_loopback_detect.go | 21 ++++++--- route/interface_finder.go | 54 ----------------------- route/router.go | 17 +++---- 7 files changed, 26 insertions(+), 87 deletions(-) delete mode 100644 route/interface_finder.go diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index 42fc878f56..3e5cd9ea1b 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -9,7 +9,6 @@ import ( "github.com/sagernet/sing-box" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/process" - "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" @@ -75,7 +74,7 @@ func (s *platformInterfaceStub) UsePlatformInterfaceGetter() bool { return true } -func (s *platformInterfaceStub) Interfaces() ([]platform.NetworkInterface, error) { +func (s *platformInterfaceStub) Interfaces() ([]control.Interface, error) { return nil, os.ErrInvalid } diff --git a/experimental/libbox/platform/interface.go b/experimental/libbox/platform/interface.go index 54d35fa315..b250c8ae38 100644 --- a/experimental/libbox/platform/interface.go +++ b/experimental/libbox/platform/interface.go @@ -2,7 +2,6 @@ package platform import ( "context" - "net/netip" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/process" @@ -20,16 +19,9 @@ type Interface interface { UsePlatformDefaultInterfaceMonitor() bool CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor UsePlatformInterfaceGetter() bool - Interfaces() ([]NetworkInterface, error) + Interfaces() ([]control.Interface, error) UnderNetworkExtension() bool ClearDNSCache() ReadWIFIState() adapter.WIFIState process.Searcher } - -type NetworkInterface struct { - Index int - MTU int - Name string - Addresses []netip.Prefix -} diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 030aee8dce..2d755d0d4e 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -192,14 +192,14 @@ func (w *platformInterfaceWrapper) UsePlatformInterfaceGetter() bool { return w.iif.UsePlatformInterfaceGetter() } -func (w *platformInterfaceWrapper) Interfaces() ([]platform.NetworkInterface, error) { +func (w *platformInterfaceWrapper) Interfaces() ([]control.Interface, error) { interfaceIterator, err := w.iif.GetInterfaces() if err != nil { return nil, err } - var interfaces []platform.NetworkInterface + var interfaces []control.Interface for _, netInterface := range iteratorToArray[*NetworkInterface](interfaceIterator) { - interfaces = append(interfaces, platform.NetworkInterface{ + interfaces = append(interfaces, control.Interface{ Index: int(netInterface.Index), MTU: int(netInterface.MTU), Name: netInterface.Name, diff --git a/outbound/direct.go b/outbound/direct.go index 49ac760e64..11f650e4d9 100644 --- a/outbound/direct.go +++ b/outbound/direct.go @@ -51,7 +51,7 @@ func NewDirect(router adapter.Router, logger log.ContextLogger, tag string, opti domainStrategy: dns.DomainStrategy(options.DomainStrategy), fallbackDelay: time.Duration(options.FallbackDelay), dialer: outboundDialer, - loopBack: newLoopBackDetector(), + loopBack: newLoopBackDetector(router), } if options.ProxyProtocol != 0 { return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0") diff --git a/outbound/direct_loopback_detect.go b/outbound/direct_loopback_detect.go index 62cff87662..1469b9d017 100644 --- a/outbound/direct_loopback_detect.go +++ b/outbound/direct_loopback_detect.go @@ -5,21 +5,22 @@ import ( "net/netip" "sync" + "github.com/sagernet/sing-box/adapter" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) type loopBackDetector struct { - // router adapter.Router + router adapter.Router connAccess sync.RWMutex packetConnAccess sync.RWMutex connMap map[netip.AddrPort]netip.AddrPort packetConnMap map[uint16]uint16 } -func newLoopBackDetector( /*router adapter.Router*/ ) *loopBackDetector { +func newLoopBackDetector(router adapter.Router) *loopBackDetector { return &loopBackDetector{ - // router: router, + router: router, connMap: make(map[netip.AddrPort]netip.AddrPort), packetConnMap: make(map[uint16]uint16), } @@ -31,12 +32,12 @@ func (l *loopBackDetector) NewConn(conn net.Conn) net.Conn { return conn } if udpConn, isUDPConn := conn.(abstractUDPConn); isUDPConn { - /*if !source.Addr().IsLoopback() { + if !source.Addr().IsLoopback() { _, err := l.router.InterfaceFinder().InterfaceByAddr(source.Addr()) if err != nil { return conn } - }*/ + } if !N.IsPublicAddr(source.Addr()) { return conn } @@ -57,6 +58,12 @@ func (l *loopBackDetector) NewPacketConn(conn N.NetPacketConn, destination M.Soc if !source.IsValid() { return conn } + if !source.Addr().IsLoopback() { + _, err := l.router.InterfaceFinder().InterfaceByAddr(source.Addr()) + if err != nil { + return conn + } + } l.packetConnAccess.Lock() l.packetConnMap[source.Port()] = destination.AddrPort().Port() l.packetConnAccess.Unlock() @@ -74,12 +81,12 @@ func (l *loopBackDetector) CheckPacketConn(source netip.AddrPort, local netip.Ad if !source.IsValid() { return false } - /*if !source.Addr().IsLoopback() { + if !source.Addr().IsLoopback() { _, err := l.router.InterfaceFinder().InterfaceByAddr(source.Addr()) if err != nil { return false } - }*/ + } if N.IsPublicAddr(source.Addr()) { return false } diff --git a/route/interface_finder.go b/route/interface_finder.go deleted file mode 100644 index 850f091f81..0000000000 --- a/route/interface_finder.go +++ /dev/null @@ -1,54 +0,0 @@ -package route - -import ( - "net" - - "github.com/sagernet/sing/common/control" -) - -var _ control.InterfaceFinder = (*myInterfaceFinder)(nil) - -type myInterfaceFinder struct { - interfaces []net.Interface -} - -func (f *myInterfaceFinder) update() error { - ifs, err := net.Interfaces() - if err != nil { - return err - } - f.interfaces = ifs - return nil -} - -func (f *myInterfaceFinder) updateInterfaces(interfaces []net.Interface) { - f.interfaces = interfaces -} - -func (f *myInterfaceFinder) InterfaceIndexByName(name string) (interfaceIndex int, err error) { - for _, netInterface := range f.interfaces { - if netInterface.Name == name { - return netInterface.Index, nil - } - } - netInterface, err := net.InterfaceByName(name) - if err != nil { - return - } - f.update() - return netInterface.Index, nil -} - -func (f *myInterfaceFinder) InterfaceNameByIndex(index int) (interfaceName string, err error) { - for _, netInterface := range f.interfaces { - if netInterface.Index == index { - return netInterface.Name, nil - } - } - netInterface, err := net.InterfaceByIndex(index) - if err != nil { - return - } - f.update() - return netInterface.Name, nil -} diff --git a/route/router.go b/route/router.go index 15d8ef1061..f2fe1a6daa 100644 --- a/route/router.go +++ b/route/router.go @@ -79,7 +79,7 @@ type Router struct { transportDomainStrategy map[dns.Transport]dns.DomainStrategy dnsReverseMapping *DNSReverseMapping fakeIPStore adapter.FakeIPStore - interfaceFinder myInterfaceFinder + interfaceFinder *control.DefaultInterfaceFinder autoDetectInterface bool defaultInterface string defaultMark int @@ -124,6 +124,7 @@ func NewRouter( needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess, defaultDetour: options.Final, defaultDomainStrategy: dns.DomainStrategy(dnsOptions.Strategy), + interfaceFinder: control.NewDefaultInterfaceFinder(), autoDetectInterface: options.AutoDetectInterface, defaultInterface: options.DefaultInterface, defaultMark: options.DefaultMark, @@ -305,7 +306,7 @@ func NewRouter( } router.networkMonitor = networkMonitor networkMonitor.RegisterCallback(func() { - _ = router.interfaceFinder.update() + _ = router.interfaceFinder.Update() }) interfaceMonitor, err := tun.NewDefaultInterfaceMonitor(router.networkMonitor, router.logger, tun.DefaultInterfaceMonitorOptions{ OverrideAndroidVPN: options.OverrideAndroidVPN, @@ -1063,24 +1064,18 @@ func (r *Router) match0(ctx context.Context, metadata *adapter.InboundContext, d } func (r *Router) InterfaceFinder() control.InterfaceFinder { - return &r.interfaceFinder + return r.interfaceFinder } func (r *Router) UpdateInterfaces() error { if r.platformInterface == nil || !r.platformInterface.UsePlatformInterfaceGetter() { - return r.interfaceFinder.update() + return r.interfaceFinder.Update() } else { interfaces, err := r.platformInterface.Interfaces() if err != nil { return err } - r.interfaceFinder.updateInterfaces(common.Map(interfaces, func(it platform.NetworkInterface) net.Interface { - return net.Interface{ - Name: it.Name, - Index: it.Index, - MTU: it.MTU, - } - })) + r.interfaceFinder.UpdateInterfaces(interfaces) return nil } } From f5a404e23a316db42277afb8a1838ed89429c581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 17 Jan 2024 05:48:33 +0800 Subject: [PATCH 12/29] Fix timezone for Android and iOS --- constant/quic.go | 5 +++++ constant/quic_stub.go | 5 +++++ experimental/libbox/setup.go | 1 + inbound/naive.go | 5 ++--- include/quic.go | 2 -- include/quic_stub.go | 2 -- include/tz_android.go | 21 +++++++++++++++++++++ include/tz_ios.go | 30 ++++++++++++++++++++++++++++++ 8 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 constant/quic.go create mode 100644 constant/quic_stub.go create mode 100644 include/tz_android.go create mode 100644 include/tz_ios.go diff --git a/constant/quic.go b/constant/quic.go new file mode 100644 index 0000000000..50bddf8809 --- /dev/null +++ b/constant/quic.go @@ -0,0 +1,5 @@ +//go:build with_quic + +package constant + +const WithQUIC = true diff --git a/constant/quic_stub.go b/constant/quic_stub.go new file mode 100644 index 0000000000..95b47fefe4 --- /dev/null +++ b/constant/quic_stub.go @@ -0,0 +1,5 @@ +//go:build !with_quic + +package constant + +const WithQUIC = false diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go index a4514dfea9..ea468f391c 100644 --- a/experimental/libbox/setup.go +++ b/experimental/libbox/setup.go @@ -7,6 +7,7 @@ import ( "github.com/sagernet/sing-box/common/humanize" C "github.com/sagernet/sing-box/constant" + _ "github.com/sagernet/sing-box/include" ) var ( diff --git a/inbound/naive.go b/inbound/naive.go index 36bda492d8..07328c09f4 100644 --- a/inbound/naive.go +++ b/inbound/naive.go @@ -15,7 +15,6 @@ import ( "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" @@ -109,8 +108,8 @@ func (n *Naive) Start() error { if common.Contains(n.network, N.NetworkUDP) { err := n.configureHTTP3Listener() - if !include.WithQUIC && len(n.network) > 1 { - log.Warn(E.Cause(err, "naive http3 disabled")) + if !C.WithQUIC && len(n.network) > 1 { + n.logger.Warn(E.Cause(err, "naive http3 disabled")) } else if err != nil { return err } diff --git a/include/quic.go b/include/quic.go index 1e507f7b5f..1bcc0fbc90 100644 --- a/include/quic.go +++ b/include/quic.go @@ -6,5 +6,3 @@ import ( _ "github.com/sagernet/sing-box/transport/v2rayquic" _ "github.com/sagernet/sing-dns/quic" ) - -const WithQUIC = true diff --git a/include/quic_stub.go b/include/quic_stub.go index 682eb536c8..17b502a716 100644 --- a/include/quic_stub.go +++ b/include/quic_stub.go @@ -16,8 +16,6 @@ import ( N "github.com/sagernet/sing/common/network" ) -const WithQUIC = false - func init() { dns.RegisterTransport([]string{"quic", "h3"}, func(name string, ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, link string) (dns.Transport, error) { return nil, C.ErrQUICNotIncluded diff --git a/include/tz_android.go b/include/tz_android.go new file mode 100644 index 0000000000..7be1c2da98 --- /dev/null +++ b/include/tz_android.go @@ -0,0 +1,21 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// kanged from https://github.com/golang/mobile/blob/c713f31d574bb632a93f169b2cc99c9e753fef0e/app/android.go#L89 + +package include + +// #include +import "C" +import "time" + +func init() { + var currentT C.time_t + var currentTM C.struct_tm + C.time(¤tT) + C.localtime_r(¤tT, ¤tTM) + tzOffset := int(currentTM.tm_gmtoff) + tz := C.GoString(currentTM.tm_zone) + time.Local = time.FixedZone(tz, tzOffset) +} diff --git a/include/tz_ios.go b/include/tz_ios.go new file mode 100644 index 0000000000..fc30479c61 --- /dev/null +++ b/include/tz_ios.go @@ -0,0 +1,30 @@ +package include + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation +#import +const char* getSystemTimeZone() { + NSTimeZone *timeZone = [NSTimeZone systemTimeZone]; + NSString *timeZoneName = [timeZone description]; + return [timeZoneName UTF8String]; +} +*/ +import "C" + +import ( + "strings" + "time" +) + +func init() { + tzDescription := C.GoString(C.getSystemTimeZone()) + if len(tzDescription) == 0 { + return + } + location, err := time.LoadLocation(strings.Split(tzDescription, " ")[0]) + if err != nil { + return + } + time.Local = location +} From 4576d5cd4505f71a8ff0aec846bd737f833e29d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 3 Feb 2024 17:45:27 +0800 Subject: [PATCH 13/29] Add address filter support for DNS rules --- adapter/inbound.go | 13 +- adapter/router.go | 3 + docs/configuration/dns/index.md | 4 +- docs/configuration/dns/rule.md | 42 +++- docs/configuration/dns/rule.zh.md | 47 ++++- docs/configuration/experimental/cache-file.md | 4 - .../experimental/cache-file.zh.md | 4 - docs/configuration/experimental/clash-api.md | 4 - .../experimental/clash-api.zh.md | 4 - docs/configuration/experimental/index.md | 4 - docs/configuration/experimental/index.zh.md | 4 - docs/configuration/inbound/tun.md | 4 - docs/configuration/inbound/tun.zh.md | 4 - docs/configuration/outbound/wireguard.md | 4 - docs/configuration/outbound/wireguard.zh.md | 4 - docs/configuration/route/index.md | 4 - docs/configuration/route/index.zh.md | 4 - docs/configuration/route/rule.md | 4 - docs/configuration/route/rule.zh.md | 4 - docs/configuration/rule-set/headless-rule.md | 4 - docs/configuration/rule-set/index.md | 4 - docs/configuration/rule-set/source-format.md | 4 - docs/configuration/shared/tls.md | 5 - docs/configuration/shared/tls.zh.md | 4 - docs/manual/proxy/client.md | 110 ++++------- go.mod | 2 +- go.sum | 4 +- option/rule_dns.go | 3 + route/router_dns.go | 187 +++++++++++++----- route/router_rule.go | 6 +- route/rule_abstract.go | 24 ++- route/rule_default.go | 6 +- route/rule_dns.go | 91 +++++++++ route/rule_headless.go | 4 +- route/rule_item_rule_set.go | 9 +- route/rule_set_local.go | 7 + route/rule_set_remote.go | 7 + 37 files changed, 417 insertions(+), 229 deletions(-) diff --git a/adapter/inbound.go b/adapter/inbound.go index f32b804d21..4742f1b7b0 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -51,11 +51,13 @@ type InboundContext struct { // rule cache - IPCIDRMatchSource bool - SourceAddressMatch bool - SourcePortMatch bool - DestinationAddressMatch bool - DestinationPortMatch bool + IPCIDRMatchSource bool + SourceAddressMatch bool + SourcePortMatch bool + DestinationAddressMatch bool + DestinationPortMatch bool + DidMatch bool + IgnoreDestinationIPCIDRMatch bool } func (c *InboundContext) ResetRuleCache() { @@ -64,6 +66,7 @@ func (c *InboundContext) ResetRuleCache() { c.SourcePortMatch = false c.DestinationAddressMatch = false c.DestinationPortMatch = false + c.DidMatch = false } type inboundContextKey struct{} diff --git a/adapter/router.go b/adapter/router.go index 9d8bcb3e8a..b5eceb1f0d 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -86,6 +86,8 @@ type DNSRule interface { Rule DisableCache() bool RewriteTTL() *uint32 + WithAddressLimit() bool + MatchAddressLimit(metadata *InboundContext) bool } type RuleSet interface { @@ -99,6 +101,7 @@ type RuleSet interface { type RuleSetMetadata struct { ContainsProcessRule bool ContainsWIFIRule bool + ContainsIPCIDRRule bool } type RuleSetStartContext interface { diff --git a/docs/configuration/dns/index.md b/docs/configuration/dns/index.md index e2832c4275..cfb6bc6b16 100644 --- a/docs/configuration/dns/index.md +++ b/docs/configuration/dns/index.md @@ -21,8 +21,8 @@ ### Fields -| Key | Format | -|----------|--------------------------------| +| Key | Format | +|----------|---------------------------------| | `server` | List of [DNS Server](./server/) | | `rules` | List of [DNS Rule](./rule/) | | `fakeip` | [FakeIP](./fakeip/) | diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 68cc32cfae..26b86d956a 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -1,7 +1,13 @@ --- -icon: material/alert-decagram +icon: material/new-box --- +!!! quote "Changes in sing-box 1.9.0" + + :material-plus: [geoip](#geoip) + :material-plus: [ip_cidr](#ip_cidr) + :material-plus: [ip_is_private](#ip_is_private) + !!! quote "Changes in sing-box 1.8.0" :material-plus: [rule_set](#rule_set) @@ -53,11 +59,19 @@ icon: material/alert-decagram "source_geoip": [ "private" ], + "geoip": [ + "cn" + ], "source_ip_cidr": [ "10.0.0.0/24", "192.168.0.1" ], "source_ip_is_private": false, + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_is_private": false, "source_port": [ 12345 ], @@ -312,6 +326,32 @@ Disable cache and save cache in this query. Rewrite TTL in DNS responses. +### Address Filter Fields + +Only takes effect for IP address requests. When the query results do not match the address filtering rule items, the current rule will be skipped. + +!!! note "" + + `ip_cidr` items in included rule sets also takes effect as an address filtering field. + +#### geoip + +!!! question "Since sing-box 1.9.0" + +Match GeoIP with query response. + +#### ip_cidr + +!!! question "Since sing-box 1.9.0" + +Match IP CIDR with query response. + +#### ip_is_private + +!!! question "Since sing-box 1.9.0" + +Match private IP with query response. + ### Logical Fields #### type diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 5b1d75019a..ebc81c0f94 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -1,7 +1,13 @@ --- -icon: material/alert-decagram +icon: material/new-box --- +!!! quote "sing-box 1.9.0 中的更改" + + :material-plus: [geoip](#geoip) + :material-plus: [ip_cidr](#ip_cidr) + :material-plus: [ip_is_private](#ip_is_private) + !!! quote "sing-box 1.8.0 中的更改" :material-plus: [rule_set](#rule_set) @@ -53,10 +59,19 @@ icon: material/alert-decagram "source_geoip": [ "private" ], + "geoip": [ + "cn" + ], "source_ip_cidr": [ - "10.0.0.0/24" + "10.0.0.0/24", + "192.168.0.1" ], "source_ip_is_private": false, + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_is_private": false, "source_port": [ 12345 ], @@ -307,6 +322,32 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 重写 DNS 回应中的 TTL。 +### 地址筛选字段 + +仅对IP地址请求生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。 + +!!! note "" + + 引用的规则集中的 `ip_cidr` 项也作为地址筛选字段生效。 + +#### geoip + +!!! question "自 sing-box 1.9.0 起" + +与查询响应匹配 GeoIP。 + +#### ip_cidr + +!!! question "自 sing-box 1.9.0 起" + +与查询相应匹配 IP CIDR。 + +#### ip_is_private + +!!! question "自 sing-box 1.9.0 起" + +与查询响应匹配非公开 IP。 + ### 逻辑字段 #### type @@ -319,4 +360,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### rules -包括的规则。 \ No newline at end of file +包括的规则。 diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md index 66e30ef9b0..ca3f62e55f 100644 --- a/docs/configuration/experimental/cache-file.md +++ b/docs/configuration/experimental/cache-file.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - !!! question "Since sing-box 1.8.0" ### Structure diff --git a/docs/configuration/experimental/cache-file.zh.md b/docs/configuration/experimental/cache-file.zh.md index f4417ede45..da0ce39b06 100644 --- a/docs/configuration/experimental/cache-file.zh.md +++ b/docs/configuration/experimental/cache-file.zh.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - !!! question "自 sing-box 1.8.0 起" ### 结构 diff --git a/docs/configuration/experimental/clash-api.md b/docs/configuration/experimental/clash-api.md index 0525d14d64..e1ca981521 100644 --- a/docs/configuration/experimental/clash-api.md +++ b/docs/configuration/experimental/clash-api.md @@ -1,7 +1,3 @@ ---- -icon: material/alert-decagram ---- - !!! quote "Changes in sing-box 1.8.0" :material-delete-alert: [store_mode](#store_mode) diff --git a/docs/configuration/experimental/clash-api.zh.md b/docs/configuration/experimental/clash-api.zh.md index 5a490e587b..092769ac93 100644 --- a/docs/configuration/experimental/clash-api.zh.md +++ b/docs/configuration/experimental/clash-api.zh.md @@ -1,7 +1,3 @@ ---- -icon: material/alert-decagram ---- - !!! quote "sing-box 1.8.0 中的更改" :material-delete-alert: [store_mode](#store_mode) diff --git a/docs/configuration/experimental/index.md b/docs/configuration/experimental/index.md index 4ddcc41af7..a1a515cf85 100644 --- a/docs/configuration/experimental/index.md +++ b/docs/configuration/experimental/index.md @@ -1,7 +1,3 @@ ---- -icon: material/alert-decagram ---- - # Experimental !!! quote "Changes in sing-box 1.8.0" diff --git a/docs/configuration/experimental/index.zh.md b/docs/configuration/experimental/index.zh.md index 4be70aa7d4..01246c44ef 100644 --- a/docs/configuration/experimental/index.zh.md +++ b/docs/configuration/experimental/index.zh.md @@ -1,7 +1,3 @@ ---- -icon: material/alert-decagram ---- - # 实验性 !!! quote "sing-box 1.8.0 中的更改" diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 6e6c2ae027..2eed45535e 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -1,7 +1,3 @@ ---- -icon: material/alert-decagram ---- - !!! quote "Changes in sing-box 1.8.0" :material-plus: [gso](#gso) diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index 71c66704ba..05c7c3140b 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -1,7 +1,3 @@ ---- -icon: material/alert-decagram ---- - !!! quote "sing-box 1.8.0 中的更改" :material-plus: [gso](#gso) diff --git a/docs/configuration/outbound/wireguard.md b/docs/configuration/outbound/wireguard.md index 4cd91d2225..c3f51f1fb6 100644 --- a/docs/configuration/outbound/wireguard.md +++ b/docs/configuration/outbound/wireguard.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - !!! quote "Changes in sing-box 1.8.0" :material-plus: [gso](#gso) diff --git a/docs/configuration/outbound/wireguard.zh.md b/docs/configuration/outbound/wireguard.zh.md index e853d72e85..5de2813225 100644 --- a/docs/configuration/outbound/wireguard.zh.md +++ b/docs/configuration/outbound/wireguard.zh.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - !!! quote "sing-box 1.8.0 中的更改" :material-plus: [gso](#gso) diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 5deb44f5b7..7b2a7e7ef2 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -1,7 +1,3 @@ ---- -icon: material/alert-decagram ---- - # Route !!! quote "Changes in sing-box 1.8.0" diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md index 290268f4a7..68d4f66d91 100644 --- a/docs/configuration/route/index.zh.md +++ b/docs/configuration/route/index.zh.md @@ -1,7 +1,3 @@ ---- -icon: material/alert-decagram ---- - # 路由 !!! quote "sing-box 1.8.0 中的更改" diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 9bedef8675..b21bf658cd 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -1,7 +1,3 @@ ---- -icon: material/alert-decagram ---- - !!! quote "Changes in sing-box 1.8.0" :material-plus: [rule_set](#rule_set) diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index 0e6f989604..3f8b471573 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -1,7 +1,3 @@ ---- -icon: material/alert-decagram ---- - !!! quote "sing-box 1.8.0 中的更改" :material-plus: [rule_set](#rule_set) diff --git a/docs/configuration/rule-set/headless-rule.md b/docs/configuration/rule-set/headless-rule.md index 6ab62eb2e3..9998489915 100644 --- a/docs/configuration/rule-set/headless-rule.md +++ b/docs/configuration/rule-set/headless-rule.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - ### Structure !!! question "Since sing-box 1.8.0" diff --git a/docs/configuration/rule-set/index.md b/docs/configuration/rule-set/index.md index 5aff55b371..ba2f741e4f 100644 --- a/docs/configuration/rule-set/index.md +++ b/docs/configuration/rule-set/index.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - # Rule Set !!! question "Since sing-box 1.8.0" diff --git a/docs/configuration/rule-set/source-format.md b/docs/configuration/rule-set/source-format.md index 8e1934aec4..ee5e48e04c 100644 --- a/docs/configuration/rule-set/source-format.md +++ b/docs/configuration/rule-set/source-format.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - # Source Format !!! question "Since sing-box 1.8.0" diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index a5c7bec4c2..b1441a8abc 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -1,8 +1,3 @@ ---- -icon: material/alert-decagram ---- - - !!! quote "Changes in sing-box 1.8.0" :material-alert-decagram: [utls](#utls) diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md index 5a75945d15..360c453642 100644 --- a/docs/configuration/shared/tls.zh.md +++ b/docs/configuration/shared/tls.zh.md @@ -1,7 +1,3 @@ ---- -icon: material/alert-decagram ---- - !!! quote "sing-box 1.8.0 中的更改" :material-alert-decagram: [utls](#utls) diff --git a/docs/manual/proxy/client.md b/docs/manual/proxy/client.md index 3ba7eaccdc..41755cca79 100644 --- a/docs/manual/proxy/client.md +++ b/docs/manual/proxy/client.md @@ -290,10 +290,6 @@ flowchart TB === ":material-dns: DNS rules" - !!! info - - DNS rules are optional if FakeIP is used. - ```json { "dns": { @@ -322,19 +318,29 @@ flowchart TB "server": "google" }, { - "geosite": "geolocation-cn", + "rule_set": "geosite-geolocation-cn", "server": "local" } ] + }, + "route": { + "rule_set": [ + { + "type": "remote", + "tag": "geosite-geolocation-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs" + } + ] } } ``` -=== ":material-dns: DNS rules (1.8.0+)" +=== ":material-dns: DNS rules (1.9.0+)" - !!! info - - DNS rules are optional if FakeIP is used. + !!! warning "DNS leaks" + + The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS if using this method. ```json { @@ -346,7 +352,7 @@ flowchart TB }, { "tag": "local", - "address": "223.5.5.5", + "address": "https://223.5.5.5/dns-query", "detour": "direct" } ], @@ -366,6 +372,14 @@ flowchart TB { "rule_set": "geosite-geolocation-cn", "server": "local" + }, + { + "clash_mode": "Default", + "server": "google" + }, + { + "rule_set": "geoip-cn", + "server": "local" } ] }, @@ -376,80 +390,24 @@ flowchart TB "tag": "geosite-geolocation-cn", "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs" - } - ] - } - } - ``` - -=== ":material-router-network: Route rules" - - ```json - { - "outbounds": [ - { - "type": "direct", - "tag": "direct" - }, - { - "type": "block", - "tag": "block" - } - ], - "route": { - "rules": [ - { - "type": "logical", - "mode": "or", - "rules": [ - { - "protocol": "dns" - }, - { - "port": 53 - } - ], - "outbound": "dns" - }, - { - "geoip": "private", - "outbound": "direct" }, { - "clash_mode": "Direct", - "outbound": "direct" - }, - { - "clash_mode": "Global", - "outbound": "default" - }, - { - "type": "logical", - "mode": "or", - "rules": [ - { - "port": 853 - }, - { - "network": "udp", - "port": 443 - }, - { - "protocol": "stun" - } - ], - "outbound": "block" - }, - { - "geosite": "geolocation-cn", - "outbound": "direct" + "type": "remote", + "tag": "geoip-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" } ] + }, + "experimental": { + "clash_api": { + "default_mode": "Leak" + } } } ``` -=== ":material-router-network: Route rules (1.8.0+)" +=== ":material-router-network: Route rules" ```json { diff --git a/go.mod b/go.mod index 67fa36aa3f..8f355a7e4d 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/sagernet/quic-go v0.40.1 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 github.com/sagernet/sing v0.4.0-beta.18 - github.com/sagernet/sing-dns v0.1.14 + github.com/sagernet/sing-dns v0.2.0-beta.16 github.com/sagernet/sing-mux v0.2.0 github.com/sagernet/sing-quic v0.1.12 github.com/sagernet/sing-shadowsocks v0.2.6 diff --git a/go.sum b/go.sum index 3f73e42435..f070b62a82 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4Wk github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing v0.4.0-beta.18 h1:oK+pvyXnFwxwvQkeUqgxIeATiMHcrH5doLKKDGNmQkU= github.com/sagernet/sing v0.4.0-beta.18/go.mod h1:PFQKbElc2Pke7faBLv8oEba5ehtKO21Ho+TkYemTI3Y= -github.com/sagernet/sing-dns v0.1.14 h1:kxE/Ik3jMXmD3sXsdt9MgrNzLFWt64mghV+MQqzyf40= -github.com/sagernet/sing-dns v0.1.14/go.mod h1:AA+vZMNovuPN5i/sPnfF6756Nq94nzb5nXodMWbta5w= +github.com/sagernet/sing-dns v0.2.0-beta.16 h1:bzd4B8eHD7/WO3HrYknvgE8A56/R3n5oXBjNF97iPzQ= +github.com/sagernet/sing-dns v0.2.0-beta.16/go.mod h1:XU6Vqr6aHcMz/34Fcv8jmXpRCEuShzW+B7Qg1Xe1nxY= github.com/sagernet/sing-mux v0.2.0 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo= github.com/sagernet/sing-mux v0.2.0/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ= github.com/sagernet/sing-quic v0.1.12 h1:4KjG7LASZck0svGDfzf3aVNidRRQRC/w2HUMk/PHiNE= diff --git a/option/rule_dns.go b/option/rule_dns.go index 443f931475..d148e2645f 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -77,6 +77,9 @@ type DefaultDNSRule struct { DomainRegex Listable[string] `json:"domain_regex,omitempty"` Geosite Listable[string] `json:"geosite,omitempty"` SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` + GeoIP Listable[string] `json:"geoip,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,omitempty"` SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` SourcePort Listable[uint16] `json:"source_port,omitempty"` diff --git a/route/router_dns.go b/route/router_dns.go index 8ae9171002..ee767e9ee4 100644 --- a/route/router_dns.go +++ b/route/router_dns.go @@ -2,13 +2,13 @@ package route import ( "context" + "errors" "net/netip" "strings" "time" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-dns" "github.com/sagernet/sing/common/cache" E "github.com/sagernet/sing/common/exceptions" @@ -37,41 +37,51 @@ func (m *DNSReverseMapping) Query(address netip.Addr) (string, bool) { return domain, loaded } -func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool) (context.Context, dns.Transport, dns.DomainStrategy) { +func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, index int) (context.Context, dns.Transport, dns.DomainStrategy, adapter.DNSRule, int) { metadata := adapter.ContextFrom(ctx) if metadata == nil { panic("no context") } - for i, rule := range r.dnsRules { - metadata.ResetRuleCache() - if rule.Match(metadata) { - detour := rule.Outbound() - transport, loaded := r.transportMap[detour] - if !loaded { - r.dnsLogger.ErrorContext(ctx, "transport not found: ", detour) - continue - } - if _, isFakeIP := transport.(adapter.FakeIPTransport); isFakeIP && !allowFakeIP { - continue - } - r.dnsLogger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour) - if rule.DisableCache() { - ctx = dns.ContextWithDisableCache(ctx, true) - } - if rewriteTTL := rule.RewriteTTL(); rewriteTTL != nil { - ctx = dns.ContextWithRewriteTTL(ctx, *rewriteTTL) - } - if domainStrategy, dsLoaded := r.transportDomainStrategy[transport]; dsLoaded { - return ctx, transport, domainStrategy - } else { - return ctx, transport, r.defaultDomainStrategy + if index < len(r.dnsRules) { + dnsRules := r.dnsRules + if index != -1 { + dnsRules = dnsRules[index+1:] + } + for ruleIndex, rule := range dnsRules { + metadata.ResetRuleCache() + if rule.Match(metadata) { + detour := rule.Outbound() + transport, loaded := r.transportMap[detour] + if !loaded { + r.dnsLogger.ErrorContext(ctx, "transport not found: ", detour) + continue + } + if _, isFakeIP := transport.(adapter.FakeIPTransport); isFakeIP && !allowFakeIP { + continue + } + displayRuleIndex := ruleIndex + if index != -1 { + displayRuleIndex += index + 1 + } + r.dnsLogger.DebugContext(ctx, "match[", displayRuleIndex, "] ", rule.String(), " => ", detour) + if rule.DisableCache() { + ctx = dns.ContextWithDisableCache(ctx, true) + } + if rewriteTTL := rule.RewriteTTL(); rewriteTTL != nil { + ctx = dns.ContextWithRewriteTTL(ctx, *rewriteTTL) + } + if domainStrategy, dsLoaded := r.transportDomainStrategy[transport]; dsLoaded { + return ctx, transport, domainStrategy, rule, ruleIndex + } else { + return ctx, transport, r.defaultDomainStrategy, rule, ruleIndex + } } } } if domainStrategy, dsLoaded := r.transportDomainStrategy[r.defaultTransport]; dsLoaded { - return ctx, r.defaultTransport, domainStrategy + return ctx, r.defaultTransport, domainStrategy, nil, -1 } else { - return ctx, r.defaultTransport, r.defaultDomainStrategy + return ctx, r.defaultTransport, r.defaultDomainStrategy, nil, -1 } } @@ -86,7 +96,8 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er ) response, cached = r.dnsClient.ExchangeCache(ctx, message) if !cached { - ctx, metadata := adapter.AppendContext(ctx) + var metadata *adapter.InboundContext + ctx, metadata = adapter.AppendContext(ctx) if len(message.Question) > 0 { metadata.QueryType = message.Question[0].Qtype switch metadata.QueryType { @@ -97,17 +108,47 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er } metadata.Domain = fqdnToDomain(message.Question[0].Name) } - ctx, transport, strategy := r.matchDNS(ctx, true) - ctx, cancel := context.WithTimeout(ctx, C.DNSTimeout) - defer cancel() - response, err = r.dnsClient.Exchange(ctx, transport, message, strategy) - if err != nil && len(message.Question) > 0 { - r.dnsLogger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", formatQuestion(message.Question[0].String()))) + var ( + transport dns.Transport + strategy dns.DomainStrategy + rule adapter.DNSRule + ruleIndex int + ) + ruleIndex = -1 + for { + var ( + dnsCtx context.Context + cancel context.CancelFunc + addressLimit bool + ) + + dnsCtx, transport, strategy, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex) + dnsCtx, cancel = context.WithTimeout(dnsCtx, C.DNSTimeout) + if rule != nil && rule.WithAddressLimit() && isAddressQuery(message) { + addressLimit = true + response, err = r.dnsClient.ExchangeWithResponseCheck(dnsCtx, transport, message, strategy, func(response *mDNS.Msg) bool { + metadata.DestinationAddresses, _ = dns.MessageToAddresses(response) + return rule.MatchAddressLimit(metadata) + }) + } else { + addressLimit = false + response, err = r.dnsClient.Exchange(dnsCtx, transport, message, strategy) + } + cancel() + if err != nil { + if errors.Is(err, dns.ErrResponseRejected) { + r.dnsLogger.DebugContext(ctx, E.Cause(err, "response rejected for ", formatQuestion(message.Question[0].String()))) + } else if len(message.Question) > 0 { + r.dnsLogger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", formatQuestion(message.Question[0].String()))) + } else { + r.dnsLogger.ErrorContext(ctx, E.Cause(err, "exchange failed for ")) + } + } + if !addressLimit || err == nil { + break + } } } - if len(message.Question) > 0 && response != nil { - LogDNSAnswers(r.dnsLogger, ctx, message.Question[0].Name, response.Answer) - } if r.dnsReverseMapping != nil && len(message.Question) > 0 && response != nil && len(response.Answer) > 0 { for _, answer := range response.Answer { switch record := answer.(type) { @@ -125,22 +166,57 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS r.dnsLogger.DebugContext(ctx, "lookup domain ", domain) ctx, metadata := adapter.AppendContext(ctx) metadata.Domain = domain - ctx, transport, transportStrategy := r.matchDNS(ctx, false) - if strategy == dns.DomainStrategyAsIS { - strategy = transportStrategy + var ( + transport dns.Transport + transportStrategy dns.DomainStrategy + rule adapter.DNSRule + ruleIndex int + resultAddrs []netip.Addr + err error + ) + ruleIndex = -1 + for { + var ( + dnsCtx context.Context + cancel context.CancelFunc + addressLimit bool + ) + metadata.ResetRuleCache() + metadata.DestinationAddresses = nil + dnsCtx, transport, transportStrategy, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex) + if strategy == dns.DomainStrategyAsIS { + strategy = transportStrategy + } + dnsCtx, cancel = context.WithTimeout(dnsCtx, C.DNSTimeout) + if rule != nil && rule.WithAddressLimit() { + addressLimit = true + resultAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, strategy, func(responseAddrs []netip.Addr) bool { + metadata.DestinationAddresses = responseAddrs + return rule.MatchAddressLimit(metadata) + }) + } else { + addressLimit = false + resultAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, strategy) + } + cancel() + if err != nil { + if errors.Is(err, dns.ErrResponseRejected) { + r.dnsLogger.DebugContext(ctx, "response rejected for ", domain) + } else { + r.dnsLogger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain)) + } + } else if len(resultAddrs) == 0 { + r.dnsLogger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result") + err = dns.RCodeNameError + } + if !addressLimit || err == nil { + break + } } - ctx, cancel := context.WithTimeout(ctx, C.DNSTimeout) - defer cancel() - addrs, err := r.dnsClient.Lookup(ctx, transport, domain, strategy) - if len(addrs) > 0 { - r.dnsLogger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(addrs), " ")) - } else if err != nil { - r.dnsLogger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain)) - } else { - r.dnsLogger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result") - err = dns.RCodeNameError + if len(resultAddrs) > 0 { + r.dnsLogger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(resultAddrs), " ")) } - return addrs, err + return resultAddrs, err } func (r *Router) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) { @@ -154,10 +230,13 @@ func (r *Router) ClearDNSCache() { } } -func LogDNSAnswers(logger log.ContextLogger, ctx context.Context, domain string, answers []mDNS.RR) { - for _, answer := range answers { - logger.InfoContext(ctx, "exchanged ", domain, " ", mDNS.Type(answer.Header().Rrtype).String(), " ", formatQuestion(answer.String())) +func isAddressQuery(message *mDNS.Msg) bool { + for _, question := range message.Question { + if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { + return true + } } + return false } func fqdnToDomain(fqdn string) string { diff --git a/route/router_rule.go b/route/router_rule.go index 9850b5bc10..4a99a31cc3 100644 --- a/route/router_rule.go +++ b/route/router_rule.go @@ -59,7 +59,7 @@ func isGeoIPRule(rule option.DefaultRule) bool { } func isGeoIPDNSRule(rule option.DefaultDNSRule) bool { - return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) + return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) || len(rule.GeoIP) > 0 && common.Any(rule.GeoIP, notPrivateNode) } func isGeositeRule(rule option.DefaultRule) bool { @@ -97,3 +97,7 @@ func isWIFIDNSRule(rule option.DefaultDNSRule) bool { func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool { return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 } + +func isIPCIDRHeadlessRule(rule option.DefaultHeadlessRule) bool { + return len(rule.IPCIDR) > 0 || rule.IPSet != nil +} diff --git a/route/rule_abstract.go b/route/rule_abstract.go index 6decb9f37e..c13bdd8d96 100644 --- a/route/rule_abstract.go +++ b/route/rule_abstract.go @@ -15,6 +15,7 @@ type abstractDefaultRule struct { sourceAddressItems []RuleItem sourcePortItems []RuleItem destinationAddressItems []RuleItem + destinationIPCIDRItems []RuleItem destinationPortItems []RuleItem allItems []RuleItem ruleSetItem RuleItem @@ -64,6 +65,7 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool { } if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch { + metadata.DidMatch = true for _, item := range r.sourceAddressItems { if item.Match(metadata) { metadata.SourceAddressMatch = true @@ -73,6 +75,7 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool { } if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch { + metadata.DidMatch = true for _, item := range r.sourcePortItems { if item.Match(metadata) { metadata.SourcePortMatch = true @@ -82,6 +85,7 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool { } if len(r.destinationAddressItems) > 0 && !metadata.DestinationAddressMatch { + metadata.DidMatch = true for _, item := range r.destinationAddressItems { if item.Match(metadata) { metadata.DestinationAddressMatch = true @@ -90,7 +94,18 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool { } } + if !metadata.IgnoreDestinationIPCIDRMatch && len(r.destinationIPCIDRItems) > 0 && !metadata.DestinationAddressMatch { + metadata.DidMatch = true + for _, item := range r.destinationIPCIDRItems { + if item.Match(metadata) { + metadata.DestinationAddressMatch = true + break + } + } + } + if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch { + metadata.DidMatch = true for _, item := range r.destinationPortItems { if item.Match(metadata) { metadata.DestinationPortMatch = true @@ -100,6 +115,9 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool { } for _, item := range r.items { + if _, isRuleSet := item.(*RuleSetItem); !isRuleSet { + metadata.DidMatch = true + } if !item.Match(metadata) { return r.invert } @@ -113,7 +131,7 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool { return r.invert } - if len(r.destinationAddressItems) > 0 && !metadata.DestinationAddressMatch { + if ((!metadata.IgnoreDestinationIPCIDRMatch && len(r.destinationIPCIDRItems) > 0) || len(r.destinationAddressItems) > 0) && !metadata.DestinationAddressMatch { return r.invert } @@ -121,6 +139,10 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool { return r.invert } + if !metadata.DidMatch { + return true + } + return !r.invert } diff --git a/route/rule_default.go b/route/rule_default.go index d2227bb314..d1d13f7d72 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -109,7 +109,7 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt } if len(options.GeoIP) > 0 { item := NewGeoIPItem(router, logger, false, options.GeoIP) - rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } if len(options.SourceIPCIDR) > 0 { @@ -130,12 +130,12 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt if err != nil { return nil, E.Cause(err, "ipcidr") } - rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } if options.IPIsPrivate { item := NewIPIsPrivateItem(false) - rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } if len(options.SourcePort) > 0 { diff --git a/route/rule_dns.go b/route/rule_dns.go index c43f629083..3eab61f8a3 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -5,6 +5,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" ) @@ -111,6 +112,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } + if len(options.GeoIP) > 0 { + item := NewGeoIPItem(router, logger, false, options.GeoIP) + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) + rule.allItems = append(rule.allItems, item) + } if len(options.SourceIPCIDR) > 0 { item, err := NewIPCIDRItem(true, options.SourceIPCIDR) if err != nil { @@ -119,11 +125,24 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } + if len(options.IPCIDR) > 0 { + item, err := NewIPCIDRItem(false, options.IPCIDR) + if err != nil { + return nil, E.Cause(err, "ip_cidr") + } + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) + rule.allItems = append(rule.allItems, item) + } if options.SourceIPIsPrivate { item := NewIPIsPrivateItem(true) rule.sourceAddressItems = append(rule.sourceAddressItems, item) rule.allItems = append(rule.allItems, item) } + if options.IPIsPrivate { + item := NewIPIsPrivateItem(false) + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) + rule.allItems = append(rule.allItems, item) + } if len(options.SourcePort) > 0 { item := NewPortItem(true, options.SourcePort) rule.sourcePortItems = append(rule.sourcePortItems, item) @@ -211,6 +230,34 @@ func (r *DefaultDNSRule) RewriteTTL() *uint32 { return r.rewriteTTL } +func (r *DefaultDNSRule) WithAddressLimit() bool { + if len(r.destinationIPCIDRItems) > 0 { + return true + } + for _, rawRule := range r.items { + ruleSet, isRuleSet := rawRule.(*RuleSetItem) + if !isRuleSet { + continue + } + if ruleSet.ContainsIPCIDRRule() { + return true + } + } + return false +} + +func (r *DefaultDNSRule) Match(metadata *adapter.InboundContext) bool { + metadata.IgnoreDestinationIPCIDRMatch = true + defer func() { + metadata.IgnoreDestinationIPCIDRMatch = false + }() + return r.abstractDefaultRule.Match(metadata) +} + +func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool { + return r.abstractDefaultRule.Match(metadata) +} + var _ adapter.DNSRule = (*LogicalDNSRule)(nil) type LogicalDNSRule struct { @@ -254,3 +301,47 @@ func (r *LogicalDNSRule) DisableCache() bool { func (r *LogicalDNSRule) RewriteTTL() *uint32 { return r.rewriteTTL } + +func (r *LogicalDNSRule) WithAddressLimit() bool { + for _, rawRule := range r.rules { + switch rule := rawRule.(type) { + case *DefaultDNSRule: + if rule.WithAddressLimit() { + return true + } + case *LogicalDNSRule: + if rule.WithAddressLimit() { + return true + } + } + } + return false +} + +func (r *LogicalDNSRule) Match(metadata *adapter.InboundContext) bool { + if r.mode == C.LogicalTypeAnd { + return common.All(r.rules, func(it adapter.HeadlessRule) bool { + metadata.ResetRuleCache() + return it.(adapter.DNSRule).Match(metadata) + }) != r.invert + } else { + return common.Any(r.rules, func(it adapter.HeadlessRule) bool { + metadata.ResetRuleCache() + return it.(adapter.DNSRule).Match(metadata) + }) != r.invert + } +} + +func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool { + if r.mode == C.LogicalTypeAnd { + return common.All(r.rules, func(it adapter.HeadlessRule) bool { + metadata.ResetRuleCache() + return it.(adapter.DNSRule).MatchAddressLimit(metadata) + }) != r.invert + } else { + return common.Any(r.rules, func(it adapter.HeadlessRule) bool { + metadata.ResetRuleCache() + return it.(adapter.DNSRule).MatchAddressLimit(metadata) + }) != r.invert + } +} diff --git a/route/rule_headless.go b/route/rule_headless.go index 82c07d3102..92b6720c18 100644 --- a/route/rule_headless.go +++ b/route/rule_headless.go @@ -80,11 +80,11 @@ func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadles if err != nil { return nil, E.Cause(err, "ipcidr") } - rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } else if options.IPSet != nil { item := NewRawIPCIDRItem(false, options.IPSet) - rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) rule.allItems = append(rule.allItems, item) } if len(options.SourcePort) > 0 { diff --git a/route/rule_item_rule_set.go b/route/rule_item_rule_set.go index 959b2f6110..8354e4215d 100644 --- a/route/rule_item_rule_set.go +++ b/route/rule_item_rule_set.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" ) @@ -13,7 +14,7 @@ var _ RuleItem = (*RuleSetItem)(nil) type RuleSetItem struct { router adapter.Router tagList []string - setList []adapter.HeadlessRule + setList []adapter.RuleSet ipcidrMatchSource bool } @@ -46,6 +47,12 @@ func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { return false } +func (r *RuleSetItem) ContainsIPCIDRRule() bool { + return common.Any(r.setList, func(ruleSet adapter.RuleSet) bool { + return ruleSet.Metadata().ContainsIPCIDRRule + }) +} + func (r *RuleSetItem) String() string { if len(r.tagList) == 1 { return F.ToString("rule_set=", r.tagList[0]) diff --git a/route/rule_set_local.go b/route/rule_set_local.go index 635f22ed01..1fd0924636 100644 --- a/route/rule_set_local.go +++ b/route/rule_set_local.go @@ -3,12 +3,14 @@ package route import ( "context" "os" + "strings" "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" E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" ) @@ -55,6 +57,7 @@ func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleS var metadata adapter.RuleSetMetadata metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) + metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) return &LocalRuleSet{rules, metadata}, nil } @@ -67,6 +70,10 @@ func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { return false } +func (s *LocalRuleSet) String() string { + return strings.Join(F.MapToString(s.rules), " ") +} + func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error { return nil } diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go index 595e328c5c..a14c6fe543 100644 --- a/route/rule_set_remote.go +++ b/route/rule_set_remote.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "runtime" + "strings" "time" "github.com/sagernet/sing-box/adapter" @@ -14,6 +15,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" 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" @@ -68,6 +70,10 @@ func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { return false } +func (s *RemoteRuleSet) String() string { + return strings.Join(F.MapToString(s.rules), " ") +} + func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error { var dialer N.Dialer if s.options.RemoteOptions.DownloadDetour != "" { @@ -150,6 +156,7 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error { } s.metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) + s.metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) s.rules = rules return nil } From 581e8df86f8bbd751a007cb145ecffc065a7b09f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 9 Feb 2024 18:37:25 +0800 Subject: [PATCH 14/29] Add support for `client-subnet` DNS options --- adapter/router.go | 1 + docs/configuration/dns/index.md | 17 ++- docs/configuration/dns/index.zh.md | 17 +++ docs/configuration/dns/rule.md | 19 ++- docs/configuration/dns/rule.zh.md | 17 ++- docs/configuration/dns/server.md | 34 ++++-- docs/configuration/dns/server.zh.md | 32 +++-- docs/manual/proxy/client.md | 181 ++++++++++++++++++---------- experimental/libbox/dns.go | 8 +- include/dhcp_stub.go | 6 +- include/quic_stub.go | 3 +- option/dns.go | 2 + option/rule_dns.go | 15 ++- route/router.go | 22 +++- route/router_dns.go | 3 + route/rule_dns.go | 13 ++ transport/dhcp/server.go | 46 ++++--- transport/fakeip/server.go | 13 +- 18 files changed, 312 insertions(+), 137 deletions(-) diff --git a/adapter/router.go b/adapter/router.go index b5eceb1f0d..0d771deee7 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -86,6 +86,7 @@ type DNSRule interface { Rule DisableCache() bool RewriteTTL() *uint32 + ClientSubnet() *netip.Addr WithAddressLimit() bool MatchAddressLimit(metadata *InboundContext) bool } diff --git a/docs/configuration/dns/index.md b/docs/configuration/dns/index.md index cfb6bc6b16..71219dbb11 100644 --- a/docs/configuration/dns/index.md +++ b/docs/configuration/dns/index.md @@ -1,3 +1,11 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.9.0" + + :material-plus: [client_subnet](#client_subnet) + # DNS ### Structure @@ -13,6 +21,7 @@ "disable_expire": false, "independent_cache": false, "reverse_mapping": false, + "client_subnet": "", "fakeip": {} } } @@ -60,6 +69,10 @@ Stores a reverse mapping of IP addresses after responding to a DNS query in orde Since this process relies on the act of resolving domain names by an application before making a request, it can be problematic in environments such as macOS, where DNS is proxied and cached by the system. -#### fakeip +#### client_subnet + +!!! question "Since sing-box 1.9.0" + +Append a `edns0-subnet` OPT extra record with the specified IP address to every query by default. -[FakeIP](./fakeip/) settings. +Can be overrides by `servers.[].client_subnet` or `rules.[].client_subnet`. diff --git a/docs/configuration/dns/index.zh.md b/docs/configuration/dns/index.zh.md index afc6e9311c..164c37cd98 100644 --- a/docs/configuration/dns/index.zh.md +++ b/docs/configuration/dns/index.zh.md @@ -1,3 +1,11 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.9.0 中的更改" + + :material-plus: [client_subnet](#client_subnet) + # DNS ### 结构 @@ -13,6 +21,7 @@ "disable_expire": false, "independent_cache": false, "reverse_mapping": false, + "client_subnet": "", "fakeip": {} } } @@ -58,6 +67,14 @@ 由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。 +#### client_subnet + +!!! question "自 sing-box 1.9.0 起" + +默认情况下,将带有指定 IP 地址的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +可以被 `servers.[].client_subnet` 或 `rules.[].client_subnet` 覆盖。 + #### fakeip [FakeIP](./fakeip/) 设置。 diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 26b86d956a..5b42f20c04 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -6,7 +6,8 @@ icon: material/new-box :material-plus: [geoip](#geoip) :material-plus: [ip_cidr](#ip_cidr) - :material-plus: [ip_is_private](#ip_is_private) + :material-plus: [ip_is_private](#ip_is_private) + :material-plus: [client_subnet](#client_subnet) !!! quote "Changes in sing-box 1.8.0" @@ -121,7 +122,8 @@ icon: material/new-box ], "server": "local", "disable_cache": false, - "rewrite_ttl": 100 + "rewrite_ttl": 100, + "client_subnet": "127.0.0.1" }, { "type": "logical", @@ -129,7 +131,8 @@ icon: material/new-box "rules": [], "server": "local", "disable_cache": false, - "rewrite_ttl": 100 + "rewrite_ttl": 100, + "client_subnet": "127.0.0.1" } ] } @@ -280,8 +283,6 @@ Match Clash mode. #### wifi_ssid - - !!! quote "" Only supported in graphical clients on Android and iOS. @@ -326,6 +327,14 @@ Disable cache and save cache in this query. Rewrite TTL in DNS responses. +#### client_subnet + +!!! question "Since sing-box 1.9.0" + +Append a `edns0-subnet` OPT extra record with the specified IP address to every query by default. + +Will overrides `dns.client_subnet` and `servers.[].client_subnet`. + ### Address Filter Fields Only takes effect for IP address requests. When the query results do not match the address filtering rule items, the current rule will be skipped. diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index ebc81c0f94..eaeb8e682f 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -6,7 +6,8 @@ icon: material/new-box :material-plus: [geoip](#geoip) :material-plus: [ip_cidr](#ip_cidr) - :material-plus: [ip_is_private](#ip_is_private) + :material-plus: [ip_is_private](#ip_is_private) + :material-plus: [client_subnet](#client_subnet) !!! quote "sing-box 1.8.0 中的更改" @@ -120,14 +121,16 @@ icon: material/new-box "direct" ], "server": "local", - "disable_cache": false + "disable_cache": false, + "client_subnet": "127.0.0.1" }, { "type": "logical", "mode": "and", "rules": [], "server": "local", - "disable_cache": false + "disable_cache": false, + "client_subnet": "127.0.0.1" } ] } @@ -322,6 +325,14 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 重写 DNS 回应中的 TTL。 +#### client_subnet + +!!! question "自 sing-box 1.9.0 起" + +默认情况下,将带有指定 IP 地址的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +将覆盖 `dns.client_subnet` 与 `servers.[].client_subnet`。 + ### 地址筛选字段 仅对IP地址请求生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。 diff --git a/docs/configuration/dns/server.md b/docs/configuration/dns/server.md index 545810bf9e..e4d93544d5 100644 --- a/docs/configuration/dns/server.md +++ b/docs/configuration/dns/server.md @@ -1,3 +1,11 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.9.0" + + :material-plus: [client_subnet](#client_subnet) + ### Structure ```json @@ -5,17 +13,17 @@ "dns": { "servers": [ { - "tag": "google", - "address": "tls://dns.google", - "address_resolver": "local", - "address_strategy": "prefer_ipv4", - "strategy": "ipv4_only", - "detour": "direct" + "tag": "", + "address": "", + "address_resolver": "", + "address_strategy": "", + "strategy": "", + "detour": "", + "client_subnet": "" } ] } } - ``` ### Fields @@ -80,10 +88,20 @@ Default domain strategy for resolving the domain names. One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. -Take no effect if override by other settings. +Take no effect if overridden by other settings. #### detour Tag of an outbound for connecting to the dns server. Default outbound will be used if empty. + +#### client_subnet + +!!! question "Since sing-box 1.9.0" + +Append a `edns0-subnet` OPT extra record with the specified IP address to every query by default. + +Can be overrides by `rules.[].client_subnet`. + +Will overrides `dns.client_subnet`. diff --git a/docs/configuration/dns/server.zh.md b/docs/configuration/dns/server.zh.md index 36bcde5d3c..a15fdfd3d6 100644 --- a/docs/configuration/dns/server.zh.md +++ b/docs/configuration/dns/server.zh.md @@ -1,3 +1,11 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.9.0 中的更改" + + :material-plus: [client_subnet](#client_subnet) + ### 结构 ```json @@ -5,17 +13,17 @@ "dns": { "servers": [ { - "tag": "google", - "address": "tls://dns.google", - "address_resolver": "local", - "address_strategy": "prefer_ipv4", - "strategy": "ipv4_only", - "detour": "direct" + "tag": "", + "address": "", + "address_resolver": "", + "address_strategy": "", + "strategy": "", + "detour": "", + "client_subnet": "" } ] } } - ``` ### 字段 @@ -87,3 +95,13 @@ DNS 服务器的地址。 用于连接到 DNS 服务器的出站的标签。 如果为空,将使用默认出站。 + +#### client_subnet + +!!! question "自 sing-box 1.9.0 起" + +默认情况下,将带有指定 IP 地址的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +可以被 `rules.[].client_subnet` 覆盖。 + +将覆盖 `dns.client_subnet`。 diff --git a/docs/manual/proxy/client.md b/docs/manual/proxy/client.md index 41755cca79..12a8303962 100644 --- a/docs/manual/proxy/client.md +++ b/docs/manual/proxy/client.md @@ -338,74 +338,131 @@ flowchart TB === ":material-dns: DNS rules (1.9.0+)" - !!! warning "DNS leaks" - - The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS if using this method. - - ```json - { - "dns": { - "servers": [ - { - "tag": "google", - "address": "tls://8.8.8.8" - }, - { - "tag": "local", - "address": "https://223.5.5.5/dns-query", - "detour": "direct" - } - ], - "rules": [ - { - "outbound": "any", - "server": "local" - }, - { - "clash_mode": "Direct", - "server": "local" - }, - { - "clash_mode": "Global", - "server": "google" - }, - { - "rule_set": "geosite-geolocation-cn", - "server": "local" + === ":material-shield-off: With DNS Leaks" + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "address": "tls://8.8.8.8" + }, + { + "tag": "local", + "address": "https://223.5.5.5/dns-query", + "detour": "direct" + } + ], + "rules": [ + { + "outbound": "any", + "server": "local" + }, + { + "clash_mode": "Direct", + "server": "local" + }, + { + "clash_mode": "Global", + "server": "google" + }, + { + "rule_set": "geosite-geolocation-cn", + "server": "local" + }, + { + "clash_mode": "Default", + "server": "google" + }, + { + "rule_set": "geoip-cn", + "server": "local" + } + ] }, - { - "clash_mode": "Default", - "server": "google" + "route": { + "rule_set": [ + { + "type": "remote", + "tag": "geosite-geolocation-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs" + }, + { + "type": "remote", + "tag": "geoip-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" + } + ] }, - { - "rule_set": "geoip-cn", - "server": "local" + "experimental": { + "clash_api": { + "default_mode": "Leak" + } } - ] - }, - "route": { - "rule_set": [ - { - "type": "remote", - "tag": "geosite-geolocation-cn", - "format": "binary", - "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs" + } + ``` + + === ":material-security: Without DNS Leaks (1.9.0-alpha.2+)" + + ```json + { + "dns": { + "servers": [ + { + "tag": "google", + "address": "tls://8.8.8.8" + }, + { + "tag": "local", + "address": "https://223.5.5.5/dns-query", + "detour": "direct" + } + ], + "rules": [ + { + "outbound": "any", + "server": "local" + }, + { + "clash_mode": "Direct", + "server": "local" + }, + { + "clash_mode": "Global", + "server": "google" + }, + { + "rule_set": "geosite-geolocation-cn", + "server": "local" + }, + { + "rule_set": "geoip-cn", + "server": "google", + "client_subnet": "114.114.114.114" // Any China client IP address + } + ] }, - { - "type": "remote", - "tag": "geoip-cn", - "format": "binary", - "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" + "route": { + "rule_set": [ + { + "type": "remote", + "tag": "geosite-geolocation-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs" + }, + { + "type": "remote", + "tag": "geoip-cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" + } + ] } - ] - }, - "experimental": { - "clash_api": { - "default_mode": "Leak" } - } - } - ``` + ``` === ":material-router-network: Route rules" diff --git a/experimental/libbox/dns.go b/experimental/libbox/dns.go index fcdaaa9225..e1f8bcc3b6 100644 --- a/experimental/libbox/dns.go +++ b/experimental/libbox/dns.go @@ -9,9 +9,7 @@ import ( "github.com/sagernet/sing-dns" "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" "github.com/sagernet/sing/common/task" mDNS "github.com/miekg/dns" @@ -25,9 +23,11 @@ type LocalDNSTransport interface { func RegisterLocalDNSTransport(transport LocalDNSTransport) { if transport == nil { - dns.RegisterTransport([]string{"local"}, dns.CreateLocalTransport) + dns.RegisterTransport([]string{"local"}, func(options dns.TransportOptions) (dns.Transport, error) { + return dns.NewLocalTransport(options), nil + }) } else { - dns.RegisterTransport([]string{"local"}, func(name string, ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, link string) (dns.Transport, error) { + dns.RegisterTransport([]string{"local"}, func(options dns.TransportOptions) (dns.Transport, error) { return &platformLocalDNSTransport{ iif: transport, }, nil diff --git a/include/dhcp_stub.go b/include/dhcp_stub.go index c57aa43094..47a19d2e57 100644 --- a/include/dhcp_stub.go +++ b/include/dhcp_stub.go @@ -3,16 +3,12 @@ package include import ( - "context" - "github.com/sagernet/sing-dns" E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/logger" - N "github.com/sagernet/sing/common/network" ) func init() { - dns.RegisterTransport([]string{"dhcp"}, func(name string, ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, link string) (dns.Transport, error) { + dns.RegisterTransport([]string{"dhcp"}, func(options dns.TransportOptions) (dns.Transport, error) { return nil, E.New(`DHCP is not included in this build, rebuild with -tags with_dhcp`) }) } diff --git a/include/quic_stub.go b/include/quic_stub.go index 17b502a716..ddf9723f9a 100644 --- a/include/quic_stub.go +++ b/include/quic_stub.go @@ -11,13 +11,12 @@ import ( "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2ray" "github.com/sagernet/sing-dns" - "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) func init() { - dns.RegisterTransport([]string{"quic", "h3"}, func(name string, ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, link string) (dns.Transport, error) { + dns.RegisterTransport([]string{"quic", "h3"}, func(options dns.TransportOptions) (dns.Transport, error) { return nil, C.ErrQUICNotIncluded }) v2ray.RegisterQUICConstructor( diff --git a/option/dns.go b/option/dns.go index e0d237b7d3..152013430a 100644 --- a/option/dns.go +++ b/option/dns.go @@ -19,6 +19,7 @@ type DNSServerOptions struct { AddressFallbackDelay Duration `json:"address_fallback_delay,omitempty"` Strategy DomainStrategy `json:"strategy,omitempty"` Detour string `json:"detour,omitempty"` + ClientSubnet *ListenAddress `json:"client_subnet,omitempty"` } type DNSClientOptions struct { @@ -26,6 +27,7 @@ type DNSClientOptions struct { DisableCache bool `json:"disable_cache,omitempty"` DisableExpire bool `json:"disable_expire,omitempty"` IndependentCache bool `json:"independent_cache,omitempty"` + ClientSubnet *ListenAddress `json:"client_subnet,omitempty"` } type DNSFakeIPOptions struct { diff --git a/option/rule_dns.go b/option/rule_dns.go index d148e2645f..dc5e5c2bdf 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -100,6 +100,7 @@ type DefaultDNSRule struct { Server string `json:"server,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *ListenAddress `json:"client_subnet,omitempty"` } func (r DefaultDNSRule) IsValid() bool { @@ -108,16 +109,18 @@ func (r DefaultDNSRule) IsValid() bool { defaultValue.Server = r.Server defaultValue.DisableCache = r.DisableCache defaultValue.RewriteTTL = r.RewriteTTL + defaultValue.ClientSubnet = r.ClientSubnet return !reflect.DeepEqual(r, defaultValue) } type LogicalDNSRule struct { - Mode string `json:"mode"` - Rules []DNSRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + Mode string `json:"mode"` + Rules []DNSRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` + Server string `json:"server,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *ListenAddress `json:"client_subnet,omitempty"` } func (r LogicalDNSRule) IsValid() bool { diff --git a/route/router.go b/route/router.go index f2fe1a6daa..ae57fc6ba1 100644 --- a/route/router.go +++ b/route/router.go @@ -225,7 +225,20 @@ func NewRouter( return nil, E.New("parse dns server[", tag, "]: missing address_resolver") } } - transport, err := dns.CreateTransport(tag, ctx, logFactory.NewLogger(F.ToString("dns/transport[", tag, "]")), detour, server.Address) + var clientSubnet netip.Addr + if server.ClientSubnet != nil { + clientSubnet = server.ClientSubnet.Build() + } else if dnsOptions.ClientSubnet != nil { + clientSubnet = dnsOptions.ClientSubnet.Build() + } + transport, err := dns.CreateTransport(dns.TransportOptions{ + Context: ctx, + Logger: logFactory.NewLogger(F.ToString("dns/transport[", tag, "]")), + Name: tag, + Dialer: detour, + Address: server.Address, + ClientSubnet: clientSubnet, + }) if err != nil { return nil, E.Cause(err, "parse dns server[", tag, "]") } @@ -265,7 +278,12 @@ func NewRouter( } if defaultTransport == nil { if len(transports) == 0 { - transports = append(transports, dns.NewLocalTransport("local", N.SystemDialer)) + transports = append(transports, common.Must1(dns.CreateTransport(dns.TransportOptions{ + Context: ctx, + Name: "local", + Address: "local", + Dialer: common.Must1(dialer.NewDefault(router, option.DialerOptions{})), + }))) } defaultTransport = transports[0] } diff --git a/route/router_dns.go b/route/router_dns.go index ee767e9ee4..7114882b08 100644 --- a/route/router_dns.go +++ b/route/router_dns.go @@ -70,6 +70,9 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, index int) (con if rewriteTTL := rule.RewriteTTL(); rewriteTTL != nil { ctx = dns.ContextWithRewriteTTL(ctx, *rewriteTTL) } + if clientSubnet := rule.ClientSubnet(); clientSubnet != nil { + ctx = dns.ContextWithClientSubnet(ctx, *clientSubnet) + } if domainStrategy, dsLoaded := r.transportDomainStrategy[transport]; dsLoaded { return ctx, transport, domainStrategy, rule, ruleIndex } else { diff --git a/route/rule_dns.go b/route/rule_dns.go index 3eab61f8a3..153c9dcb4f 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -1,6 +1,8 @@ package route import ( + "net/netip" + "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" @@ -38,6 +40,7 @@ type DefaultDNSRule struct { abstractDefaultRule disableCache bool rewriteTTL *uint32 + clientSubnet *netip.Addr } func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) { @@ -48,6 +51,7 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options }, disableCache: options.DisableCache, rewriteTTL: options.RewriteTTL, + clientSubnet: (*netip.Addr)(options.ClientSubnet), } if len(options.Inbound) > 0 { item := NewInboundRule(options.Inbound) @@ -230,6 +234,10 @@ func (r *DefaultDNSRule) RewriteTTL() *uint32 { return r.rewriteTTL } +func (r *DefaultDNSRule) ClientSubnet() *netip.Addr { + return r.clientSubnet +} + func (r *DefaultDNSRule) WithAddressLimit() bool { if len(r.destinationIPCIDRItems) > 0 { return true @@ -264,6 +272,7 @@ type LogicalDNSRule struct { abstractLogicalRule disableCache bool rewriteTTL *uint32 + clientSubnet *netip.Addr } func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { @@ -302,6 +311,10 @@ func (r *LogicalDNSRule) RewriteTTL() *uint32 { return r.rewriteTTL } +func (r *LogicalDNSRule) ClientSubnet() *netip.Addr { + return r.clientSubnet +} + func (r *LogicalDNSRule) WithAddressLimit() bool { for _, rawRule := range r.rules { switch rule := rawRule.(type) { diff --git a/transport/dhcp/server.go b/transport/dhcp/server.go index 1a2c2938b2..2b7346c653 100644 --- a/transport/dhcp/server.go +++ b/transport/dhcp/server.go @@ -21,9 +21,6 @@ import ( "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" 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" "github.com/sagernet/sing/common/task" "github.com/sagernet/sing/common/x/list" @@ -32,14 +29,14 @@ import ( ) func init() { - dns.RegisterTransport([]string{"dhcp"}, NewTransport) + dns.RegisterTransport([]string{"dhcp"}, func(options dns.TransportOptions) (dns.Transport, error) { + return NewTransport(options) + }) } type Transport struct { - name string - ctx context.Context + options dns.TransportOptions router adapter.Router - logger logger.Logger interfaceName string autoInterface bool interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback] @@ -48,23 +45,20 @@ type Transport struct { updatedAt time.Time } -func NewTransport(name string, ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, link string) (dns.Transport, error) { - linkURL, err := url.Parse(link) +func NewTransport(options dns.TransportOptions) (*Transport, error) { + linkURL, err := url.Parse(options.Address) if err != nil { return nil, err } if linkURL.Host == "" { return nil, E.New("missing interface name for DHCP") } - router := adapter.RouterFromContext(ctx) + router := adapter.RouterFromContext(options.Context) if router == nil { return nil, E.New("missing router in context") } transport := &Transport{ - name: name, - ctx: ctx, router: router, - logger: logger, interfaceName: linkURL.Host, autoInterface: linkURL.Host == "auto", } @@ -72,7 +66,7 @@ func NewTransport(name string, ctx context.Context, logger logger.ContextLogger, } func (t *Transport) Name() string { - return t.name + return t.options.Name } func (t *Transport) Start() error { @@ -158,8 +152,8 @@ func (t *Transport) updateServers() error { return E.Cause(err, "dhcp: prepare interface") } - t.logger.Info("dhcp: query DNS servers on ", iface.Name) - fetchCtx, cancel := context.WithTimeout(t.ctx, C.DHCPTimeout) + t.options.Logger.Info("dhcp: query DNS servers on ", iface.Name) + fetchCtx, cancel := context.WithTimeout(t.options.Context, C.DHCPTimeout) err = t.fetchServers0(fetchCtx, iface) cancel() if err != nil { @@ -175,7 +169,7 @@ func (t *Transport) updateServers() error { func (t *Transport) interfaceUpdated(int) { err := t.updateServers() if err != nil { - t.logger.Error("update servers: ", err) + t.options.Logger.Error("update servers: ", err) } } @@ -187,7 +181,7 @@ func (t *Transport) fetchServers0(ctx context.Context, iface *net.Interface) err if runtime.GOOS == "linux" || runtime.GOOS == "android" { listenAddr = "255.255.255.255:68" } - packetConn, err := listener.ListenPacket(t.ctx, "udp4", listenAddr) + packetConn, err := listener.ListenPacket(t.options.Context, "udp4", listenAddr) if err != nil { return err } @@ -225,17 +219,17 @@ func (t *Transport) fetchServersResponse(iface *net.Interface, packetConn net.Pa dhcpPacket, err := dhcpv4.FromBytes(buffer.Bytes()) if err != nil { - t.logger.Trace("dhcp: parse DHCP response: ", err) + t.options.Logger.Trace("dhcp: parse DHCP response: ", err) return err } if dhcpPacket.MessageType() != dhcpv4.MessageTypeOffer { - t.logger.Trace("dhcp: expected OFFER response, but got ", dhcpPacket.MessageType()) + t.options.Logger.Trace("dhcp: expected OFFER response, but got ", dhcpPacket.MessageType()) continue } if dhcpPacket.TransactionID != transactionID { - t.logger.Trace("dhcp: expected transaction ID ", transactionID, ", but got ", dhcpPacket.TransactionID) + t.options.Logger.Trace("dhcp: expected transaction ID ", transactionID, ", but got ", dhcpPacket.TransactionID) continue } @@ -255,20 +249,22 @@ func (t *Transport) fetchServersResponse(iface *net.Interface, packetConn net.Pa func (t *Transport) recreateServers(iface *net.Interface, serverAddrs []netip.Addr) error { if len(serverAddrs) > 0 { - t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, func(it netip.Addr) string { + t.options.Logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, func(it netip.Addr) string { return it.String() }), ","), "]") } - serverDialer := common.Must1(dialer.NewDefault(t.router, option.DialerOptions{ BindInterface: iface.Name, UDPFragmentDefault: true, })) var transports []dns.Transport for _, serverAddr := range serverAddrs { - serverTransport, err := dns.NewUDPTransport(t.name, t.ctx, serverDialer, M.Socksaddr{Addr: serverAddr, Port: 53}) + newOptions := t.options + newOptions.Address = serverAddr.String() + newOptions.Dialer = serverDialer + serverTransport, err := dns.NewUDPTransport(newOptions) if err != nil { - return err + return E.Cause(err, "create UDP transport from DHCP result: ", serverAddr) } transports = append(transports, serverTransport) } diff --git a/transport/fakeip/server.go b/transport/fakeip/server.go index 40149aa494..5e0c7eef02 100644 --- a/transport/fakeip/server.go +++ b/transport/fakeip/server.go @@ -9,7 +9,6 @@ import ( "github.com/sagernet/sing-dns" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" - N "github.com/sagernet/sing/common/network" mDNS "github.com/miekg/dns" ) @@ -20,7 +19,9 @@ var ( ) func init() { - dns.RegisterTransport([]string{"fakeip"}, NewTransport) + dns.RegisterTransport([]string{"fakeip"}, func(options dns.TransportOptions) (dns.Transport, error) { + return NewTransport(options) + }) } type Transport struct { @@ -30,15 +31,15 @@ type Transport struct { logger logger.ContextLogger } -func NewTransport(name string, ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, link string) (dns.Transport, error) { - router := adapter.RouterFromContext(ctx) +func NewTransport(options dns.TransportOptions) (*Transport, error) { + router := adapter.RouterFromContext(options.Context) if router == nil { return nil, E.New("missing router in context") } return &Transport{ - name: name, + name: options.Name, router: router, - logger: logger, + logger: options.Logger, }, nil } From 992587ba0d381a83627d95862b7966df8f371292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 14 Feb 2024 20:42:58 +0800 Subject: [PATCH 15/29] Add rejected DNS response cache support --- adapter/experimental.go | 4 + docs/configuration/dns/rule.md | 6 +- docs/configuration/dns/rule.zh.md | 6 +- docs/configuration/experimental/cache-file.md | 32 +++++- .../experimental/cache-file.zh.md | 29 ++++- experimental/cachefile/cache.go | 34 ++++-- experimental/cachefile/fakeip.go | 18 ++-- experimental/cachefile/rdrc.go | 101 ++++++++++++++++++ option/experimental.go | 10 +- route/router.go | 17 ++- route/router_dns.go | 31 ++++-- transport/dhcp/server.go | 1 + 12 files changed, 253 insertions(+), 36 deletions(-) create mode 100644 experimental/cachefile/rdrc.go diff --git a/adapter/experimental.go b/adapter/experimental.go index 2a6776cd0e..5e1cbd9d9d 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -9,6 +9,7 @@ import ( "time" "github.com/sagernet/sing-box/common/urltest" + "github.com/sagernet/sing-dns" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/rw" ) @@ -30,6 +31,9 @@ type CacheFile interface { StoreFakeIP() bool FakeIPStorage + StoreRDRC() bool + dns.RDRCStore + LoadMode() string StoreMode(mode string) error LoadSelected(group string) string diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 5b42f20c04..84b9b66947 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -339,10 +339,14 @@ Will overrides `dns.client_subnet` and `servers.[].client_subnet`. Only takes effect for IP address requests. When the query results do not match the address filtering rule items, the current rule will be skipped. -!!! note "" +!!! info "" `ip_cidr` items in included rule sets also takes effect as an address filtering field. +!!! note "" + + Enable `experimental.cache_file.store_rdrc` to cache results. + #### geoip !!! question "Since sing-box 1.9.0" diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index eaeb8e682f..c7977bc164 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -337,10 +337,14 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 仅对IP地址请求生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。 -!!! note "" +!!! info "" 引用的规则集中的 `ip_cidr` 项也作为地址筛选字段生效。 +!!! note "" + + 启用 `experimental.cache_file.store_rdrc` 以缓存结果。 + #### geoip !!! question "自 sing-box 1.9.0 起" diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md index ca3f62e55f..b30538e591 100644 --- a/docs/configuration/experimental/cache-file.md +++ b/docs/configuration/experimental/cache-file.md @@ -1,5 +1,14 @@ +--- +icon: material/new-box +--- + !!! question "Since sing-box 1.8.0" +!!! quote "Changes in sing-box 1.9.0" + + :material-plus: [store_rdrc](#store_rdrc) + :material-plus: [rdrc_timeout](#rdrc_timeout) + ### Structure ```json @@ -7,7 +16,9 @@ "enabled": true, "path": "", "cache_id": "", - "store_fakeip": false + "store_fakeip": false, + "store_rdrc": false, + "rdrc_timeout": "" } ``` @@ -25,6 +36,23 @@ Path to the cache file. #### cache_id -Identifier in cache file. +Identifier in the cache file If not empty, configuration specified data will use a separate store keyed by it. + +#### store_fakeip + +Store fakeip in the cache file + +#### store_rdrc + +Store rejected DNS response cache in the cache file + +The check results of [Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) +will be cached until expiration. + +#### rdrc_timeout + +Timeout of rejected DNS response cache. + +`7d` is used by default. diff --git a/docs/configuration/experimental/cache-file.zh.md b/docs/configuration/experimental/cache-file.zh.md index da0ce39b06..6d86dc8423 100644 --- a/docs/configuration/experimental/cache-file.zh.md +++ b/docs/configuration/experimental/cache-file.zh.md @@ -1,5 +1,14 @@ +--- +icon: material/new-box +--- + !!! question "自 sing-box 1.8.0 起" +!!! quote "sing-box 1.9.0 中的更改" + + :material-plus: [store_rdrc](#store_rdrc) + :material-plus: [rdrc_timeout](#rdrc_timeout) + ### 结构 ```json @@ -7,7 +16,9 @@ "enabled": true, "path": "", "cache_id": "", - "store_fakeip": false + "store_fakeip": false, + "store_rdrc": false, + "rdrc_timeout": "" } ``` @@ -26,3 +37,19 @@ 缓存文件中的标识符。 如果不为空,配置特定的数据将使用由其键控的单独存储。 + +#### store_fakeip + +将 fakeip 存储在缓存文件中。 + +#### store_rdrc + +将拒绝的 DNS 响应缓存存储在缓存文件中。 + +[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#_3) 的检查结果将被缓存至过期。 + +#### rdrc_timeout + +拒绝的 DNS 响应缓存超时。 + +默认使用 `7d`。 diff --git a/experimental/cachefile/cache.go b/experimental/cachefile/cache.go index 43b8456215..9d45ea8eec 100644 --- a/experimental/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -29,6 +29,7 @@ var ( string(bucketExpand), string(bucketMode), string(bucketRuleSet), + string(bucketRDRC), } cacheIDDefault = []byte("default") @@ -37,17 +38,25 @@ var ( var _ adapter.CacheFile = (*CacheFile)(nil) type CacheFile struct { - ctx context.Context - path string - cacheID []byte - storeFakeIP bool - + ctx context.Context + path string + cacheID []byte + storeFakeIP bool + storeRDRC bool + rdrcTimeout time.Duration DB *bbolt.DB - saveAccess sync.RWMutex + saveMetadataTimer *time.Timer + saveFakeIPAccess sync.RWMutex saveDomain map[netip.Addr]string saveAddress4 map[string]netip.Addr saveAddress6 map[string]netip.Addr - saveMetadataTimer *time.Timer + saveRDRCAccess sync.RWMutex + saveRDRC map[saveRDRCCacheKey]bool +} + +type saveRDRCCacheKey struct { + TransportName string + QuestionName string } func New(ctx context.Context, options option.CacheFileOptions) *CacheFile { @@ -61,14 +70,25 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile { if options.CacheID != "" { cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...) } + var rdrcTimeout time.Duration + if options.StoreRDRC { + if options.RDRCTimeout > 0 { + rdrcTimeout = time.Duration(options.RDRCTimeout) + } else { + rdrcTimeout = 7 * 24 * time.Hour + } + } return &CacheFile{ ctx: ctx, path: filemanager.BasePath(ctx, path), cacheID: cacheIDBytes, storeFakeIP: options.StoreFakeIP, + storeRDRC: options.StoreRDRC, + rdrcTimeout: rdrcTimeout, saveDomain: make(map[netip.Addr]string), saveAddress4: make(map[string]netip.Addr), saveAddress6: make(map[string]netip.Addr), + saveRDRC: make(map[saveRDRCCacheKey]bool), } } diff --git a/experimental/cachefile/fakeip.go b/experimental/cachefile/fakeip.go index 690b0d9146..1c387da8cf 100644 --- a/experimental/cachefile/fakeip.go +++ b/experimental/cachefile/fakeip.go @@ -91,34 +91,34 @@ func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error { } func (c *CacheFile) FakeIPStoreAsync(address netip.Addr, domain string, logger logger.Logger) { - c.saveAccess.Lock() + c.saveFakeIPAccess.Lock() c.saveDomain[address] = domain if address.Is4() { c.saveAddress4[domain] = address } else { c.saveAddress6[domain] = address } - c.saveAccess.Unlock() + c.saveFakeIPAccess.Unlock() go func() { err := c.FakeIPStore(address, domain) if err != nil { - logger.Warn("save FakeIP address pair: ", err) + logger.Warn("save FakeIP cache: ", err) } - c.saveAccess.Lock() + c.saveFakeIPAccess.Lock() delete(c.saveDomain, address) if address.Is4() { delete(c.saveAddress4, domain) } else { delete(c.saveAddress6, domain) } - c.saveAccess.Unlock() + c.saveFakeIPAccess.Unlock() }() } func (c *CacheFile) FakeIPLoad(address netip.Addr) (string, bool) { - c.saveAccess.RLock() + c.saveFakeIPAccess.RLock() cachedDomain, cached := c.saveDomain[address] - c.saveAccess.RUnlock() + c.saveFakeIPAccess.RUnlock() if cached { return cachedDomain, true } @@ -139,13 +139,13 @@ func (c *CacheFile) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bo cachedAddress netip.Addr cached bool ) - c.saveAccess.RLock() + c.saveFakeIPAccess.RLock() if !isIPv6 { cachedAddress, cached = c.saveAddress4[domain] } else { cachedAddress, cached = c.saveAddress6[domain] } - c.saveAccess.RUnlock() + c.saveFakeIPAccess.RUnlock() if cached { return cachedAddress, true } diff --git a/experimental/cachefile/rdrc.go b/experimental/cachefile/rdrc.go new file mode 100644 index 0000000000..836beba177 --- /dev/null +++ b/experimental/cachefile/rdrc.go @@ -0,0 +1,101 @@ +package cachefile + +import ( + "encoding/binary" + "time" + + "github.com/sagernet/bbolt" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/logger" +) + +var bucketRDRC = []byte("rdrc") + +func (c *CacheFile) StoreRDRC() bool { + return c.storeRDRC +} + +func (c *CacheFile) RDRCTimeout() time.Duration { + return c.rdrcTimeout +} + +func (c *CacheFile) LoadRDRC(transportName string, qName string) (rejected bool) { + c.saveRDRCAccess.RLock() + rejected, cached := c.saveRDRC[saveRDRCCacheKey{transportName, qName}] + c.saveRDRCAccess.RUnlock() + if cached { + return + } + var deleteCache bool + err := c.DB.View(func(tx *bbolt.Tx) error { + bucket := c.bucket(tx, bucketRDRC) + if bucket == nil { + return nil + } + bucket = bucket.Bucket([]byte(transportName)) + if bucket == nil { + return nil + } + content := bucket.Get([]byte(qName)) + if content == nil { + return nil + } + expiresAt := time.Unix(int64(binary.BigEndian.Uint64(content)), 0) + if time.Now().After(expiresAt) { + deleteCache = true + return nil + } + rejected = true + return nil + }) + if err != nil { + return + } + if deleteCache { + c.DB.Update(func(tx *bbolt.Tx) error { + bucket := c.bucket(tx, bucketRDRC) + if bucket == nil { + return nil + } + bucket = bucket.Bucket([]byte(transportName)) + if bucket == nil { + return nil + } + return bucket.Delete([]byte(qName)) + }) + } + return +} + +func (c *CacheFile) SaveRDRC(transportName string, qName string) error { + return c.DB.Batch(func(tx *bbolt.Tx) error { + bucket, err := c.createBucket(tx, bucketRDRC) + if err != nil { + return err + } + bucket, err = bucket.CreateBucketIfNotExists([]byte(transportName)) + if err != nil { + return err + } + expiresAt := buf.Get(8) + defer buf.Put(expiresAt) + binary.BigEndian.PutUint64(expiresAt, uint64(time.Now().Add(c.rdrcTimeout).Unix())) + return bucket.Put([]byte(qName), expiresAt) + }) +} + +func (c *CacheFile) SaveRDRCAsync(transportName string, qName string, logger logger.Logger) { + saveKey := saveRDRCCacheKey{transportName, qName} + c.saveRDRCAccess.Lock() + c.saveRDRC[saveKey] = true + c.saveRDRCAccess.Unlock() + go func() { + err := c.SaveRDRC(transportName, qName) + if err != nil { + logger.Warn("save RDRC: ", err) + } + c.saveRDRCAccess.Lock() + delete(c.saveRDRC, saveKey) + c.saveRDRCAccess.Unlock() + }() +} diff --git a/option/experimental.go b/option/experimental.go index c685f51f54..9f6071baee 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -8,10 +8,12 @@ type ExperimentalOptions struct { } type CacheFileOptions struct { - Enabled bool `json:"enabled,omitempty"` - Path string `json:"path,omitempty"` - CacheID string `json:"cache_id,omitempty"` - StoreFakeIP bool `json:"store_fakeip,omitempty"` + Enabled bool `json:"enabled,omitempty"` + Path string `json:"path,omitempty"` + CacheID string `json:"cache_id,omitempty"` + StoreFakeIP bool `json:"store_fakeip,omitempty"` + StoreRDRC bool `json:"store_rdrc,omitempty"` + RDRCTimeout Duration `json:"rdrc_timeout,omitempty"` } type ClashAPIOptions struct { diff --git a/route/router.go b/route/router.go index ae57fc6ba1..e9807bd467 100644 --- a/route/router.go +++ b/route/router.go @@ -139,7 +139,17 @@ func NewRouter( DisableCache: dnsOptions.DNSClientOptions.DisableCache, DisableExpire: dnsOptions.DNSClientOptions.DisableExpire, IndependentCache: dnsOptions.DNSClientOptions.IndependentCache, - Logger: router.dnsLogger, + RDRC: func() dns.RDRCStore { + cacheFile := service.FromContext[adapter.CacheFile](ctx) + if cacheFile == nil { + return nil + } + if !cacheFile.StoreRDRC() { + return nil + } + return cacheFile + }, + Logger: router.dnsLogger, }) for i, ruleOptions := range options.Rules { routeRule, err := NewRule(router, router.logger, ruleOptions, true) @@ -625,6 +635,11 @@ func (r *Router) Start() error { return E.Cause(err, "initialize rule[", i, "]") } } + + monitor.Start("initialize DNS client") + r.dnsClient.Start() + monitor.Finish() + for i, rule := range r.dnsRules { monitor.Start("initialize DNS rule[", i, "]") err := rule.Start() diff --git a/route/router_dns.go b/route/router_dns.go index 7114882b08..4bcc4f2398 100644 --- a/route/router_dns.go +++ b/route/router_dns.go @@ -139,7 +139,9 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er } cancel() if err != nil { - if errors.Is(err, dns.ErrResponseRejected) { + if errors.Is(err, dns.ErrResponseRejectedCached) { + r.dnsLogger.DebugContext(ctx, E.Cause(err, "response rejected for ", formatQuestion(message.Question[0].String())), " (cached)") + } else if errors.Is(err, dns.ErrResponseRejected) { r.dnsLogger.DebugContext(ctx, E.Cause(err, "response rejected for ", formatQuestion(message.Question[0].String()))) } else if len(message.Question) > 0 { r.dnsLogger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", formatQuestion(message.Question[0].String()))) @@ -166,6 +168,15 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er } func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) { + var ( + responseAddrs []netip.Addr + cached bool + err error + ) + responseAddrs, cached = r.dnsClient.LookupCache(ctx, domain, strategy) + if cached { + return responseAddrs, nil + } r.dnsLogger.DebugContext(ctx, "lookup domain ", domain) ctx, metadata := adapter.AppendContext(ctx) metadata.Domain = domain @@ -174,8 +185,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS transportStrategy dns.DomainStrategy rule adapter.DNSRule ruleIndex int - resultAddrs []netip.Addr - err error ) ruleIndex = -1 for { @@ -193,22 +202,24 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS dnsCtx, cancel = context.WithTimeout(dnsCtx, C.DNSTimeout) if rule != nil && rule.WithAddressLimit() { addressLimit = true - resultAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, strategy, func(responseAddrs []netip.Addr) bool { + responseAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, strategy, func(responseAddrs []netip.Addr) bool { metadata.DestinationAddresses = responseAddrs return rule.MatchAddressLimit(metadata) }) } else { addressLimit = false - resultAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, strategy) + responseAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, strategy) } cancel() if err != nil { - if errors.Is(err, dns.ErrResponseRejected) { + if errors.Is(err, dns.ErrResponseRejectedCached) { + r.dnsLogger.DebugContext(ctx, "response rejected for ", domain, " (cached)") + } else if errors.Is(err, dns.ErrResponseRejected) { r.dnsLogger.DebugContext(ctx, "response rejected for ", domain) } else { r.dnsLogger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain)) } - } else if len(resultAddrs) == 0 { + } else if len(responseAddrs) == 0 { r.dnsLogger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result") err = dns.RCodeNameError } @@ -216,10 +227,10 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS break } } - if len(resultAddrs) > 0 { - r.dnsLogger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(resultAddrs), " ")) + if len(responseAddrs) > 0 { + r.dnsLogger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(responseAddrs), " ")) } - return resultAddrs, err + return responseAddrs, err } func (r *Router) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) { diff --git a/transport/dhcp/server.go b/transport/dhcp/server.go index 2b7346c653..8325c37b2f 100644 --- a/transport/dhcp/server.go +++ b/transport/dhcp/server.go @@ -58,6 +58,7 @@ func NewTransport(options dns.TransportOptions) (*Transport, error) { return nil, E.New("missing router in context") } transport := &Transport{ + options: options, router: router, interfaceName: linkURL.Host, autoInterface: linkURL.Host == "auto", From ce4c97eb64e307f77da9d71266da58b9abd9c752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 15 Mar 2024 17:21:52 +0800 Subject: [PATCH 16/29] Fix DNS fallthrough incorrectly --- route/router_dns.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/route/router_dns.go b/route/router_dns.go index 4bcc4f2398..21beca97f4 100644 --- a/route/router_dns.go +++ b/route/router_dns.go @@ -138,10 +138,13 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er response, err = r.dnsClient.Exchange(dnsCtx, transport, message, strategy) } cancel() + var rejected bool if err != nil { if errors.Is(err, dns.ErrResponseRejectedCached) { + rejected = true r.dnsLogger.DebugContext(ctx, E.Cause(err, "response rejected for ", formatQuestion(message.Question[0].String())), " (cached)") } else if errors.Is(err, dns.ErrResponseRejected) { + rejected = true r.dnsLogger.DebugContext(ctx, E.Cause(err, "response rejected for ", formatQuestion(message.Question[0].String()))) } else if len(message.Question) > 0 { r.dnsLogger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", formatQuestion(message.Question[0].String()))) @@ -149,9 +152,10 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er r.dnsLogger.ErrorContext(ctx, E.Cause(err, "exchange failed for ")) } } - if !addressLimit || err == nil { - break + if addressLimit && rejected { + continue } + break } } if r.dnsReverseMapping != nil && len(message.Question) > 0 && response != nil && len(response.Answer) > 0 { From 719ba9bbe7553d8900df7c706cbd0ad97a950fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 14 Feb 2024 20:42:58 +0800 Subject: [PATCH 17/29] Improve DNS truncate behavior --- outbound/dns.go | 40 +++++----------------------------------- 1 file changed, 5 insertions(+), 35 deletions(-) diff --git a/outbound/dns.go b/outbound/dns.go index df32a019d7..b18b901e7a 100644 --- a/outbound/dns.go +++ b/outbound/dns.go @@ -46,8 +46,8 @@ func (d *DNS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.Pa } func (d *DNS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + metadata.Destination = M.Socksaddr{} defer conn.Close() - ctx = adapter.WithContext(ctx, &metadata) for { err := d.handleConnection(ctx, conn, metadata) if err != nil { @@ -98,6 +98,7 @@ func (d *DNS) handleConnection(ctx context.Context, conn net.Conn, metadata adap } func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + metadata.Destination = M.Socksaddr{} var reader N.PacketReader = conn var counters []N.CountFunc var cachedPackets []*N.PacketBuffer @@ -111,14 +112,11 @@ func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metada } } if readWaiter, created := bufio.CreatePacketReadWaiter(reader); created { - readWaiter.InitializeReadWaiter(N.ReadWaitOptions{ - MTU: dns.FixedPacketSize, - }) + readWaiter.InitializeReadWaiter(N.ReadWaitOptions{}) return d.newPacketConnection(ctx, conn, readWaiter, counters, cachedPackets, metadata) } break } - ctx = adapter.WithContext(ctx, &metadata) fastClose, cancel := common.ContextWithCancelCause(ctx) timeout := canceler.New(fastClose, cancel, C.DNSTimeout) var group task.Group @@ -167,15 +165,11 @@ func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metada return err } timeout.Update() - responseBuffer := buf.NewPacket() - responseBuffer.Resize(1024, 0) - n, err := response.PackBuffer(responseBuffer.FreeBytes()) + responseBuffer, err := dns.TruncateDNSMessage(&message, response, 1024) if err != nil { cancel(err) - responseBuffer.Release() return err } - responseBuffer.Truncate(len(n)) err = conn.WritePacket(responseBuffer, destination) if err != nil { cancel(err) @@ -241,16 +235,11 @@ func (d *DNS) newPacketConnection(ctx context.Context, conn N.PacketConn, readWa return err } timeout.Update() - response = truncateDNSMessage(response, 512) // TODO: add an option to custom UDP buffer size - responseBuffer := buf.NewSize(dns.FixedPacketSize) - responseBuffer.Resize(1024, 0) - n, err := response.PackBuffer(responseBuffer.FreeBytes()) + responseBuffer, err := dns.TruncateDNSMessage(&message, response, 1024) if err != nil { cancel(err) - responseBuffer.Release() return err } - responseBuffer.Truncate(len(n)) err = conn.WritePacket(responseBuffer, destination) if err != nil { cancel(err) @@ -264,22 +253,3 @@ func (d *DNS) newPacketConnection(ctx context.Context, conn N.PacketConn, readWa }) return group.Run(fastClose) } - -func truncateDNSMessage(response *mDNS.Msg, maxLen int) *mDNS.Msg { - responseLen := response.Len() - if responseLen <= maxLen { - return response - } - newResponse := *response - response = &newResponse - for len(response.Answer) > 0 && responseLen > maxLen { - response.Answer = response.Answer[:len(response.Answer)-1] - response.Truncated = true - responseLen = response.Len() - } - if responseLen > maxLen { - response.Ns = nil - response.Extra = nil - } - return response -} From d53ed2ef30029ace185341cd2a9250aa48bd5d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 21 Feb 2024 14:27:55 +0800 Subject: [PATCH 18/29] Fix missing `rule_set_ipcidr_match_source` item in DNS rules --- docs/configuration/dns/rule.md | 8 +++ docs/configuration/dns/rule.zh.md | 8 +++ docs/configuration/route/rule.md | 1 + docs/configuration/route/rule.zh.md | 1 + docs/configuration/rule-set/headless-rule.md | 2 +- option/rule_dns.go | 73 ++++++++++---------- route/rule_dns.go | 4 +- route/rule_item_rule_set.go | 5 +- 8 files changed, 62 insertions(+), 40 deletions(-) diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 84b9b66947..40dce7fde7 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -8,6 +8,7 @@ icon: material/new-box :material-plus: [ip_cidr](#ip_cidr) :material-plus: [ip_is_private](#ip_is_private) :material-plus: [client_subnet](#client_subnet) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) !!! quote "Changes in sing-box 1.8.0" @@ -116,6 +117,7 @@ icon: material/new-box "geoip-cn", "geosite-cn" ], + "rule_set_ipcidr_match_source": false, "invert": false, "outbound": [ "direct" @@ -303,6 +305,12 @@ Match WiFi BSSID. Match [Rule Set](/configuration/route/#rule_set). +#### rule_set_ipcidr_match_source + +!!! question "Since sing-box 1.9.0" + +Make `ipcidr` in rule sets match the source IP. + #### invert Invert match result. diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index c7977bc164..f27aac9a2e 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -8,6 +8,7 @@ icon: material/new-box :material-plus: [ip_cidr](#ip_cidr) :material-plus: [ip_is_private](#ip_is_private) :material-plus: [client_subnet](#client_subnet) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) !!! quote "sing-box 1.8.0 中的更改" @@ -116,6 +117,7 @@ icon: material/new-box "geoip-cn", "geosite-cn" ], + "rule_set_ipcidr_match_source": false, "invert": false, "outbound": [ "direct" @@ -301,6 +303,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 匹配[规则集](/zh/configuration/route/#rule_set)。 +#### rule_set_ipcidr_match_source + +!!! question "自 sing-box 1.9.0 起" + +使规则集中的 `ipcidr` 规则匹配源 IP。 + #### invert 反选匹配结果。 diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index b21bf658cd..be9ee4cc01 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -105,6 +105,7 @@ "geoip-cn", "geosite-cn" ], + "rule_set_ipcidr_match_source": false, "invert": false, "outbound": "direct" }, diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index 3f8b471573..881f97b00e 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -103,6 +103,7 @@ "geoip-cn", "geosite-cn" ], + "rule_set_ipcidr_match_source": false, "invert": false, "outbound": "direct" }, diff --git a/docs/configuration/rule-set/headless-rule.md b/docs/configuration/rule-set/headless-rule.md index 9998489915..9109841f13 100644 --- a/docs/configuration/rule-set/headless-rule.md +++ b/docs/configuration/rule-set/headless-rule.md @@ -124,7 +124,7 @@ Match source IP CIDR. !!! info "" - `ip_cidr` is an alias for `source_ip_cidr` when the Rule Set is used in DNS rules or `rule_set_ipcidr_match_source` enabled in route rules. + `ip_cidr` is an alias for `source_ip_cidr` when `rule_set_ipcidr_match_source` enabled in route/DNS rules. Match IP CIDR. diff --git a/option/rule_dns.go b/option/rule_dns.go index dc5e5c2bdf..ababea416b 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -65,42 +65,43 @@ func (r DNSRule) IsValid() bool { } type DefaultDNSRule struct { - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol Listable[string] `json:"protocol,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - GeoIP Listable[string] `json:"geoip,omitempty"` - IPCIDR Listable[string] `json:"ip_cidr,omitempty"` - IPIsPrivate bool `json:"ip_is_private,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - Outbound Listable[string] `json:"outbound,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - RuleSet Listable[string] `json:"rule_set,omitempty"` - Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` - ClientSubnet *ListenAddress `json:"client_subnet,omitempty"` + Inbound Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` + Network Listable[string] `json:"network,omitempty"` + AuthUser Listable[string] `json:"auth_user,omitempty"` + Protocol Listable[string] `json:"protocol,omitempty"` + Domain Listable[string] `json:"domain,omitempty"` + DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex Listable[string] `json:"domain_regex,omitempty"` + Geosite Listable[string] `json:"geosite,omitempty"` + SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` + GeoIP Listable[string] `json:"geoip,omitempty"` + IPCIDR Listable[string] `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,omitempty"` + SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` + SourcePort Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange Listable[string] `json:"source_port_range,omitempty"` + Port Listable[uint16] `json:"port,omitempty"` + PortRange Listable[string] `json:"port_range,omitempty"` + ProcessName Listable[string] `json:"process_name,omitempty"` + ProcessPath Listable[string] `json:"process_path,omitempty"` + PackageName Listable[string] `json:"package_name,omitempty"` + User Listable[string] `json:"user,omitempty"` + UserID Listable[int32] `json:"user_id,omitempty"` + Outbound Listable[string] `json:"outbound,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` + Invert bool `json:"invert,omitempty"` + Server string `json:"server,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *ListenAddress `json:"client_subnet,omitempty"` } func (r DefaultDNSRule) IsValid() bool { diff --git a/route/rule_dns.go b/route/rule_dns.go index 153c9dcb4f..85ded9615d 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -219,7 +219,7 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options rule.allItems = append(rule.allItems, item) } if len(options.RuleSet) > 0 { - item := NewRuleSetItem(router, options.RuleSet, false) + item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } @@ -247,7 +247,7 @@ func (r *DefaultDNSRule) WithAddressLimit() bool { if !isRuleSet { continue } - if ruleSet.ContainsIPCIDRRule() { + if ruleSet.ContainsDestinationIPCIDRRule() { return true } } diff --git a/route/rule_item_rule_set.go b/route/rule_item_rule_set.go index 8354e4215d..482a9c7b45 100644 --- a/route/rule_item_rule_set.go +++ b/route/rule_item_rule_set.go @@ -47,7 +47,10 @@ func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { return false } -func (r *RuleSetItem) ContainsIPCIDRRule() bool { +func (r *RuleSetItem) ContainsDestinationIPCIDRRule() bool { + if r.ipcidrMatchSource { + return false + } return common.Any(r.setList, func(ruleSet adapter.RuleSet) bool { return ruleSet.Metadata().ContainsIPCIDRRule }) From c3f1dc144845cd4ac73544b96f182e77c214d074 Mon Sep 17 00:00:00 2001 From: PuerNya Date: Mon, 5 Feb 2024 02:42:15 +0800 Subject: [PATCH 19/29] Always disable cache for fake-ip DNS transport if `independent_cache` disabled --- route/router.go | 2 ++ route/router_dns.go | 32 +++++++++++++++++++------------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/route/router.go b/route/router.go index e9807bd467..5e00d2c9e4 100644 --- a/route/router.go +++ b/route/router.go @@ -69,6 +69,7 @@ type Router struct { geositeCache map[string]adapter.Rule needFindProcess bool dnsClient *dns.Client + dnsIndependentCache bool defaultDomainStrategy dns.DomainStrategy dnsRules []adapter.DNSRule ruleSets []adapter.RuleSet @@ -122,6 +123,7 @@ func NewRouter( geositeOptions: common.PtrValueOrDefault(options.Geosite), geositeCache: make(map[string]adapter.Rule), needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess, + dnsIndependentCache: dnsOptions.IndependentCache, defaultDetour: options.Final, defaultDomainStrategy: dns.DomainStrategy(dnsOptions.Strategy), interfaceFinder: control.NewDefaultInterfaceFinder(), diff --git a/route/router_dns.go b/route/router_dns.go index 21beca97f4..2ac439d794 100644 --- a/route/router_dns.go +++ b/route/router_dns.go @@ -56,7 +56,8 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, index int) (con r.dnsLogger.ErrorContext(ctx, "transport not found: ", detour) continue } - if _, isFakeIP := transport.(adapter.FakeIPTransport); isFakeIP && !allowFakeIP { + _, isFakeIP := transport.(adapter.FakeIPTransport) + if isFakeIP && !allowFakeIP { continue } displayRuleIndex := ruleIndex @@ -64,7 +65,7 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, index int) (con displayRuleIndex += index + 1 } r.dnsLogger.DebugContext(ctx, "match[", displayRuleIndex, "] ", rule.String(), " => ", detour) - if rule.DisableCache() { + if (isFakeIP && !r.dnsIndependentCache) || rule.DisableCache() { ctx = dns.ContextWithDisableCache(ctx, true) } if rewriteTTL := rule.RewriteTTL(); rewriteTTL != nil { @@ -93,9 +94,10 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er r.dnsLogger.DebugContext(ctx, "exchange ", formatQuestion(message.Question[0].String())) } var ( - response *mDNS.Msg - cached bool - err error + response *mDNS.Msg + cached bool + transport dns.Transport + err error ) response, cached = r.dnsClient.ExchangeCache(ctx, message) if !cached { @@ -112,7 +114,6 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er metadata.Domain = fqdnToDomain(message.Question[0].Name) } var ( - transport dns.Transport strategy dns.DomainStrategy rule adapter.DNSRule ruleIndex int @@ -158,17 +159,22 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er break } } + if err != nil { + return nil, err + } if r.dnsReverseMapping != nil && len(message.Question) > 0 && response != nil && len(response.Answer) > 0 { - for _, answer := range response.Answer { - switch record := answer.(type) { - case *mDNS.A: - r.dnsReverseMapping.Save(M.AddrFromIP(record.A), fqdnToDomain(record.Hdr.Name), int(record.Hdr.Ttl)) - case *mDNS.AAAA: - r.dnsReverseMapping.Save(M.AddrFromIP(record.AAAA), fqdnToDomain(record.Hdr.Name), int(record.Hdr.Ttl)) + if _, isFakeIP := transport.(adapter.FakeIPTransport); !isFakeIP { + for _, answer := range response.Answer { + switch record := answer.(type) { + case *mDNS.A: + r.dnsReverseMapping.Save(M.AddrFromIP(record.A), fqdnToDomain(record.Hdr.Name), int(record.Hdr.Ttl)) + case *mDNS.AAAA: + r.dnsReverseMapping.Save(M.AddrFromIP(record.AAAA), fqdnToDomain(record.Hdr.Name), int(record.Hdr.Ttl)) + } } } } - return response, err + return response, nil } func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) { From 95b8a0117c8d4b325675b90c3020eacca2740564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B0=94=E6=81=AF?= Date: Tue, 19 Mar 2024 12:04:16 +0800 Subject: [PATCH 20/29] Fix DNS exchange index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 气息 --- route/router_dns.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/route/router_dns.go b/route/router_dns.go index 2ac439d794..fee468fa54 100644 --- a/route/router_dns.go +++ b/route/router_dns.go @@ -47,7 +47,7 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, index int) (con if index != -1 { dnsRules = dnsRules[index+1:] } - for ruleIndex, rule := range dnsRules { + for currentRuleIndex, rule := range dnsRules { metadata.ResetRuleCache() if rule.Match(metadata) { detour := rule.Outbound() @@ -60,11 +60,11 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, index int) (con if isFakeIP && !allowFakeIP { continue } - displayRuleIndex := ruleIndex + ruleIndex := currentRuleIndex if index != -1 { - displayRuleIndex += index + 1 + ruleIndex += index + 1 } - r.dnsLogger.DebugContext(ctx, "match[", displayRuleIndex, "] ", rule.String(), " => ", detour) + r.dnsLogger.DebugContext(ctx, "match[", ruleIndex, "] ", rule.String(), " => ", detour) if (isFakeIP && !r.dnsIndependentCache) || rule.DisableCache() { ctx = dns.ContextWithDisableCache(ctx, true) } From 4b3389c8c0bbd265923cfc29d166a3d252d20961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 6 Feb 2024 18:52:45 +0800 Subject: [PATCH 21/29] Fixed order for Clash modes --- experimental/clashapi.go | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/experimental/clashapi.go b/experimental/clashapi.go index 805fbd5be7..872d9b9956 100644 --- a/experimental/clashapi.go +++ b/experimental/clashapi.go @@ -3,6 +3,7 @@ package experimental import ( "context" "os" + "sort" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" @@ -27,11 +28,26 @@ func NewClashServer(ctx context.Context, router adapter.Router, logFactory log.O } func CalculateClashModeList(options option.Options) []string { - var clashMode []string - clashMode = append(clashMode, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...) - clashMode = append(clashMode, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...) - clashMode = common.FilterNotDefault(common.Uniq(clashMode)) - return clashMode + var clashModes []string + clashModes = append(clashModes, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...) + clashModes = append(clashModes, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...) + clashModes = common.FilterNotDefault(common.Uniq(clashModes)) + predefinedOrder := []string{ + "Rule", "Global", "Direct", + } + var newClashModes []string + for _, mode := range clashModes { + if !common.Contains(predefinedOrder, mode) { + newClashModes = append(newClashModes, mode) + } + } + sort.Strings(newClashModes) + for _, mode := range predefinedOrder { + if common.Contains(clashModes, mode) { + newClashModes = append(newClashModes, mode) + } + } + return newClashModes } func extraClashModeFromRule(rules []option.Rule) []string { From 52a6c36332a6e03d7182f899858e913f0e5bc93b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 10 Feb 2024 11:49:31 +0800 Subject: [PATCH 22/29] Update quic-go to v0.42.0 --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 8f355a7e4d..494ecd00a4 100644 --- a/go.mod +++ b/go.mod @@ -24,12 +24,12 @@ require ( github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 github.com/sagernet/gomobile v0.1.3 github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e - github.com/sagernet/quic-go v0.40.1 + github.com/sagernet/quic-go v0.42.0-beta.3 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 github.com/sagernet/sing v0.4.0-beta.18 github.com/sagernet/sing-dns v0.2.0-beta.16 github.com/sagernet/sing-mux v0.2.0 - github.com/sagernet/sing-quic v0.1.12 + github.com/sagernet/sing-quic v0.1.13-beta.1 github.com/sagernet/sing-shadowsocks v0.2.6 github.com/sagernet/sing-shadowsocks2 v0.2.0 github.com/sagernet/sing-shadowtls v0.1.4 diff --git a/go.sum b/go.sum index f070b62a82..3d396efee5 100644 --- a/go.sum +++ b/go.sum @@ -101,8 +101,8 @@ github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e h1:DOkjByVeAR56dks github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e/go.mod h1:fLxq/gtp0qzkaEwywlRRiGmjOK5ES/xUzyIKIFP2Asw= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= -github.com/sagernet/quic-go v0.40.1 h1:qLeTIJR0d0JWRmDWo346nLsVN6EWihd1kalJYPEd0TM= -github.com/sagernet/quic-go v0.40.1/go.mod h1:CcKTpzTAISxrM4PA5M20/wYuz9Tj6Tx4DwGbNl9UQrU= +github.com/sagernet/quic-go v0.42.0-beta.3 h1:cspu1Uh1KriiDmW42OaPYokeGZT2OyfIcRMV2BdNtEo= +github.com/sagernet/quic-go v0.42.0-beta.3/go.mod h1:lf8OYop+fMxIlrfM/ZHpENt/7ZD4JaVNqMhOlq2QMwg= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= @@ -112,8 +112,8 @@ github.com/sagernet/sing-dns v0.2.0-beta.16 h1:bzd4B8eHD7/WO3HrYknvgE8A56/R3n5oX github.com/sagernet/sing-dns v0.2.0-beta.16/go.mod h1:XU6Vqr6aHcMz/34Fcv8jmXpRCEuShzW+B7Qg1Xe1nxY= github.com/sagernet/sing-mux v0.2.0 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo= github.com/sagernet/sing-mux v0.2.0/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ= -github.com/sagernet/sing-quic v0.1.12 h1:4KjG7LASZck0svGDfzf3aVNidRRQRC/w2HUMk/PHiNE= -github.com/sagernet/sing-quic v0.1.12/go.mod h1:L+VtzvuPbf8VW8F4R7KiygqpXY4lO7t2wwcQuHjh8Ew= +github.com/sagernet/sing-quic v0.1.13-beta.1 h1:/y4Io8m+C9CFUwgMqV2gWjT11I5fCROB0Z3afhhS66c= +github.com/sagernet/sing-quic v0.1.13-beta.1/go.mod h1:Bny0k0Puf7yxhtXfovVyz3gfkHvS1T+/ieKLPhfnhY4= github.com/sagernet/sing-shadowsocks v0.2.6 h1:xr7ylAS/q1cQYS8oxKKajhuQcchd5VJJ4K4UZrrpp0s= github.com/sagernet/sing-shadowsocks v0.2.6/go.mod h1:j2YZBIpWIuElPFL/5sJAj470bcn/3QQ5lxZUNKLDNAM= github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg= From ece7fa8742516aacdee4c16fe22b5f6580f482fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 14 Feb 2024 13:08:08 +0800 Subject: [PATCH 23/29] Update gVisor to 20240212.0-65-g71212d503 --- go.mod | 4 ++-- go.sum | 8 ++++---- transport/wireguard/device_stack.go | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 494ecd00a4..6543484396 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 github.com/sagernet/gomobile v0.1.3 - github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e + github.com/sagernet/gvisor v0.0.0-20240315080113-799fb6b6d311 github.com/sagernet/quic-go v0.42.0-beta.3 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 github.com/sagernet/sing v0.4.0-beta.18 @@ -33,7 +33,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.6 github.com/sagernet/sing-shadowsocks2 v0.2.0 github.com/sagernet/sing-shadowtls v0.1.4 - github.com/sagernet/sing-tun v0.2.6 + github.com/sagernet/sing-tun v0.2.7-beta.1 github.com/sagernet/sing-vmess v0.1.8 github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6 diff --git a/go.sum b/go.sum index 3d396efee5..c0c4dae0a8 100644 --- a/go.sum +++ b/go.sum @@ -97,8 +97,8 @@ github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 h1:YbmpqPQ github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1/go.mod h1:J2yAxTFPDjrDPhuAi9aWFz2L3ox9it4qAluBBbN0H5k= github.com/sagernet/gomobile v0.1.3 h1:ohjIb1Ou2+1558PnZour3od69suSuvkdSVOlO1tC4B8= github.com/sagernet/gomobile v0.1.3/go.mod h1:Pqq2+ZVvs10U7xK+UwJgwYWUykewi8H6vlslAO73n9E= -github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e h1:DOkjByVeAR56dkszjnMZke4wr7yM/1xHaJF3G9olkEE= -github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e/go.mod h1:fLxq/gtp0qzkaEwywlRRiGmjOK5ES/xUzyIKIFP2Asw= +github.com/sagernet/gvisor v0.0.0-20240315080113-799fb6b6d311 h1:eUQ6kJZXK77xYZeeNrBb/7JMv0S0Wkk7EpmKUb3fsfc= +github.com/sagernet/gvisor v0.0.0-20240315080113-799fb6b6d311/go.mod h1:mDrXZSv401qiaFiiIUC59Zp4VG5f4nqXFqDmp5o3hYI= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/quic-go v0.42.0-beta.3 h1:cspu1Uh1KriiDmW42OaPYokeGZT2OyfIcRMV2BdNtEo= @@ -120,8 +120,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wK github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k= github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4= -github.com/sagernet/sing-tun v0.2.6 h1:FKXzh34uuO5RStBgf3Zi+Txan5eS9YTVOQrJRAbJBHk= -github.com/sagernet/sing-tun v0.2.6/go.mod h1:MKAAHUzVfj7d9zos4lsz6wjXu86/mJyd/gejiAnWj/w= +github.com/sagernet/sing-tun v0.2.7-beta.1 h1:rudjVa4Gjg9f3uYaUwH8qK5uMxl7MOIiYlmGhTqWgXI= +github.com/sagernet/sing-tun v0.2.7-beta.1/go.mod h1:9pauo20NImopbZ3ixnJs6m5CbzhJitfvii6w4Rk3QMg= github.com/sagernet/sing-vmess v0.1.8 h1:XVWad1RpTy9b5tPxdm5MCU8cGfrTGdR8qCq6HV2aCNc= github.com/sagernet/sing-vmess v0.1.8/go.mod h1:vhx32UNzTDUkNwOyIjcZQohre1CaytquC5mPplId8uA= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= diff --git a/transport/wireguard/device_stack.go b/transport/wireguard/device_stack.go index 9d9b4549bd..7f57b7c73a 100644 --- a/transport/wireguard/device_stack.go +++ b/transport/wireguard/device_stack.go @@ -34,7 +34,7 @@ type StackDevice struct { stack *stack.Stack mtu uint32 events chan wgTun.Event - outbound chan stack.PacketBufferPtr + outbound chan *stack.PacketBuffer packetOutbound chan *buf.Buffer done chan struct{} dispatcher stack.NetworkDispatcher @@ -52,7 +52,7 @@ func NewStackDevice(localAddresses []netip.Prefix, mtu uint32) (*StackDevice, er stack: ipStack, mtu: mtu, events: make(chan wgTun.Event, 1), - outbound: make(chan stack.PacketBufferPtr, 256), + outbound: make(chan *stack.PacketBuffer, 256), packetOutbound: make(chan *buf.Buffer, 256), done: make(chan struct{}), } @@ -283,10 +283,10 @@ func (ep *wireEndpoint) ARPHardwareType() header.ARPHardwareType { return header.ARPHardwareNone } -func (ep *wireEndpoint) AddHeader(buffer stack.PacketBufferPtr) { +func (ep *wireEndpoint) AddHeader(buffer *stack.PacketBuffer) { } -func (ep *wireEndpoint) ParseHeader(ptr stack.PacketBufferPtr) bool { +func (ep *wireEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool { return true } From fce375222082649166744c71896e32423903ddfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 19 Feb 2024 00:19:33 +0800 Subject: [PATCH 24/29] Add `bypass_domain` and `search_domain` platform HTTP proxy options --- docs/changelog.md | 4 +- docs/configuration/dns/rule.md | 4 +- docs/configuration/dns/rule.zh.md | 4 +- docs/configuration/inbound/http.md | 2 +- docs/configuration/inbound/mixed.md | 2 +- docs/configuration/inbound/tun.md | 45 +++++++++++++++++++- docs/configuration/inbound/tun.zh.md | 45 +++++++++++++++++++- docs/configuration/route/rule.md | 4 +- docs/configuration/route/rule.zh.md | 4 +- docs/configuration/rule-set/headless-rule.md | 4 +- experimental/libbox/tun.go | 10 +++++ option/tun_platform.go | 2 + 12 files changed, 114 insertions(+), 16 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 02a765cb29..09fc2fe958 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -385,7 +385,7 @@ see [TCP Brutal](/configuration/shared/tcp-brutal/) for details. **5**: -Only supported in graphical clients on Android and iOS. +Only supported in graphical clients on Android and Apple platforms. #### 1.7.0-rc.3 @@ -422,7 +422,7 @@ Only supported in graphical clients on Android and iOS. **1**: -Only supported in graphical clients on Android and iOS. +Only supported in graphical clients on Android and Apple platforms. #### 1.7.0-beta.3 diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 40dce7fde7..ee3be87d56 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -287,7 +287,7 @@ Match Clash mode. !!! quote "" - Only supported in graphical clients on Android and iOS. + Only supported in graphical clients on Android and Apple platforms. Match WiFi SSID. @@ -295,7 +295,7 @@ Match WiFi SSID. !!! quote "" - Only supported in graphical clients on Android and iOS. + Only supported in graphical clients on Android and Apple platforms. Match WiFi BSSID. diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index f27aac9a2e..7ee72c6007 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -285,7 +285,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 !!! quote "" - 仅在 Android 与 iOS 的图形客户端中支持。 + 仅在 Android 与 Apple 平台图形客户端中支持。 匹配 WiFi SSID。 @@ -293,7 +293,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 !!! quote "" - 仅在 Android 与 iOS 的图形客户端中支持。 + 仅在 Android 与 Apple 平台图形客户端中支持。 匹配 WiFi BSSID。 diff --git a/docs/configuration/inbound/http.md b/docs/configuration/inbound/http.md index cd2ec35dc8..00343e224d 100644 --- a/docs/configuration/inbound/http.md +++ b/docs/configuration/inbound/http.md @@ -42,6 +42,6 @@ No authentication required if empty. !!! warning "" - To work on Android and iOS without privileges, use tun.platform.http_proxy instead. + To work on Android and Apple platforms without privileges, use tun.platform.http_proxy instead. Automatically set system proxy configuration when start and clean up when stop. diff --git a/docs/configuration/inbound/mixed.md b/docs/configuration/inbound/mixed.md index 1f5bf0ac0d..e9deec7592 100644 --- a/docs/configuration/inbound/mixed.md +++ b/docs/configuration/inbound/mixed.md @@ -39,6 +39,6 @@ No authentication required if empty. !!! warning "" - To work on Android and iOS without privileges, use tun.platform.http_proxy instead. + To work on Android and Apple platforms without privileges, use tun.platform.http_proxy instead. Automatically set system proxy configuration when start and clean up when stop. diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 2eed45535e..1d5d8d0f65 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -1,3 +1,12 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.9.0" + + :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain) + :material-plus: [platform.http_proxy.match_domain](#platformhttp_proxymatch_domain) + !!! quote "Changes in sing-box 1.8.0" :material-plus: [gso](#gso) @@ -69,7 +78,9 @@ "http_proxy": { "enabled": false, "server": "127.0.0.1", - "server_port": 8080 + "server_port": 8080, + "bypass_domain": [], + "match_domain": [] } }, @@ -256,6 +267,38 @@ Platform-specific settings, provided by client applications. System HTTP proxy settings. +#### platform.http_proxy.enabled + +Enable system HTTP proxy. + +#### platform.http_proxy.server + +==Required== + +HTTP proxy server address. + +#### platform.http_proxy.server_port + +==Required== + +HTTP proxy server port. + +#### platform.http_proxy.bypass_domain + +!!! note "" + + On Apple platforms, `bypass_domain` items matches hostname **suffixes**. + +Hostnames that bypass the HTTP proxy. + +#### platform.http_proxy.match_domain + +!!! quote "" + + Only supported in graphical clients on Apple platforms. + +Hostnames that use the HTTP proxy. + ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index 05c7c3140b..73d31d6497 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -1,3 +1,12 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.9.0 中的更改" + + :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain) + :material-plus: [platform.http_proxy.match_domain](#platformhttp_proxymatch_domain) + !!! quote "sing-box 1.8.0 中的更改" :material-plus: [gso](#gso) @@ -69,7 +78,9 @@ "http_proxy": { "enabled": false, "server": "127.0.0.1", - "server_port": 8080 + "server_port": 8080, + "bypass_domain": [], + "match_domain": [] } }, @@ -253,6 +264,38 @@ TCP/IP 栈。 系统 HTTP 代理设置。 +##### platform.http_proxy.enabled + +启用系统 HTTP 代理。 + +##### platform.http_proxy.server + +==必填== + +系统 HTTP 代理服务器地址。 + +##### platform.http_proxy.server_port + +==必填== + +系统 HTTP 代理服务器端口。 + +##### platform.http_proxy.bypass_domain + +!!! note "" + + 在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**. + +绕过代理的主机名列表。 + +##### platform.http_proxy.match_domain + +!!! quote "" + + 仅在 Apple 平台图形客户端中支持。 + +代理的主机名列表。 + ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index be9ee4cc01..62d33c6c53 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -281,7 +281,7 @@ Match Clash mode. !!! quote "" - Only supported in graphical clients on Android and iOS. + Only supported in graphical clients on Android and Apple platforms. Match WiFi SSID. @@ -289,7 +289,7 @@ Match WiFi SSID. !!! quote "" - Only supported in graphical clients on Android and iOS. + Only supported in graphical clients on Android and Apple platforms. Match WiFi BSSID. diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index 881f97b00e..cba35bc581 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -279,7 +279,7 @@ !!! quote "" - 仅在 Android 与 iOS 的图形客户端中支持。 + 仅在 Android 与 Apple 平台图形客户端中支持。 匹配 WiFi SSID。 @@ -287,7 +287,7 @@ !!! quote "" - 仅在 Android 与 iOS 的图形客户端中支持。 + 仅在 Android 与 Apple 平台图形客户端中支持。 匹配 WiFi BSSID。 diff --git a/docs/configuration/rule-set/headless-rule.md b/docs/configuration/rule-set/headless-rule.md index 9109841f13..e766904b5f 100644 --- a/docs/configuration/rule-set/headless-rule.md +++ b/docs/configuration/rule-set/headless-rule.md @@ -168,7 +168,7 @@ Match android package name. !!! quote "" - Only supported in graphical clients on Android and iOS. + Only supported in graphical clients on Android and Apple platforms. Match WiFi SSID. @@ -176,7 +176,7 @@ Match WiFi SSID. !!! quote "" - Only supported in graphical clients on Android and iOS. + Only supported in graphical clients on Android and Apple platforms. Match WiFi BSSID. diff --git a/experimental/libbox/tun.go b/experimental/libbox/tun.go index 53add3ceb3..5c6e3370c2 100644 --- a/experimental/libbox/tun.go +++ b/experimental/libbox/tun.go @@ -28,6 +28,8 @@ type TunOptions interface { IsHTTPProxyEnabled() bool GetHTTPProxyServer() string GetHTTPProxyServerPort() int32 + GetHTTPProxyBypassDomain() StringIterator + GetHTTPProxyMatchDomain() StringIterator } type RoutePrefix struct { @@ -156,3 +158,11 @@ func (o *tunOptions) GetHTTPProxyServer() string { func (o *tunOptions) GetHTTPProxyServerPort() int32 { return int32(o.TunPlatformOptions.HTTPProxy.ServerPort) } + +func (o *tunOptions) GetHTTPProxyBypassDomain() StringIterator { + return newIterator(o.TunPlatformOptions.HTTPProxy.BypassDomain) +} + +func (o *tunOptions) GetHTTPProxyMatchDomain() StringIterator { + return newIterator(o.TunPlatformOptions.HTTPProxy.MatchDomain) +} diff --git a/option/tun_platform.go b/option/tun_platform.go index 873d788a90..a0a54eed05 100644 --- a/option/tun_platform.go +++ b/option/tun_platform.go @@ -7,4 +7,6 @@ type TunPlatformOptions struct { type HTTPProxyOptions struct { Enabled bool `json:"enabled,omitempty"` ServerOptions + BypassDomain Listable[string] `json:"bypass_domain,omitempty"` + MatchDomain Listable[string] `json:"match_domain,omitempty"` } From c87f1c0d698f0321ee80e4ebaa87feda599d5738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 24 Mar 2024 19:23:11 +0800 Subject: [PATCH 25/29] Add `rule-set match` command --- adapter/router.go | 2 +- cmd/sing-box/cmd_rule_set_match.go | 86 ++++++++++++++++++++++++++++++ route/rule_headless.go | 16 +++--- 3 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 cmd/sing-box/cmd_rule_set_match.go diff --git a/adapter/router.go b/adapter/router.go index 0d771deee7..e2f82ec42c 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -71,6 +71,7 @@ func RouterFromContext(ctx context.Context) Router { type HeadlessRule interface { Match(metadata *InboundContext) bool + String() string } type Rule interface { @@ -79,7 +80,6 @@ type Rule interface { Type() string UpdateGeosite() error Outbound() string - String() string } type DNSRule interface { diff --git a/cmd/sing-box/cmd_rule_set_match.go b/cmd/sing-box/cmd_rule_set_match.go new file mode 100644 index 0000000000..473c82227c --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_match.go @@ -0,0 +1,86 @@ +package main + +import ( + "bytes" + "io" + "os" + + "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/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/route" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + + "github.com/spf13/cobra" +) + +var flagRuleSetMatchFormat string + +var commandRuleSetMatch = &cobra.Command{ + Use: "match ", + Short: "Check if a domain matches the rule set", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + err := ruleSetMatch(args[0], args[1]) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + commandRuleSetMatch.Flags().StringVarP(&flagRuleSetMatchFormat, "format", "f", "source", "rule-set format") + commandRuleSet.AddCommand(commandRuleSetMatch) +} + +func ruleSetMatch(sourcePath string, domain string) error { + var ( + reader io.Reader + err error + ) + if sourcePath == "stdin" { + reader = os.Stdin + } else { + reader, err = os.Open(sourcePath) + if err != nil { + return E.Cause(err, "read rule-set") + } + } + content, err := io.ReadAll(reader) + if err != nil { + return E.Cause(err, "read rule-set") + } + var plainRuleSet option.PlainRuleSet + switch flagRuleSetMatchFormat { + case C.RuleSetFormatSource: + var compat option.PlainRuleSetCompat + compat, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content) + if err != nil { + return err + } + plainRuleSet = compat.Upgrade() + case C.RuleSetFormatBinary: + plainRuleSet, err = srs.Read(bytes.NewReader(content), false) + if err != nil { + return err + } + default: + return E.New("unknown rule set format: ", flagRuleSetMatchFormat) + } + for i, ruleOptions := range plainRuleSet.Rules { + var currentRule adapter.HeadlessRule + currentRule, err = route.NewHeadlessRule(nil, ruleOptions) + if err != nil { + return E.Cause(err, "parse rule_set.rules.[", i, "]") + } + if currentRule.Match(&adapter.InboundContext{ + Domain: domain, + }) { + println("match rules.[", i, "]: "+currentRule.String()) + } + } + return nil +} diff --git a/route/rule_headless.go b/route/rule_headless.go index 92b6720c18..67ac3a1e44 100644 --- a/route/rule_headless.go +++ b/route/rule_headless.go @@ -129,14 +129,18 @@ func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadles rule.allItems = append(rule.allItems, item) } if len(options.WIFISSID) > 0 { - item := NewWIFISSIDItem(router, options.WIFISSID) - rule.items = append(rule.items, item) - rule.allItems = append(rule.allItems, item) + if router != nil { + item := NewWIFISSIDItem(router, options.WIFISSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } } if len(options.WIFIBSSID) > 0 { - item := NewWIFIBSSIDItem(router, options.WIFIBSSID) - rule.items = append(rule.items, item) - rule.allItems = append(rule.allItems, item) + if router != nil { + item := NewWIFIBSSIDItem(router, options.WIFIBSSID) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } } return rule, nil } From c4d5ef59580881062bc9936df41c9a5b9c7113c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 25 Apr 2024 22:16:13 +0800 Subject: [PATCH 26/29] dialer: Allow nil router --- common/dialer/default.go | 14 ++++++++++---- common/dialer/dialer.go | 3 +++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/common/dialer/default.go b/common/dialer/default.go index 91af85c524..4fbad07deb 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -32,14 +32,20 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi var dialer net.Dialer var listener net.ListenConfig if options.BindInterface != "" { - bindFunc := control.BindToInterface(router.InterfaceFinder(), options.BindInterface, -1) + var interfaceFinder control.InterfaceFinder + if router != nil { + interfaceFinder = router.InterfaceFinder() + } else { + interfaceFinder = control.NewDefaultInterfaceFinder() + } + bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1) dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) - } else if router.AutoDetectInterface() { + } else if router != nil && router.AutoDetectInterface() { bindFunc := router.AutoDetectInterfaceFunc() dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) - } else if router.DefaultInterface() != "" { + } else if router != nil && router.DefaultInterface() != "" { bindFunc := control.BindToInterface(router.InterfaceFinder(), router.DefaultInterface(), -1) dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) @@ -47,7 +53,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi 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.DefaultMark() != 0 { + } 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())) } diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index bbb4b3a92e..a1721b281f 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -13,6 +13,9 @@ func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error) if options.IsWireGuardListener { return NewDefault(router, options) } + if router == nil { + return NewDefault(nil, options) + } var ( dialer N.Dialer err error From 60c9ba706c37f85b4fb8800c112d6a001e123daa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 15 Mar 2024 17:15:26 +0800 Subject: [PATCH 27/29] documentation: Update DNS manual --- docs/manual/proxy/client.md | 59 ++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/docs/manual/proxy/client.md b/docs/manual/proxy/client.md index 12a8303962..c04f95a5f8 100644 --- a/docs/manual/proxy/client.md +++ b/docs/manual/proxy/client.md @@ -336,10 +336,10 @@ flowchart TB } ``` -=== ":material-dns: DNS rules (1.9.0+)" +=== ":material-dns: DNS rules (Enhanced, but slower) (1.9.0+)" + + === ":material-shield-off: With DNS leaks" - === ":material-shield-off: With DNS Leaks" - ```json { "dns": { @@ -376,7 +376,17 @@ flowchart TB "server": "google" }, { - "rule_set": "geoip-cn", + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + }, + { + "rule_set": "geoip-cn" + } + ], "server": "local" } ] @@ -389,6 +399,12 @@ flowchart TB "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs" }, + { + "type": "remote", + "tag": "geosite-geolocation-!cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs" + }, { "type": "remote", "tag": "geoip-cn", @@ -398,14 +414,18 @@ flowchart TB ] }, "experimental": { + "cache_file": { + "enabled": true, + "store_rdrc": true + }, "clash_api": { - "default_mode": "Leak" + "default_mode": "Enhanced" } } } ``` - === ":material-security: Without DNS Leaks (1.9.0-alpha.2+)" + === ":material-security: Without DNS leaks, but slower (1.9.0-alpha.2+)" ```json { @@ -439,7 +459,17 @@ flowchart TB "server": "local" }, { - "rule_set": "geoip-cn", + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + }, + { + "rule_set": "geoip-cn" + } + ], "server": "google", "client_subnet": "114.114.114.114" // Any China client IP address } @@ -453,6 +483,12 @@ flowchart TB "format": "binary", "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs" }, + { + "type": "remote", + "tag": "geosite-geolocation-!cn", + "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs" + }, { "type": "remote", "tag": "geoip-cn", @@ -460,6 +496,15 @@ flowchart TB "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" } ] + }, + "experimental": { + "cache_file": { + "enabled": true, + "store_rdrc": true + }, + "clash_api": { + "default_mode": "Enhanced" + } } } ``` From 02656401ab899f7dc0539cfadc8a90f28006ac22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 26 Apr 2024 20:37:25 +0800 Subject: [PATCH 28/29] documentation: Bump version --- docs/changelog.md | 137 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 09fc2fe958..d0789dbe6b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ icon: material/alert-decagram --- +#### 1.9.0-rc.12 + +* Fixes and improvements + #### 1.8.12 * Now we have official APT and DNF repositories **1** @@ -12,6 +16,10 @@ icon: material/alert-decagram Including stable and beta versions, see https://sing-box.sagernet.org/installation/package-manager/ +#### 1.9.0-rc.11 + +* Fixes and improvements + #### 1.8.11 * Fixes and improvements @@ -20,6 +28,24 @@ Including stable and beta versions, see https://sing-box.sagernet.org/installati * Fixes and improvements +#### 1.9.0-beta.17 + +* Update `quic-go` to v0.42.0 +* Fixes and improvements + +#### 1.9.0-beta.16 + +* Fixes and improvements + +_Our Testflight distribution has been temporarily blocked by Apple (possibly due to too many beta versions) +and you cannot join the test, install or update the sing-box beta app right now. +Please wait patiently for processing._ + +#### 1.9.0-beta.14 + +* Update gVisor to 20240212.0-65-g71212d503 +* Fixes and improvements + #### 1.8.9 * Fixes and improvements @@ -28,14 +54,125 @@ Including stable and beta versions, see https://sing-box.sagernet.org/installati * Fixes and improvements +#### 1.9.0-beta.7 + +* Fixes and improvements + +#### 1.9.0-beta.6 + +* Fix address filter DNS rule items **1** +* Fix DNS outbound responding with wrong data +* Fixes and improvements + +**1**: + +Fixed an issue where address filter DNS rule was incorrectly rejected under certain circumstances. +If you have enabled `store_rdrc` to save results, consider clearing the cache file. + #### 1.8.7 * Fixes and improvements +#### 1.9.0-alpha.15 + +* Fixes and improvements + +#### 1.9.0-alpha.14 + +* Improve DNS truncate behavior +* Fixes and improvements + +#### 1.9.0-alpha.13 + +* Fixes and improvements + #### 1.8.6 * Fixes and improvements +#### 1.9.0-alpha.12 + +* Handle Windows power events +* Always disable cache for fake-ip DNS transport if `dns.independent_cache` disabled +* Fixes and improvements + +#### 1.9.0-alpha.11 + +* Fix missing `rule_set_ipcidr_match_source` item in DNS rules **1** +* Fixes and improvements + +**1**: + +See [DNS Rule](/configuration/dns/rule/). + +#### 1.9.0-alpha.10 + +* Add `bypass_domain` and `search_domain` platform HTTP proxy options **1** +* Fixes and improvements + +**1**: + +See [TUN](/configuration/inbound/tun) inbound. + +#### 1.9.0-alpha.8 + +* Add rejected DNS response cache support **1** +* Fixes and improvements + +**1**: + +The new feature allows you to cache the check results of +[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration. + +#### 1.9.0-alpha.7 + +* Update gVisor to 20240206.0 +* Fixes and improvements + +#### 1.9.0-alpha.6 + +* Fixes and improvements + +#### 1.9.0-alpha.3 + +* Update `quic-go` to v0.41.0 +* Fixes and improvements + +#### 1.9.0-alpha.2 + +* Add support for `client-subnet` DNS options **1** +* Fixes and improvements + +**1**: + +See [DNS](/configuration/dns), [DNS Server](/configuration/dns/server) and [DNS Rules](/configuration/dns/rule). + +Since this feature makes the scenario mentioned in `alpha.1` no longer leak DNS requests, +the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) has been updated. + +#### 1.9.0-alpha.1 + +* `domain_suffix` behavior update **1** +* `process_path` format update on Windows **2** +* Add address filter DNS rule items **3** + +**1**: + +See [Migration](/migration/#domain_suffix-behavior-update). + +**2**: + +See [Migration](/migration/#process_path-format-update-on-windows). + +**3**: + +The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS +if using this method. + +See [Address Filter Fields](/configuration/dns/rule#address-filter-fields). + +[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated. + #### 1.8.5 * Fixes and improvements From a4bf933fe7f3b0109b31e2e1fea541b606153a2c Mon Sep 17 00:00:00 2001 From: dyhkwong <50692134+dyhkwong@users.noreply.github.com> Date: Sat, 27 Apr 2024 22:31:12 +0800 Subject: [PATCH 29/29] Fix fake-ip mapping --- experimental/cachefile/fakeip.go | 13 +++++++++++++ route/router_dns.go | 2 +- transport/fakeip/memory.go | 7 +++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/experimental/cachefile/fakeip.go b/experimental/cachefile/fakeip.go index 1c387da8cf..8fe0f1139c 100644 --- a/experimental/cachefile/fakeip.go +++ b/experimental/cachefile/fakeip.go @@ -74,6 +74,7 @@ func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error { if err != nil { return err } + oldDomain := bucket.Get(address.AsSlice()) err = bucket.Put(address.AsSlice(), []byte(domain)) if err != nil { return err @@ -86,12 +87,24 @@ func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error { if err != nil { return err } + if oldDomain != nil { + if err := bucket.Delete(oldDomain); err != nil { + return err + } + } return bucket.Put([]byte(domain), address.AsSlice()) }) } func (c *CacheFile) FakeIPStoreAsync(address netip.Addr, domain string, logger logger.Logger) { c.saveFakeIPAccess.Lock() + if oldDomain, loaded := c.saveDomain[address]; loaded { + if address.Is4() { + delete(c.saveAddress4, oldDomain) + } else { + delete(c.saveAddress6, oldDomain) + } + } c.saveDomain[address] = domain if address.Is4() { c.saveAddress4[domain] = address diff --git a/route/router_dns.go b/route/router_dns.go index fee468fa54..c3383e8b46 100644 --- a/route/router_dns.go +++ b/route/router_dns.go @@ -65,7 +65,7 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, index int) (con ruleIndex += index + 1 } r.dnsLogger.DebugContext(ctx, "match[", ruleIndex, "] ", rule.String(), " => ", detour) - if (isFakeIP && !r.dnsIndependentCache) || rule.DisableCache() { + if isFakeIP || rule.DisableCache() { ctx = dns.ContextWithDisableCache(ctx, true) } if rewriteTTL := rule.RewriteTTL(); rewriteTTL != nil { diff --git a/transport/fakeip/memory.go b/transport/fakeip/memory.go index 0bca491036..1640ab349d 100644 --- a/transport/fakeip/memory.go +++ b/transport/fakeip/memory.go @@ -40,6 +40,13 @@ func (s *MemoryStorage) FakeIPSaveMetadataAsync(metadata *adapter.FakeIPMetadata func (s *MemoryStorage) FakeIPStore(address netip.Addr, domain string) error { s.addressAccess.Lock() s.domainAccess.Lock() + if oldDomain, loaded := s.addressCache[address]; loaded { + if address.Is4() { + delete(s.domainCache4, oldDomain) + } else { + delete(s.domainCache6, oldDomain) + } + } s.addressCache[address] = domain if address.Is4() { s.domainCache4[domain] = address