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/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..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,13 +80,15 @@ type Rule interface { Type() string UpdateGeosite() error Outbound() string - String() string } type DNSRule interface { Rule DisableCache() bool RewriteTTL() *uint32 + ClientSubnet() *netip.Addr + WithAddressLimit() bool + MatchAddressLimit(metadata *InboundContext) bool } type RuleSet interface { @@ -99,6 +102,7 @@ type RuleSet interface { type RuleSetMetadata struct { ContainsProcessRule bool ContainsWIFIRule bool + ContainsIPCIDRRule bool } type RuleSetStartContext 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/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 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/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/docs/changelog.md b/docs/changelog.md index fdaa12177c..4935eb880d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,10 +2,32 @@ icon: material/alert-decagram --- +#### 1.9.0-rc.3 + +* Fixes and improvements + #### 1.8.10 * 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 @@ -14,14 +36,125 @@ icon: material/alert-decagram * 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 @@ -371,7 +504,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 @@ -408,7 +541,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/clients/apple/index.md b/docs/clients/apple/index.md index 36a4edd9f4..d1db99c6c9 100644 --- a/docs/clients/apple/index.md +++ b/docs/clients/apple/index.md @@ -15,7 +15,11 @@ 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)](https://testflight.apple.com/join/AcqO44FH)~~ + +_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._ ## :material-file-download: Download (macOS standalone version) diff --git a/docs/configuration/dns/index.md b/docs/configuration/dns/index.md index e2832c4275..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": {} } } @@ -21,8 +30,8 @@ ### Fields -| Key | Format | -|----------|--------------------------------| +| Key | Format | +|----------|---------------------------------| | `server` | List of [DNS Server](./server/) | | `rules` | List of [DNS Rule](./rule/) | | `fakeip` | [FakeIP](./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 68cc32cfae..ee3be87d56 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -1,7 +1,15 @@ --- -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) + :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" :material-plus: [rule_set](#rule_set) @@ -53,11 +61,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 ], @@ -101,13 +117,15 @@ icon: material/alert-decagram "geoip-cn", "geosite-cn" ], + "rule_set_ipcidr_match_source": false, "invert": false, "outbound": [ "direct" ], "server": "local", "disable_cache": false, - "rewrite_ttl": 100 + "rewrite_ttl": 100, + "client_subnet": "127.0.0.1" }, { "type": "logical", @@ -115,7 +133,8 @@ icon: material/alert-decagram "rules": [], "server": "local", "disable_cache": false, - "rewrite_ttl": 100 + "rewrite_ttl": 100, + "client_subnet": "127.0.0.1" } ] } @@ -266,11 +285,9 @@ Match Clash mode. #### wifi_ssid - - !!! quote "" - Only supported in graphical clients on Android and iOS. + Only supported in graphical clients on Android and Apple platforms. Match WiFi SSID. @@ -278,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. @@ -288,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. @@ -312,6 +335,44 @@ 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. + +!!! 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" + +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..7ee72c6007 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -1,7 +1,15 @@ --- -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) + :material-plus: [client_subnet](#client_subnet) + :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + !!! quote "sing-box 1.8.0 中的更改" :material-plus: [rule_set](#rule_set) @@ -53,10 +61,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 ], @@ -100,19 +117,22 @@ icon: material/alert-decagram "geoip-cn", "geosite-cn" ], + "rule_set_ipcidr_match_source": false, "invert": false, "outbound": [ "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" } ] } @@ -265,7 +285,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 !!! quote "" - 仅在 Android 与 iOS 的图形客户端中支持。 + 仅在 Android 与 Apple 平台图形客户端中支持。 匹配 WiFi SSID。 @@ -273,7 +293,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 !!! quote "" - 仅在 Android 与 iOS 的图形客户端中支持。 + 仅在 Android 与 Apple 平台图形客户端中支持。 匹配 WiFi BSSID。 @@ -283,6 +303,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 匹配[规则集](/zh/configuration/route/#rule_set)。 +#### rule_set_ipcidr_match_source + +!!! question "自 sing-box 1.9.0 起" + +使规则集中的 `ipcidr` 规则匹配源 IP。 + #### invert 反选匹配结果。 @@ -307,6 +333,44 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 重写 DNS 回应中的 TTL。 +#### client_subnet + +!!! question "自 sing-box 1.9.0 起" + +默认情况下,将带有指定 IP 地址的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +将覆盖 `dns.client_subnet` 与 `servers.[].client_subnet`。 + +### 地址筛选字段 + +仅对IP地址请求生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。 + +!!! info "" + + 引用的规则集中的 `ip_cidr` 项也作为地址筛选字段生效。 + +!!! note "" + + 启用 `experimental.cache_file.store_rdrc` 以缓存结果。 + +#### 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 +383,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### rules -包括的规则。 \ No newline at end of file +包括的规则。 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/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md index 66e30ef9b0..b30538e591 100644 --- a/docs/configuration/experimental/cache-file.md +++ b/docs/configuration/experimental/cache-file.md @@ -4,6 +4,11 @@ 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 @@ -11,7 +16,9 @@ icon: material/new-box "enabled": true, "path": "", "cache_id": "", - "store_fakeip": false + "store_fakeip": false, + "store_rdrc": false, + "rdrc_timeout": "" } ``` @@ -29,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 f4417ede45..6d86dc8423 100644 --- a/docs/configuration/experimental/cache-file.zh.md +++ b/docs/configuration/experimental/cache-file.zh.md @@ -4,6 +4,11 @@ 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 @@ -11,7 +16,9 @@ icon: material/new-box "enabled": true, "path": "", "cache_id": "", - "store_fakeip": false + "store_fakeip": false, + "store_rdrc": false, + "rdrc_timeout": "" } ``` @@ -30,3 +37,19 @@ icon: material/new-box 缓存文件中的标识符。 如果不为空,配置特定的数据将使用由其键控的单独存储。 + +#### store_fakeip + +将 fakeip 存储在缓存文件中。 + +#### store_rdrc + +将拒绝的 DNS 响应缓存存储在缓存文件中。 + +[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#_3) 的检查结果将被缓存至过期。 + +#### rdrc_timeout + +拒绝的 DNS 响应缓存超时。 + +默认使用 `7d`。 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/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 002c690a4b..15a342ff0d 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -1,7 +1,12 @@ --- -icon: material/alert-decagram +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) @@ -73,7 +78,9 @@ icon: material/alert-decagram "http_proxy": { "enabled": false, "server": "127.0.0.1", - "server_port": 8080 + "server_port": 8080, + "bypass_domain": [], + "match_domain": [] } }, @@ -260,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 6a80063487..78c4605892 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -1,7 +1,12 @@ --- -icon: material/alert-decagram +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) @@ -73,7 +78,9 @@ icon: material/alert-decagram "http_proxy": { "enabled": false, "server": "127.0.0.1", - "server_port": 8080 + "server_port": 8080, + "bypass_domain": [], + "match_domain": [] } }, @@ -257,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/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..62d33c6c53 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) @@ -109,6 +105,7 @@ icon: material/alert-decagram "geoip-cn", "geosite-cn" ], + "rule_set_ipcidr_match_source": false, "invert": false, "outbound": "direct" }, @@ -284,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. @@ -292,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 0e6f989604..cba35bc581 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) @@ -107,6 +103,7 @@ icon: material/alert-decagram "geoip-cn", "geosite-cn" ], + "rule_set_ipcidr_match_source": false, "invert": false, "outbound": "direct" }, @@ -282,7 +279,7 @@ icon: material/alert-decagram !!! quote "" - 仅在 Android 与 iOS 的图形客户端中支持。 + 仅在 Android 与 Apple 平台图形客户端中支持。 匹配 WiFi SSID。 @@ -290,7 +287,7 @@ icon: material/alert-decagram !!! 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 6ab62eb2e3..e766904b5f 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" @@ -128,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. @@ -172,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. @@ -180,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/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..c04f95a5f8 100644 --- a/docs/manual/proxy/client.md +++ b/docs/manual/proxy/client.md @@ -290,52 +290,6 @@ flowchart TB === ":material-dns: DNS rules" - !!! info - - DNS rules are optional if FakeIP is used. - - ```json - { - "dns": { - "servers": [ - { - "tag": "google", - "address": "tls://8.8.8.8" - }, - { - "tag": "local", - "address": "223.5.5.5", - "detour": "direct" - } - ], - "rules": [ - { - "outbound": "any", - "server": "local" - }, - { - "clash_mode": "Direct", - "server": "local" - }, - { - "clash_mode": "Global", - "server": "google" - }, - { - "geosite": "geolocation-cn", - "server": "local" - } - ] - } - } - ``` - -=== ":material-dns: DNS rules (1.8.0+)" - - !!! info - - DNS rules are optional if FakeIP is used. - ```json { "dns": { @@ -382,74 +336,180 @@ flowchart TB } ``` -=== ":material-router-network: Route rules" +=== ":material-dns: DNS rules (Enhanced, but slower) (1.9.0+)" - ```json - { - "outbounds": [ - { - "type": "direct", - "tag": "direct" - }, + === ":material-shield-off: With DNS leaks" + + ```json { - "type": "block", - "tag": "block" - } - ], - "route": { - "rules": [ - { - "type": "logical", - "mode": "or", - "rules": [ + "dns": { + "servers": [ { - "protocol": "dns" + "tag": "google", + "address": "tls://8.8.8.8" }, { - "port": 53 + "tag": "local", + "address": "https://223.5.5.5/dns-query", + "detour": "direct" } ], - "outbound": "dns" - }, - { - "geoip": "private", - "outbound": "direct" - }, - { - "clash_mode": "Direct", - "outbound": "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" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + }, + { + "rule_set": "geoip-cn" + } + ], + "server": "local" + } + ] }, - { - "clash_mode": "Global", - "outbound": "default" + "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": "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" + } + ] }, - { - "type": "logical", - "mode": "or", + "experimental": { + "cache_file": { + "enabled": true, + "store_rdrc": true + }, + "clash_api": { + "default_mode": "Enhanced" + } + } + } + ``` + + === ":material-security: Without DNS leaks, but slower (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": [ { - "port": 853 + "outbound": "any", + "server": "local" }, { - "network": "udp", - "port": 443 + "clash_mode": "Direct", + "server": "local" }, { - "protocol": "stun" + "clash_mode": "Global", + "server": "google" + }, + { + "rule_set": "geosite-geolocation-cn", + "server": "local" + }, + { + "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 } - ], - "outbound": "block" + ] }, - { - "geosite": "geolocation-cn", - "outbound": "direct" + "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": "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": { + "cache_file": { + "enabled": true, + "store_rdrc": true + }, + "clash_api": { + "default_mode": "Enhanced" + } } - ] - } - } - ``` + } + ``` -=== ":material-router-network: Route rules (1.8.0+)" +=== ":material-router-network: Route rules" ```json { diff --git a/docs/migration.md b/docs/migration.md index 44ddd8337d..b282a90fc7 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -2,6 +2,28 @@ 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. + +### `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, +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..bd63bf1767 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -2,6 +2,27 @@ icon: material/arrange-bring-forward --- +## 1.9.0 + +!!! warning "不稳定的" + + 该版本仍在开发中,迁移指南可能将在未来更改。 + +### `domain_suffix` 行为更新 + +由于历史原因,sing-box 的 `domain_suffix` 规则匹配字面前缀,而不与其他项目相同。 + +sing-box 1.9.0 修改了 `domain_suffix` 的行为:如果规则值以 `.` 为前缀则行为不变,否则改为匹配 `(domain|.+\.domain)`。 + +### 对 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 迁移到独立选项 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 e998ebb859..ab359242cb 100644 --- a/experimental/cachefile/fakeip.go +++ b/experimental/cachefile/fakeip.go @@ -89,34 +89,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 } @@ -137,13 +137,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/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 { diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 1eec8448af..8a92198d97 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -6,6 +6,7 @@ import ( "errors" "net" "net/http" + "net/url" "os" "strings" "time" @@ -95,6 +96,18 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ AllowedHeaders: []string{"Content-Type", "Authorization"}, MaxAge: 300, }) + domainList := map[string]bool{ + "clash.razord.top": true, + "yacd.haishan.me": true, + "yacd.metacubex.one": true, + "d.metacubex.one": true, + "metacubex.github.io": true, + "metacubexd.pages.dev": true, + } + for _, domain := range options.TrustedDomain { + domainList[domain] = true + } + chiRouter.Use(setPrivateNetworkAccess(domainList)) chiRouter.Use(cors.Handler) chiRouter.Group(func(r chi.Router) { r.Use(authentication(options.Secret)) @@ -270,6 +283,42 @@ func castMetadata(metadata adapter.InboundContext) trafficontrol.Metadata { } } +func setPrivateNetworkAccess(domainList map[string]bool) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" && checkDomain(r, domainList) { + w.Header().Add("Access-Control-Allow-Private-Network", "true") + } + next.ServeHTTP(w, r) + }) + } +} + +func parseOriginHost(r *http.Request) string { + if referer := r.Header.Get("Referer"); referer != "" { + URL, err := url.Parse(referer) + if err == nil { + return URL.Host + } + } + if origin := r.Header.Get("Origin"); origin != "" { + URL, err := url.Parse(origin) + if err == nil { + return URL.Host + } + } + return "" +} + +func checkDomain(r *http.Request, list map[string]bool) bool { + host := parseOriginHost(r) + if host == "" { + return false + } + _, exists := list[host] + return exists +} + func authentication(serverSecret string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { 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/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/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/go.mod b/go.mod index 09d46cef9a..0b529d9531 100644 --- a/go.mod +++ b/go.mod @@ -23,17 +23,17 @@ 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/quic-go v0.40.1 + github.com/sagernet/gvisor v0.0.0-20240315080113-799fb6b6d311 + github.com/sagernet/quic-go v0.42.0-beta.2 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 - github.com/sagernet/sing v0.3.8 - github.com/sagernet/sing-dns v0.1.14 + github.com/sagernet/sing v0.4.0-beta.6 + 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.8 + github.com/sagernet/sing-quic v0.1.9-beta.3 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.5 + github.com/sagernet/sing-tun v0.2.6-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 @@ -80,17 +80,16 @@ require ( github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect - github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect github.com/zeebo/blake3 v0.2.3 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect - golang.org/x/mod v0.14.0 // indirect + golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 // indirect + golang.org/x/mod v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.17.0 // indirect + golang.org/x/tools v0.19.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 63ab4c8f80..69a7daf9d5 100644 --- a/go.sum +++ b/go.sum @@ -100,31 +100,31 @@ 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.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.2 h1:E6vTGhveFE9Tnhu5rrt3M5fnENuqBJOv8gPcFPz0cSM= +github.com/sagernet/quic-go v0.42.0-beta.2/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= -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-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 v0.4.0-beta.6 h1:z7plSB0lWX2vt3e5UYYNQi1+bNqfxU9uh1nWlPZFwj8= +github.com/sagernet/sing v0.4.0-beta.6/go.mod h1:+60H3Cm91RnL9dpVGWDPHt0zTQImO9Vfqt9a4rSambI= +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.8 h1:G4iBXAKIII+uTzd55oZ/9cAQswGjlvHh/0yKMQioDS0= -github.com/sagernet/sing-quic v0.1.8/go.mod h1:2w7DZXtf4MPjIGpovA3+vpI6bvOf1n1f9cQ1E2qQJSg= +github.com/sagernet/sing-quic v0.1.9-beta.3 h1:turuRtN6xfAxWMseZEzwNOA6EO2ZAW82BrfPJUrtN3Q= +github.com/sagernet/sing-quic v0.1.9-beta.3/go.mod h1:oXe0g8T7edh2Xxl0QcpTO4Tret8M478LpMcr3CPuqWE= 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= 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.5 h1:DtXyS2weCc87bXe4GQkLkW7OyhXvKpBi2ppPpH7aHJM= -github.com/sagernet/sing-tun v0.2.5/go.mod h1:GtKY1Sr2CCWLHPjVj9GZpBFZ/KoXOzVBSxXvmCCPxT4= +github.com/sagernet/sing-tun v0.2.6-beta.1 h1:6yfOJQaa706/VAKb7SVa8ziXppTxVOg/p9p63qk2N/o= +github.com/sagernet/sing-tun v0.2.6-beta.1/go.mod h1:E+KwQKzYkdGEhfIxjmoaB1ZkADaxeXUNzx6GRDRKOfE= 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= @@ -137,8 +137,6 @@ github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8 h1:R0OMYASco github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8/go.mod h1:K4J7/npM+VAMUeUmTa2JaA02JmyheP0GpRBOUvn3ecc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= -github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg= -github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -170,10 +168,10 @@ golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaE golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= -golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 h1:6R2FC06FonbXQ8pK11/PDFY6N6LWlf9KlzibaCapmqc= +golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= @@ -199,8 +197,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= 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/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.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..ddf9723f9a 100644 --- a/include/quic_stub.go +++ b/include/quic_stub.go @@ -11,15 +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" ) -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) { + dns.RegisterTransport([]string{"quic", "h3"}, func(options dns.TransportOptions) (dns.Transport, error) { return nil, C.ErrQUICNotIncluded }) v2ray.RegisterQUICConstructor( 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 +} 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/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/experimental.go b/option/experimental.go index c685f51f54..175cf59a97 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -8,20 +8,23 @@ 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 { - ExternalController string `json:"external_controller,omitempty"` - ExternalUI string `json:"external_ui,omitempty"` - ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` - ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` - Secret string `json:"secret,omitempty"` - DefaultMode string `json:"default_mode,omitempty"` - ModeList []string `json:"-"` + ExternalController string `json:"external_controller,omitempty"` + ExternalUI string `json:"external_ui,omitempty"` + ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` + ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` + TrustedDomain Listable[string] `json:"trusted_domain,omitempty"` + Secret string `json:"secret,omitempty"` + DefaultMode string `json:"default_mode,omitempty"` + ModeList []string `json:"-"` // Deprecated: migrated to global cache file CacheFile string `json:"cache_file,omitempty"` 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/option/rule_dns.go b/option/rule_dns.go index 443f931475..ababea416b 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -65,38 +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"` - 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"` + 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 { @@ -105,16 +110,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/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"` } 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 -} diff --git a/route/router.go b/route/router.go index a62a318b8b..2283aa8713 100644 --- a/route/router.go +++ b/route/router.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "os/user" + "runtime" "strings" "time" @@ -22,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" @@ -39,9 +39,10 @@ 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" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" ) @@ -68,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 @@ -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 @@ -120,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), autoDetectInterface: options.AutoDetectInterface, @@ -136,7 +140,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) @@ -222,7 +236,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, "]") } @@ -262,7 +289,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] } @@ -321,12 +353,28 @@ func NewRouter( router.interfaceMonitor = interfaceMonitor } - if ntpOptions.Enabled { - timeService, err := ntp.NewService(ctx, router, logFactory.NewLogger("ntp"), ntpOptions) + if runtime.GOOS == "windows" { + powerListener, err := winpowrprof.NewEventListener(router.notifyWindowsPowerEvent) if err != nil { - return nil, err + return nil, E.Cause(err, "initialize power listener") } - service.ContextWith[serviceNTP.TimeService](ctx, timeService) + router.powerListener = powerListener + } + + if ntpOptions.Enabled { + ntpDialer, err := dialer.New(router, ntpOptions.DialerOptions) + if err != nil { + 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 @@ -560,6 +608,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 @@ -578,6 +636,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() @@ -657,6 +720,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 +1259,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() + } +} diff --git a/route/router_dns.go b/route/router_dns.go index 8ae9171002..fee468fa54 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,55 @@ 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 currentRuleIndex, 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 + } + _, isFakeIP := transport.(adapter.FakeIPTransport) + if isFakeIP && !allowFakeIP { + continue + } + ruleIndex := currentRuleIndex + if index != -1 { + ruleIndex += index + 1 + } + r.dnsLogger.DebugContext(ctx, "match[", ruleIndex, "] ", rule.String(), " => ", detour) + if (isFakeIP && !r.dnsIndependentCache) || rule.DisableCache() { + ctx = dns.ContextWithDisableCache(ctx, true) + } + 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 { + 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 } } @@ -80,13 +94,15 @@ 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 { - 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,50 +113,134 @@ 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 ( + 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() + 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()))) + } else { + r.dnsLogger.ErrorContext(ctx, E.Cause(err, "exchange failed for ")) + } + } + if addressLimit && rejected { + continue + } + break } } - if len(message.Question) > 0 && response != nil { - LogDNSAnswers(r.dnsLogger, ctx, message.Question[0].Name, response.Answer) + 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) { + 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 - ctx, transport, transportStrategy := r.matchDNS(ctx, false) - if strategy == dns.DomainStrategyAsIS { - strategy = transportStrategy - } - 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 + var ( + transport dns.Transport + transportStrategy dns.DomainStrategy + rule adapter.DNSRule + ruleIndex int + ) + 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 + responseAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, strategy, func(responseAddrs []netip.Addr) bool { + metadata.DestinationAddresses = responseAddrs + return rule.MatchAddressLimit(metadata) + }) + } else { + addressLimit = false + responseAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, strategy) + } + cancel() + if err != nil { + 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(responseAddrs) == 0 { + r.dnsLogger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result") + err = dns.RCodeNameError + } + if !addressLimit || err == nil { + break + } + } + if len(responseAddrs) > 0 { + r.dnsLogger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(responseAddrs), " ")) } - return addrs, err + return responseAddrs, err } func (r *Router) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) { @@ -154,10 +254,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..85ded9615d 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -1,10 +1,13 @@ package route import ( + "net/netip" + "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" ) @@ -37,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) { @@ -47,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) @@ -111,6 +116,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 +129,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) @@ -196,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) } @@ -211,12 +234,45 @@ 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 + } + for _, rawRule := range r.items { + ruleSet, isRuleSet := rawRule.(*RuleSetItem) + if !isRuleSet { + continue + } + if ruleSet.ContainsDestinationIPCIDRRule() { + 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 { abstractLogicalRule disableCache bool rewriteTTL *uint32 + clientSubnet *netip.Addr } func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { @@ -254,3 +310,51 @@ func (r *LogicalDNSRule) DisableCache() bool { 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) { + 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..67ac3a1e44 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 { @@ -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 } diff --git a/route/rule_item_rule_set.go b/route/rule_item_rule_set.go index 959b2f6110..482a9c7b45 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,15 @@ func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { return false } +func (r *RuleSetItem) ContainsDestinationIPCIDRRule() bool { + if r.ipcidrMatchSource { + return false + } + 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 } diff --git a/transport/dhcp/server.go b/transport/dhcp/server.go index 1a2c2938b2..8325c37b2f 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,21 @@ 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, + options: options, router: router, - logger: logger, interfaceName: linkURL.Host, autoInterface: linkURL.Host == "auto", } @@ -72,7 +67,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 +153,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 +170,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 +182,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 +220,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 +250,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 } 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 }