diff --git a/adapter/experimental.go b/adapter/experimental.go index 2a6776cd..5e1cbd9d 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 bcf3ea5f..063671c1 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 9d8bcb3e..786a777e 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.Prefix + 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/clients/android b/clients/android index 9d463b26..e20fa632 160000 --- a/clients/android +++ b/clients/android @@ -1 +1 @@ -Subproject commit 9d463b269a2af428e5c3d0e94bab260f45721fab +Subproject commit e20fa632f6fd89f7e9411ebab21fbce52b89120e diff --git a/clients/apple b/clients/apple index 17eec086..0cbe335c 160000 --- a/clients/apple +++ b/clients/apple @@ -1 +1 @@ -Subproject commit 17eec0861543489ad4519eef974c65d2a159244d +Subproject commit 0cbe335cbbaef9747171c44b169c23daf2d89261 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 00000000..473c8222 --- /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 fdae8a1c..334bcfa8 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 } -//go:linkname tlsReadRecord crypto/tls.(*Conn).readRecord -func tlsReadRecord(c *tls.STDConn) error +var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) -//go:linkname tlsHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage -func tlsHandlePostHandshakeMessage(c *tls.STDConn) 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 stdTLSReadRecord crypto/tls.(*Conn).readRecord +func stdTLSReadRecord(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 00000000..6a0d5b5f --- /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 00000000..ebdb2251 --- /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/dialer/default.go b/common/dialer/default.go index 0234b1b9..4fbad07d 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -32,14 +32,20 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi var dialer net.Dialer var listener net.ListenConfig if options.BindInterface != "" { - bindFunc := control.BindToInterface(router.InterfaceFinder(), options.BindInterface, -1) + var interfaceFinder control.InterfaceFinder + if router != nil { + interfaceFinder = router.InterfaceFinder() + } else { + interfaceFinder = control.NewDefaultInterfaceFinder() + } + bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1) dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) - } else if router.AutoDetectInterface() { + } else if router != nil && router.AutoDetectInterface() { bindFunc := router.AutoDetectInterfaceFunc() dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) - } else if router.DefaultInterface() != "" { + } else if router != nil && router.DefaultInterface() != "" { bindFunc := control.BindToInterface(router.InterfaceFinder(), router.DefaultInterface(), -1) dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) @@ -47,7 +53,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi if options.RoutingMark != 0 { dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark)) listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark)) - } else if router.DefaultMark() != 0 { + } else if router != nil && router.DefaultMark() != 0 { dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark())) listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark())) } @@ -63,6 +69,9 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi } else { dialer.Timeout = C.TCPTimeout } + // TODO: Add an option to customize the keep alive period + dialer.KeepAlive = C.TCPKeepAliveInitial + dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(C.TCPKeepAliveInitial, C.TCPKeepAliveInterval)) var udpFragment bool if options.UDPFragment != nil { udpFragment = *options.UDPFragment diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index bbb4b3a9..a1721b28 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -13,6 +13,9 @@ func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error) if options.IsWireGuardListener { return NewDefault(router, options) } + if router == nil { + return NewDefault(nil, options) + } var ( dialer N.Dialer err error diff --git a/common/process/searcher_windows.go b/common/process/searcher_windows.go index f13b440e..5b3d59b5 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/common/tls/ech_quic.go b/common/tls/ech_quic.go index b9cc5ede..fef506db 100644 --- a/common/tls/ech_quic.go +++ b/common/tls/ech_quic.go @@ -27,11 +27,10 @@ func (c *echClientConfig) DialEarly(ctx context.Context, conn net.PacketConn, ad return quic.DialEarly(ctx, conn, addr, c.config, config) } -func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config, enableDatagrams bool) http.RoundTripper { +func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config) http.RoundTripper { return &http3.RoundTripper{ TLSClientConfig: c.config, - QuicConfig: quicConfig, - EnableDatagrams: enableDatagrams, + QUICConfig: quicConfig, Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { quicConn, err := quic.DialEarly(ctx, conn, serverAddr.UDPAddr(), tlsCfg, cfg) if err != nil { diff --git a/constant/quic.go b/constant/quic.go new file mode 100644 index 00000000..50bddf88 --- /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 00000000..95b47fef --- /dev/null +++ b/constant/quic_stub.go @@ -0,0 +1,5 @@ +//go:build !with_quic + +package constant + +const WithQUIC = false diff --git a/constant/timeout.go b/constant/timeout.go index 0d7a0b7d..b270a050 100644 --- a/constant/timeout.go +++ b/constant/timeout.go @@ -3,6 +3,8 @@ package constant import "time" const ( + TCPKeepAliveInitial = 10 * time.Minute + TCPKeepAliveInterval = 75 * time.Second TCPTimeout = 5 * time.Second ReadPayloadTimeout = 300 * time.Millisecond DNSTimeout = 10 * time.Second diff --git a/docs/changelog.md b/docs/changelog.md index 9a6d0ef1..1b7ab6ad 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,16 +2,119 @@ icon: material/alert-decagram --- +### 1.9.0 + +* Fixes and improvements + +Important changes since 1.8: + +* `domain_suffix` behavior update **1** +* `process_path` format update on Windows **2** +* Add address filter DNS rule items **3** +* Add support for `client-subnet` DNS options **4** +* Add rejected DNS response cache support **5** +* Add `bypass_domain` and `search_domain` platform HTTP proxy options **6** +* Fix missing `rule_set_ipcidr_match_source` item in DNS rules **7** +* Handle Windows power events +* Always disable cache for fake-ip DNS transport if `dns.independent_cache` disabled +* Improve DNS truncate behavior +* Update Hysteria protocol +* Update quic-go to v0.43.1 +* Update gVisor to 20240422.0 +* Mitigating TunnelVision attacks **8** + +**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. + +**4**: + +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. + +**5**: + +The new feature allows you to cache the check results of +[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration. + +**6**: + +See [TUN](/configuration/inbound/tun) inbound. + +**7**: + +See [DNS Rule](/configuration/dns/rule/). + +**8**: + +See [TunnelVision](/manual/misc/tunnelvision). + +#### 1.9.0-rc.22 + +* Fixes and improvements + +#### 1.9.0-rc.20 + +* Prioritize `*_route_address` in linux auto-route +* Fix `*_route_address` in darwin auto-route + #### 1.8.14 * Fix hysteria2 panic * Fixes and improvements +#### 1.9.0-rc.18 + +* Add custom prefix support in EDNS0 client subnet options +* Fix hysteria2 crash +* Fix `store_rdrc` corrupted +* Update quic-go to v0.43.1 +* Fixes and improvements + +#### 1.9.0-rc.16 + +* Mitigating TunnelVision attacks **1** +* Fixes and improvements + +**1**: + +See [TunnelVision](/manual/misc/tunnelvision). + +#### 1.9.0-rc.15 + +* Fixes and improvements + #### 1.8.13 * Fix fake-ip mapping * Fixes and improvements +#### 1.9.0-rc.14 + +* Fixes and improvements + +#### 1.9.0-rc.13 + +* Update Hysteria protocol +* Update quic-go to v0.43.0 +* Update gVisor to 20240422.0 +* Fixes and improvements + #### 1.8.12 * Now we have official APT and DNF repositories **1** @@ -22,6 +125,10 @@ icon: material/alert-decagram Including stable and beta versions, see https://sing-box.sagernet.org/installation/package-manager/ +#### 1.9.0-rc.11 + +* Fixes and improvements + #### 1.8.11 * Fixes and improvements @@ -30,6 +137,24 @@ Including stable and beta versions, see https://sing-box.sagernet.org/installati * Fixes and improvements +#### 1.9.0-beta.17 + +* Update `quic-go` to v0.42.0 +* Fixes and improvements + +#### 1.9.0-beta.16 + +* Fixes and improvements + +_Our Testflight distribution has been temporarily blocked by Apple (possibly due to too many beta versions) +and you cannot join the test, install or update the sing-box beta app right now. +Please wait patiently for processing._ + +#### 1.9.0-beta.14 + +* Update gVisor to 20240212.0-65-g71212d503 +* Fixes and improvements + #### 1.8.9 * Fixes and improvements @@ -38,14 +163,125 @@ Including stable and beta versions, see https://sing-box.sagernet.org/installati * Fixes and improvements +#### 1.9.0-beta.7 + +* Fixes and improvements + +#### 1.9.0-beta.6 + +* Fix address filter DNS rule items **1** +* Fix DNS outbound responding with wrong data +* Fixes and improvements + +**1**: + +Fixed an issue where address filter DNS rule was incorrectly rejected under certain circumstances. +If you have enabled `store_rdrc` to save results, consider clearing the cache file. + #### 1.8.7 * Fixes and improvements +#### 1.9.0-alpha.15 + +* Fixes and improvements + +#### 1.9.0-alpha.14 + +* Improve DNS truncate behavior +* Fixes and improvements + +#### 1.9.0-alpha.13 + +* Fixes and improvements + #### 1.8.6 * Fixes and improvements +#### 1.9.0-alpha.12 + +* Handle Windows power events +* Always disable cache for fake-ip DNS transport if `dns.independent_cache` disabled +* Fixes and improvements + +#### 1.9.0-alpha.11 + +* Fix missing `rule_set_ipcidr_match_source` item in DNS rules **1** +* Fixes and improvements + +**1**: + +See [DNS Rule](/configuration/dns/rule/). + +#### 1.9.0-alpha.10 + +* Add `bypass_domain` and `search_domain` platform HTTP proxy options **1** +* Fixes and improvements + +**1**: + +See [TUN](/configuration/inbound/tun) inbound. + +#### 1.9.0-alpha.8 + +* Add rejected DNS response cache support **1** +* Fixes and improvements + +**1**: + +The new feature allows you to cache the check results of +[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration. + +#### 1.9.0-alpha.7 + +* Update gVisor to 20240206.0 +* Fixes and improvements + +#### 1.9.0-alpha.6 + +* Fixes and improvements + +#### 1.9.0-alpha.3 + +* Update `quic-go` to v0.41.0 +* Fixes and improvements + +#### 1.9.0-alpha.2 + +* Add support for `client-subnet` DNS options **1** +* Fixes and improvements + +**1**: + +See [DNS](/configuration/dns), [DNS Server](/configuration/dns/server) and [DNS Rules](/configuration/dns/rule). + +Since this feature makes the scenario mentioned in `alpha.1` no longer leak DNS requests, +the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) has been updated. + +#### 1.9.0-alpha.1 + +* `domain_suffix` behavior update **1** +* `process_path` format update on Windows **2** +* Add address filter DNS rule items **3** + +**1**: + +See [Migration](/migration/#domain_suffix-behavior-update). + +**2**: + +See [Migration](/migration/#process_path-format-update-on-windows). + +**3**: + +The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS +if using this method. + +See [Address Filter Fields](/configuration/dns/rule#address-filter-fields). + +[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated. + #### 1.8.5 * Fixes and improvements @@ -62,7 +298,7 @@ Including stable and beta versions, see https://sing-box.sagernet.org/installati * Fixes and improvements -#### 1.8.0 +### 1.8.0 * Fixes and improvements @@ -353,7 +589,7 @@ New commands manage GeoIP, Geosite and rule set resources, and help you migrate Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules. -#### 1.7.0 +### 1.7.0 * Fixes and improvements @@ -395,7 +631,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 @@ -432,7 +668,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 @@ -513,7 +749,7 @@ Introduced in V2Ray 5.10.0. The new HTTPUpgrade transport has better performance than WebSocket and is better suited for CDN abuse. -#### 1.6.0 +### 1.6.0 * Fixes and improvements @@ -692,7 +928,7 @@ introduce new issues. None of the existing Golang BBR congestion control implementations have been reviewed or unit tested. This update is intended to address the multi-send defects of the old implementation and may introduce new issues. -#### 1.5.0 +### 1.5.0 * Fixes and improvements @@ -886,7 +1122,7 @@ All inbounds and outbounds are supported, including `Naiveproxy`, `Hysteria`, `T * Fixes and improvements -#### 1.4.0 +### 1.4.0 * Fix bugs and update dependencies @@ -1028,7 +1264,7 @@ The old testflight link and app are no longer valid. * Fixes and improvements -#### 1.3.0 +### 1.3.0 * Fix bugs and update dependencies @@ -1220,7 +1456,7 @@ to `domain` rule. * Flush DNS cache for macOS when tun start/close * Fix tun's DNS hijacking compatibility with systemd-resolved -#### 1.2.0 +### 1.2.0 * Fix bugs and update dependencies diff --git a/docs/configuration/dns/index.md b/docs/configuration/dns/index.md index e2832c42..c0eafccc 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,12 @@ 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 -[FakeIP](./fakeip/) settings. +!!! question "Since sing-box 1.9.0" + +Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default. + +If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. + +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 afc6e931..ba390cef 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,16 @@ 由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。 +#### client_subnet + +!!! question "自 sing-box 1.9.0 起" + +默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 + +可以被 `servers.[].client_subnet` 或 `rules.[].client_subnet` 覆盖。 + #### fakeip [FakeIP](./fakeip/) 设置。 diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 68cc32cf..4c4abacb 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/24" }, { "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/24" } ] } @@ -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,46 @@ 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 prefix to every query by default. + +If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. + +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 5b1d7501..796d29e8 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/24" }, { "type": "logical", "mode": "and", "rules": [], "server": "local", - "disable_cache": false + "disable_cache": false, + "client_subnet": "127.0.0.1/24" } ] } @@ -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,46 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 重写 DNS 回应中的 TTL。 +#### client_subnet + +!!! question "自 sing-box 1.9.0 起" + +默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 + +将覆盖 `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 +385,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### rules -包括的规则。 \ No newline at end of file +包括的规则。 diff --git a/docs/configuration/dns/server.md b/docs/configuration/dns/server.md index 545810bf..3c524581 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,22 @@ 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 prefix to every query by default. + +If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. + +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 36bcde5d..baa11751 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,15 @@ DNS 服务器的地址。 用于连接到 DNS 服务器的出站的标签。 如果为空,将使用默认出站。 + +#### client_subnet + +!!! question "自 sing-box 1.9.0 起" + +默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 + +可以被 `rules.[].client_subnet` 覆盖。 + +将覆盖 `dns.client_subnet`。 diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md index 66e30ef9..b30538e5 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 f4417ede..6d86dc84 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 0525d14d..e1ca9815 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 5a490e58..092769ac 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 4ddcc41a..a1a515cf 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 4be70aa7..01246c44 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 cd2ec35d..00343e22 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 1f5bf0ac..e9deec75 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 6e6c2ae0..1d5d8d0f 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 71c66704..73d31d64 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 4cd91d22..c3f51f1f 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 e853d72e..5de28132 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 5deb44f5..7b2a7e7e 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 290268f4..68d4f66d 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 9bedef86..62d33c6c 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 0e6f9896..cba35bc5 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 6ab62eb2..e766904b 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 5aff55b3..ba2f741e 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 8e1934ae..ee5e48e0 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 a5c7bec4..b1441a8a 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 5a75945d..360c4536 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/misc/tunnelvision.md b/docs/manual/misc/tunnelvision.md new file mode 100644 index 00000000..0d6caf76 --- /dev/null +++ b/docs/manual/misc/tunnelvision.md @@ -0,0 +1,38 @@ +--- +icon: material/book-lock-open +--- + +# TunnelVision + +TunnelVision is an attack that uses DHCP option 121 to set higher priority routes +so that traffic does not go through the VPN. + +Reference: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-3661 + +## Status + +### Android + +Android does not handle DHCP option 121 and is not affected. + +### Apple platforms + +Update [sing-box graphical client](/clients/apple/#download) to `1.9.0-rc.16` or newer, +then enable `includeAllNetworks` in `Settings` — `Packet Tunnel` and you will be unaffected. + +Note: when `includeAllNetworks` is enabled, the default TUN stack is changed to `gvisor`, +and the `system` and `mixed` stacks are not available. + +### Linux + +Update sing-box to `1.9.0-rc.16` or newer, rules generated by `auto-route` are unaffected. + +### Windows + +No solution yet. + +## Workarounds + +* Don't connect to untrusted networks +* Relay untrusted network through another device +* Just ignore it diff --git a/docs/manual/proxy-protocol/tuic.md b/docs/manual/proxy-protocol/tuic.md deleted file mode 100644 index a2e01d88..00000000 --- a/docs/manual/proxy-protocol/tuic.md +++ /dev/null @@ -1,208 +0,0 @@ ---- -icon: material/alpha-t-box ---- - -# TUIC - -A recently popular Chinese-made simple protocol based on QUIC, the selling point is the BBR congestion control algorithm. - -!!! warning - - Even though GFW rarely blocks UDP-based proxies, such protocols actually have far more characteristics than TCP based proxies. - -| Specification | Binary Characteristics | Active Detect Hiddenness | -|-----------------------------------------------------------|------------------------|--------------------------| -| [GitHub](https://github.com/EAimTY/tuic/blob/dev/SPEC.md) | :material-alert: | :material-check: | - -## Password Generator - -| Generated UUID | Generated Password | Action | -|------------------------|----------------------------|-----------------------------------------------------------------| -| | | | - - - -## :material-server: Server Example - -=== ":material-harddisk: With local certificate" - - ```json - { - "inbounds": [ - { - "type": "tuic", - "listen": "::", - "listen_port": 8080, - "users": [ - { - "name": "sekai", - "uuid": "", - "password": "" - } - ], - "congestion_control": "bbr", - "tls": { - "enabled": true, - "server_name": "example.org", - "key_path": "/path/to/key.pem", - "certificate_path": "/path/to/certificate.pem" - } - } - ] - } - ``` - -=== ":material-auto-fix: With ACME" - - ```json - { - "inbounds": [ - { - "type": "tuic", - "listen": "::", - "listen_port": 8080, - "users": [ - { - "name": "sekai", - "uuid": "", - "password": "" - } - ], - "congestion_control": "bbr", - "tls": { - "enabled": true, - "server_name": "example.org", - "acme": { - "domain": "example.org", - "email": "admin@example.org" - } - } - } - ] - } - ``` - -=== ":material-cloud: With ACME and Cloudflare API" - - ```json - { - "inbounds": [ - { - "type": "tuic", - "listen": "::", - "listen_port": 8080, - "users": [ - { - "name": "sekai", - "uuid": "", - "password": "" - } - ], - "congestion_control": "bbr", - "tls": { - "enabled": true, - "server_name": "example.org", - "acme": { - "domain": "example.org", - "email": "admin@example.org", - "dns01_challenge": { - "provider": "cloudflare", - "api_token": "my_token" - } - } - } - } - ] - } - ``` - -## :material-cellphone-link: Client Example - -=== ":material-web-check: With valid certificate" - - ```json - { - "outbounds": [ - { - "type": "tuic", - "server": "127.0.0.1", - "server_port": 8080, - "uuid": "", - "password": "", - "congestion_control": "bbr", - "tls": { - "enabled": true, - "server_name": "example.org" - } - } - ] - } - ``` - -=== ":material-check: With self-sign certificate" - - !!! info "Tip" - - Use `sing-box merge` command to merge configuration and certificate into one file. - - ```json - { - "outbounds": [ - { - "type": "tuic", - "server": "127.0.0.1", - "server_port": 8080, - "uuid": "", - "password": "", - "congestion_control": "bbr", - "tls": { - "enabled": true, - "server_name": "example.org", - "certificate_path": "/path/to/certificate.pem" - } - } - ] - } - ``` - -=== ":material-alert: Ignore certificate verification" - - ```json - { - "outbounds": [ - { - "type": "tuic", - "server": "127.0.0.1", - "server_port": 8080, - "uuid": "", - "password": "", - "congestion_control": "bbr", - "tls": { - "enabled": true, - "server_name": "example.org", - "insecure": true - } - } - ] - } - ``` - diff --git a/docs/manual/proxy/client.md b/docs/manual/proxy/client.md index 3ba7eacc..1cf5a1ce 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": [ + === ":material-shield-off: With DNS leaks" + + ```json { - "type": "direct", - "tag": "direct" - }, - { - "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" - }, - { - "clash_mode": "Global", - "outbound": "default" - }, - { - "type": "logical", - "mode": "or", "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" + }, + { + "clash_mode": "Default", + "server": "google" + }, + { + "type": "logical", + "mode": "and", + "rules": [ + { + "rule_set": "geosite-geolocation-!cn", + "invert": true + }, + { + "rule_set": "geoip-cn" + } + ], + "server": "local" } - ], - "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-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": [ + { + "outbound": "any", + "server": "local" + }, + { + "clash_mode": "Direct", + "server": "local" + }, + { + "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/24" // Any China client IP address + } + ] + }, + "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" ```json { diff --git a/docs/migration.md b/docs/migration.md index 44ddd833..b282a90f 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 0422833d..bd63bf17 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 43b84562..1027588f 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,26 @@ 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 + QType uint16 } func New(ctx context.Context, options option.CacheFileOptions) *CacheFile { @@ -61,14 +71,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 41c1dee6..8fe0f113 100644 --- a/experimental/cachefile/fakeip.go +++ b/experimental/cachefile/fakeip.go @@ -97,7 +97,7 @@ 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() if oldDomain, loaded := c.saveDomain[address]; loaded { if address.Is4() { delete(c.saveAddress4, oldDomain) @@ -111,27 +111,27 @@ func (c *CacheFile) FakeIPStoreAsync(address netip.Addr, domain string, logger l } 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 } @@ -152,13 +152,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 00000000..c4800951 --- /dev/null +++ b/experimental/cachefile/rdrc.go @@ -0,0 +1,109 @@ +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("rdrc2") + +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, qType uint16) (rejected bool) { + c.saveRDRCAccess.RLock() + rejected, cached := c.saveRDRC[saveRDRCCacheKey{transportName, qName, qType}] + c.saveRDRCAccess.RUnlock() + if cached { + return + } + key := buf.Get(2 + len(qName)) + binary.BigEndian.PutUint16(key, qType) + copy(key[2:], qName) + defer buf.Put(key) + 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(key) + 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(key) + }) + } + return +} + +func (c *CacheFile) SaveRDRC(transportName string, qName string, qType uint16) 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 + } + key := buf.Get(2 + len(qName)) + binary.BigEndian.PutUint16(key, qType) + copy(key[2:], qName) + defer buf.Put(key) + expiresAt := buf.Get(8) + defer buf.Put(expiresAt) + binary.BigEndian.PutUint64(expiresAt, uint64(time.Now().Add(c.rdrcTimeout).Unix())) + return bucket.Put(key, expiresAt) + }) +} + +func (c *CacheFile) SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger) { + saveKey := saveRDRCCacheKey{transportName, qName, qType} + c.saveRDRCAccess.Lock() + c.saveRDRC[saveKey] = true + c.saveRDRCAccess.Unlock() + go func() { + err := c.SaveRDRC(transportName, qName, qType) + 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 805fbd5b..872d9b99 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/libbox/config.go b/experimental/libbox/config.go index bad61dbc..b7731143 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -9,7 +9,6 @@ import ( "github.com/sagernet/sing-box" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/process" - "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" @@ -75,7 +74,7 @@ func (s *platformInterfaceStub) UsePlatformInterfaceGetter() bool { return true } -func (s *platformInterfaceStub) Interfaces() ([]platform.NetworkInterface, error) { +func (s *platformInterfaceStub) Interfaces() ([]control.Interface, error) { return nil, os.ErrInvalid } @@ -83,6 +82,10 @@ func (s *platformInterfaceStub) UnderNetworkExtension() bool { return false } +func (s *platformInterfaceStub) IncludeAllNetworks() bool { + return false +} + func (s *platformInterfaceStub) ClearDNSCache() { } diff --git a/experimental/libbox/dns.go b/experimental/libbox/dns.go index fcdaaa92..e1f8bcc3 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/platform.go b/experimental/libbox/platform.go index 451a72a9..4078140f 100644 --- a/experimental/libbox/platform.go +++ b/experimental/libbox/platform.go @@ -19,6 +19,7 @@ type PlatformInterface interface { UsePlatformInterfaceGetter() bool GetInterfaces() (NetworkInterfaceIterator, error) UnderNetworkExtension() bool + IncludeAllNetworks() bool ReadWIFIState() *WIFIState ClearDNSCache() } diff --git a/experimental/libbox/platform/interface.go b/experimental/libbox/platform/interface.go index 54d35fa3..3bec13fa 100644 --- a/experimental/libbox/platform/interface.go +++ b/experimental/libbox/platform/interface.go @@ -2,7 +2,6 @@ package platform import ( "context" - "net/netip" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/process" @@ -20,16 +19,10 @@ type Interface interface { UsePlatformDefaultInterfaceMonitor() bool CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor UsePlatformInterfaceGetter() bool - Interfaces() ([]NetworkInterface, error) + Interfaces() ([]control.Interface, error) UnderNetworkExtension() bool + IncludeAllNetworks() bool ClearDNSCache() ReadWIFIState() adapter.WIFIState process.Searcher } - -type NetworkInterface struct { - Index int - MTU int - Name string - Addresses []netip.Prefix -} diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 030aee8d..0a54d7ab 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -192,14 +192,14 @@ func (w *platformInterfaceWrapper) UsePlatformInterfaceGetter() bool { return w.iif.UsePlatformInterfaceGetter() } -func (w *platformInterfaceWrapper) Interfaces() ([]platform.NetworkInterface, error) { +func (w *platformInterfaceWrapper) Interfaces() ([]control.Interface, error) { interfaceIterator, err := w.iif.GetInterfaces() if err != nil { return nil, err } - var interfaces []platform.NetworkInterface + var interfaces []control.Interface for _, netInterface := range iteratorToArray[*NetworkInterface](interfaceIterator) { - interfaces = append(interfaces, platform.NetworkInterface{ + interfaces = append(interfaces, control.Interface{ Index: int(netInterface.Index), MTU: int(netInterface.MTU), Name: netInterface.Name, @@ -213,6 +213,10 @@ func (w *platformInterfaceWrapper) UnderNetworkExtension() bool { return w.iif.UnderNetworkExtension() } +func (w *platformInterfaceWrapper) IncludeAllNetworks() bool { + return w.iif.IncludeAllNetworks() +} + func (w *platformInterfaceWrapper) ClearDNSCache() { w.iif.ClearDNSCache() } diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go index a4514dfe..ea468f39 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 53add3ce..5c6e3370 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 af2b0d2e..81993d51 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/go-chi/chi/v5 v5.0.12 github.com/go-chi/cors v1.2.1 github.com/go-chi/render v1.0.3 - github.com/gofrs/uuid/v5 v5.1.0 + github.com/gofrs/uuid/v5 v5.2.0 github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 github.com/libdns/alidns v1.0.3 github.com/libdns/cloudflare v0.1.1 @@ -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-20240428053021-e691de28565f + github.com/sagernet/quic-go v0.43.1-beta.1 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.20 + github.com/sagernet/sing-dns v0.2.0-beta.18 github.com/sagernet/sing-mux v0.2.0 - github.com/sagernet/sing-quic v0.1.15 + github.com/sagernet/sing-quic v0.2.0-beta.5 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.7 + github.com/sagernet/sing-tun v0.3.0-beta.6 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 @@ -44,9 +44,9 @@ require ( github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.22.0 - golang.org/x/net v0.24.0 - golang.org/x/sys v0.19.0 + golang.org/x/crypto v0.23.0 + golang.org/x/net v0.25.0 + golang.org/x/sys v0.20.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 google.golang.org/grpc v1.63.2 google.golang.org/protobuf v1.33.0 @@ -78,18 +78,18 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect 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/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba // 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-20240416160154-fe59bbe5cc7f // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/text v0.15.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.20.0 // indirect + golang.org/x/tools v0.21.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 1987152a..672d3469 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gofrs/uuid/v5 v5.1.0 h1:S5rqVKIigghZTCBKPCw0Y+bXkn26K3TB5mvQq2Ix8dk= -github.com/gofrs/uuid/v5 v5.1.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM= +github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= @@ -97,31 +97,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/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/gvisor v0.0.0-20240428053021-e691de28565f h1:NkhuupzH5ch7b/Y/6ZHJWrnNLoiNnSJaow6DPb8VW2I= +github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f/go.mod h1:KXmw+ouSJNOsuRpg4wgwwCQuunrGz4yoAqQjsLjc6N0= +github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba h1:EY5AS7CCtfmARNv2zXUOrsEMPFDGYxaw65JzA2p51Vk= +github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= +github.com/sagernet/quic-go v0.43.1-beta.1 h1:alizUjpvWYcz08dBCQsULOd+1xu0o7UtlyYf6SLbRNg= +github.com/sagernet/quic-go v0.43.1-beta.1/go.mod h1:BkrQYeop7Jx3hN3TW8/76CXcdhYiNPyYEBL/BVJ1ifc= 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.20 h1:8rEepj4LMcR0Wd389fJIziv/jr3MBtX5qXBHsfxJ+dY= +github.com/sagernet/sing v0.4.0-beta.20/go.mod h1:PFQKbElc2Pke7faBLv8oEba5ehtKO21Ho+TkYemTI3Y= +github.com/sagernet/sing-dns v0.2.0-beta.18 h1:6vzXZThRdA7YUzBOpSbUT48XRumtl/KIpIHFSOP0za8= +github.com/sagernet/sing-dns v0.2.0-beta.18/go.mod h1:k/dmFcQpg6+m08gC1yQBy+13+QkuLqpKr4bIreq4U24= 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.15 h1:LGWPxQEeg89+68RHP7HtAV0RZeEWQikUqOfE9nYmr2A= -github.com/sagernet/sing-quic v0.1.15/go.mod h1:L+VtzvuPbf8VW8F4R7KiygqpXY4lO7t2wwcQuHjh8Ew= +github.com/sagernet/sing-quic v0.2.0-beta.5 h1:ceKFLd1iS5AtM+pScKmcDp5k7R6WgYIe8vl6nB0aVsE= +github.com/sagernet/sing-quic v0.2.0-beta.5/go.mod h1:lfad61lScAZhAxZ0DHZWvEIcAaT38O6zPTR4vLsHeP0= 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.7 h1:6QtJkeSj6BTTQPGxbbiuV8eh7GdV46w2G0N8CmISwdc= -github.com/sagernet/sing-tun v0.2.7/go.mod h1:MKAAHUzVfj7d9zos4lsz6wjXu86/mJyd/gejiAnWj/w= +github.com/sagernet/sing-tun v0.3.0-beta.6 h1:L11kMrM7UfUW0pzQiU66Fffh4o86KZc1SFGbkYi8Ma8= +github.com/sagernet/sing-tun v0.3.0-beta.6/go.mod h1:DxLIyhjWU/HwGYoX0vNGg2c5QgTQIakphU1MuERR5tQ= 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= @@ -163,16 +163,16 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.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.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -184,19 +184,19 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.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.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= diff --git a/inbound/default_tcp.go b/inbound/default_tcp.go index 69880183..d680c695 100644 --- a/inbound/default_tcp.go +++ b/inbound/default_tcp.go @@ -5,7 +5,9 @@ import ( "net" "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" @@ -16,6 +18,9 @@ func (a *myInboundAdapter) ListenTCP() (net.Listener, error) { bindAddr := M.SocksaddrFrom(a.listenOptions.Listen.Build(), a.listenOptions.ListenPort) var tcpListener net.Listener var listenConfig net.ListenConfig + // TODO: Add an option to customize the keep alive period + listenConfig.KeepAlive = C.TCPKeepAliveInitial + listenConfig.Control = control.Append(listenConfig.Control, control.SetKeepAlivePeriod(C.TCPKeepAliveInitial, C.TCPKeepAliveInterval)) if a.listenOptions.TCPMultiPath { if !go121Available { return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.") diff --git a/inbound/naive.go b/inbound/naive.go index 36bda492..07328c09 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/inbound/tun.go b/inbound/tun.go index c86273d8..e82ea122 100644 --- a/inbound/tun.go +++ b/inbound/tun.go @@ -166,6 +166,14 @@ func (t *Tun) Start() error { } t.logger.Trace("creating stack") t.tunIf = tunInterface + var ( + forwarderBindInterface bool + includeAllNetworks bool + ) + if t.platformInterface != nil { + forwarderBindInterface = true + includeAllNetworks = t.platformInterface.IncludeAllNetworks() + } t.tunStack, err = tun.NewStack(t.stack, tun.StackOptions{ Context: t.ctx, Tun: tunInterface, @@ -174,8 +182,9 @@ func (t *Tun) Start() error { UDPTimeout: t.udpTimeout, Handler: t, Logger: t.logger, - ForwarderBindInterface: t.platformInterface != nil, + ForwarderBindInterface: forwarderBindInterface, InterfaceFinder: t.router.InterfaceFinder(), + IncludeAllNetworks: includeAllNetworks, }) if err != nil { return err diff --git a/include/dhcp_stub.go b/include/dhcp_stub.go index c57aa430..47a19d2e 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 1e507f7b..1bcc0fbc 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 682eb536..ddf9723f 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 00000000..7be1c2da --- /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 00000000..fc30479c --- /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/mkdocs.yml b/mkdocs.yml index 877d73c4..d5218f4d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,8 +66,9 @@ nav: - Proxy Protocol: - Shadowsocks: manual/proxy-protocol/shadowsocks.md - Trojan: manual/proxy-protocol/trojan.md - - TUIC: manual/proxy-protocol/tuic.md - Hysteria 2: manual/proxy-protocol/hysteria2.md + - Misc: + - TunnelVision: manual/misc/tunnelvision.md - Configuration: - configuration/index.md - Log: diff --git a/ntp/service.go b/ntp/service.go deleted file mode 100644 index 70a41c0e..00000000 --- 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 e0d237b7..be947583 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 *AddrPrefix `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 *AddrPrefix `json:"client_subnet,omitempty"` } type DNSFakeIPOptions struct { diff --git a/option/experimental.go b/option/experimental.go index c685f51f..9f6071ba 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -8,10 +8,12 @@ type ExperimentalOptions struct { } type CacheFileOptions struct { - Enabled bool `json:"enabled,omitempty"` - Path string `json:"path,omitempty"` - CacheID string `json:"cache_id,omitempty"` - StoreFakeIP bool `json:"store_fakeip,omitempty"` + Enabled bool `json:"enabled,omitempty"` + Path string `json:"path,omitempty"` + CacheID string `json:"cache_id,omitempty"` + StoreFakeIP bool `json:"store_fakeip,omitempty"` + StoreRDRC bool `json:"store_rdrc,omitempty"` + RDRCTimeout Duration `json:"rdrc_timeout,omitempty"` } type ClashAPIOptions struct { diff --git a/option/ntp.go b/option/ntp.go index 000a658c..0bd2489a 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 443f9314..c5994e1c 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 *AddrPrefix `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 *AddrPrefix `json:"client_subnet,omitempty"` } func (r LogicalDNSRule) IsValid() bool { diff --git a/option/tun_platform.go b/option/tun_platform.go index 873d788a..a0a54eed 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/option/types.go b/option/types.go index aba445ee..83fee8f0 100644 --- a/option/types.go +++ b/option/types.go @@ -51,6 +51,40 @@ func (a *ListenAddress) Build() netip.Addr { return (netip.Addr)(*a) } +type AddrPrefix netip.Prefix + +func (a AddrPrefix) MarshalJSON() ([]byte, error) { + prefix := netip.Prefix(a) + if prefix.Bits() == prefix.Addr().BitLen() { + return json.Marshal(prefix.Addr().String()) + } else { + return json.Marshal(prefix.String()) + } +} + +func (a *AddrPrefix) UnmarshalJSON(content []byte) error { + var value string + err := json.Unmarshal(content, &value) + if err != nil { + return err + } + prefix, prefixErr := netip.ParsePrefix(value) + if prefixErr == nil { + *a = AddrPrefix(prefix) + return nil + } + addr, addrErr := netip.ParseAddr(value) + if addrErr == nil { + *a = AddrPrefix(netip.PrefixFrom(addr, addr.BitLen())) + return nil + } + return prefixErr +} + +func (a AddrPrefix) Build() netip.Prefix { + return netip.Prefix(a) +} + type NetworkList string func (v *NetworkList) UnmarshalJSON(content []byte) error { diff --git a/outbound/direct.go b/outbound/direct.go index 49ac760e..11f650e4 100644 --- a/outbound/direct.go +++ b/outbound/direct.go @@ -51,7 +51,7 @@ func NewDirect(router adapter.Router, logger log.ContextLogger, tag string, opti domainStrategy: dns.DomainStrategy(options.DomainStrategy), fallbackDelay: time.Duration(options.FallbackDelay), dialer: outboundDialer, - loopBack: newLoopBackDetector(), + loopBack: newLoopBackDetector(router), } if options.ProxyProtocol != 0 { return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0") diff --git a/outbound/direct_loopback_detect.go b/outbound/direct_loopback_detect.go index 62cff876..1469b9d0 100644 --- a/outbound/direct_loopback_detect.go +++ b/outbound/direct_loopback_detect.go @@ -5,21 +5,22 @@ import ( "net/netip" "sync" + "github.com/sagernet/sing-box/adapter" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) type loopBackDetector struct { - // router adapter.Router + router adapter.Router connAccess sync.RWMutex packetConnAccess sync.RWMutex connMap map[netip.AddrPort]netip.AddrPort packetConnMap map[uint16]uint16 } -func newLoopBackDetector( /*router adapter.Router*/ ) *loopBackDetector { +func newLoopBackDetector(router adapter.Router) *loopBackDetector { return &loopBackDetector{ - // router: router, + router: router, connMap: make(map[netip.AddrPort]netip.AddrPort), packetConnMap: make(map[uint16]uint16), } @@ -31,12 +32,12 @@ func (l *loopBackDetector) NewConn(conn net.Conn) net.Conn { return conn } if udpConn, isUDPConn := conn.(abstractUDPConn); isUDPConn { - /*if !source.Addr().IsLoopback() { + if !source.Addr().IsLoopback() { _, err := l.router.InterfaceFinder().InterfaceByAddr(source.Addr()) if err != nil { return conn } - }*/ + } if !N.IsPublicAddr(source.Addr()) { return conn } @@ -57,6 +58,12 @@ func (l *loopBackDetector) NewPacketConn(conn N.NetPacketConn, destination M.Soc if !source.IsValid() { return conn } + if !source.Addr().IsLoopback() { + _, err := l.router.InterfaceFinder().InterfaceByAddr(source.Addr()) + if err != nil { + return conn + } + } l.packetConnAccess.Lock() l.packetConnMap[source.Port()] = destination.AddrPort().Port() l.packetConnAccess.Unlock() @@ -74,12 +81,12 @@ func (l *loopBackDetector) CheckPacketConn(source netip.AddrPort, local netip.Ad if !source.IsValid() { return false } - /*if !source.Addr().IsLoopback() { + if !source.Addr().IsLoopback() { _, err := l.router.InterfaceFinder().InterfaceByAddr(source.Addr()) if err != nil { return false } - }*/ + } if N.IsPublicAddr(source.Addr()) { return false } diff --git a/outbound/dns.go b/outbound/dns.go index df32a019..b18b901e 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/interface_finder.go b/route/interface_finder.go deleted file mode 100644 index 850f091f..00000000 --- a/route/interface_finder.go +++ /dev/null @@ -1,54 +0,0 @@ -package route - -import ( - "net" - - "github.com/sagernet/sing/common/control" -) - -var _ control.InterfaceFinder = (*myInterfaceFinder)(nil) - -type myInterfaceFinder struct { - interfaces []net.Interface -} - -func (f *myInterfaceFinder) update() error { - ifs, err := net.Interfaces() - if err != nil { - return err - } - f.interfaces = ifs - return nil -} - -func (f *myInterfaceFinder) updateInterfaces(interfaces []net.Interface) { - f.interfaces = interfaces -} - -func (f *myInterfaceFinder) InterfaceIndexByName(name string) (interfaceIndex int, err error) { - for _, netInterface := range f.interfaces { - if netInterface.Name == name { - return netInterface.Index, nil - } - } - netInterface, err := net.InterfaceByName(name) - if err != nil { - return - } - f.update() - return netInterface.Index, nil -} - -func (f *myInterfaceFinder) InterfaceNameByIndex(index int) (interfaceName string, err error) { - for _, netInterface := range f.interfaces { - if netInterface.Index == index { - return netInterface.Name, nil - } - } - netInterface, err := net.InterfaceByIndex(index) - if err != nil { - return - } - f.update() - return netInterface.Name, nil -} diff --git a/route/router.go b/route/router.go index 1631b4b8..e8950f18 100644 --- a/route/router.go +++ b/route/router.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "os/user" + "runtime" "strings" "time" @@ -22,12 +23,11 @@ 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" "github.com/sagernet/sing-dns" - mux "github.com/sagernet/sing-mux" + "github.com/sagernet/sing-mux" "github.com/sagernet/sing-tun" "github.com/sagernet/sing-vmess" "github.com/sagernet/sing/common" @@ -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" ) @@ -78,13 +79,14 @@ type Router struct { transportDomainStrategy map[dns.Transport]dns.DomainStrategy dnsReverseMapping *DNSReverseMapping fakeIPStore adapter.FakeIPStore - interfaceFinder myInterfaceFinder + interfaceFinder *control.DefaultInterfaceFinder autoDetectInterface bool defaultInterface string defaultMark int networkMonitor tun.NetworkUpdateMonitor interfaceMonitor tun.DefaultInterfaceMonitor packageManager tun.PackageManager + powerListener winpowrprof.EventListener processSearcher process.Searcher timeService *ntp.Service pauseManager pause.Manager @@ -122,6 +124,7 @@ func NewRouter( needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess, defaultDetour: options.Final, defaultDomainStrategy: dns.DomainStrategy(dnsOptions.Strategy), + interfaceFinder: control.NewDefaultInterfaceFinder(), autoDetectInterface: options.AutoDetectInterface, defaultInterface: options.DefaultInterface, defaultMark: options.DefaultMark, @@ -136,7 +139,17 @@ func NewRouter( DisableCache: dnsOptions.DNSClientOptions.DisableCache, DisableExpire: dnsOptions.DNSClientOptions.DisableExpire, IndependentCache: dnsOptions.DNSClientOptions.IndependentCache, - Logger: router.dnsLogger, + RDRC: func() dns.RDRCStore { + cacheFile := service.FromContext[adapter.CacheFile](ctx) + if cacheFile == nil { + return nil + } + if !cacheFile.StoreRDRC() { + return nil + } + return cacheFile + }, + Logger: router.dnsLogger, }) for i, ruleOptions := range options.Rules { routeRule, err := NewRule(router, router.logger, ruleOptions, true) @@ -222,7 +235,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.Prefix + 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 +288,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] } @@ -303,7 +334,7 @@ func NewRouter( } router.networkMonitor = networkMonitor networkMonitor.RegisterCallback(func() { - _ = router.interfaceFinder.update() + _ = router.interfaceFinder.Update() }) interfaceMonitor, err := tun.NewDefaultInterfaceMonitor(router.networkMonitor, router.logger, tun.DefaultInterfaceMonitorOptions{ OverrideAndroidVPN: options.OverrideAndroidVPN, @@ -321,12 +352,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 +607,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 +635,11 @@ func (r *Router) Start() error { return E.Cause(err, "initialize rule[", i, "]") } } + + monitor.Start("initialize DNS client") + r.dnsClient.Start() + monitor.Finish() + for i, rule := range r.dnsRules { monitor.Start("initialize DNS rule[", i, "]") err := rule.Start() @@ -657,6 +719,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 { @@ -1045,24 +1114,18 @@ func (r *Router) match0(ctx context.Context, metadata *adapter.InboundContext, d } func (r *Router) InterfaceFinder() control.InterfaceFinder { - return &r.interfaceFinder + return r.interfaceFinder } func (r *Router) UpdateInterfaces() error { if r.platformInterface == nil || !r.platformInterface.UsePlatformInterfaceGetter() { - return r.interfaceFinder.update() + return r.interfaceFinder.Update() } else { interfaces, err := r.platformInterface.Interfaces() if err != nil { return err } - r.interfaceFinder.updateInterfaces(common.Map(interfaces, func(it platform.NetworkInterface) net.Interface { - return net.Interface{ - Name: it.Name, - Index: it.Index, - MTU: it.MTU, - } - })) + r.interfaceFinder.UpdateInterfaces(interfaces) return nil } } @@ -1206,3 +1269,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 8ae91710..c3383e8b 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 || 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 + 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 + } } - ctx, cancel := context.WithTimeout(ctx, C.DNSTimeout) - defer cancel() - addrs, err := r.dnsClient.Lookup(ctx, transport, domain, strategy) - if len(addrs) > 0 { - r.dnsLogger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(addrs), " ")) - } else if err != nil { - r.dnsLogger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain)) - } else { - r.dnsLogger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result") - err = dns.RCodeNameError + if len(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 9850b5bc..4a99a31c 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 6decb9f3..c13bdd8d 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 d2227bb3..d1d13f7d 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 c43f6290..955526fc 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.Prefix } 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.Prefix)(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.Prefix { + 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.Prefix } func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { @@ -228,6 +284,7 @@ func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options }, disableCache: options.DisableCache, rewriteTTL: options.RewriteTTL, + clientSubnet: (*netip.Prefix)(options.ClientSubnet), } switch options.Mode { case C.LogicalTypeAnd: @@ -254,3 +311,51 @@ func (r *LogicalDNSRule) DisableCache() bool { func (r *LogicalDNSRule) RewriteTTL() *uint32 { return r.rewriteTTL } + +func (r *LogicalDNSRule) ClientSubnet() *netip.Prefix { + 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 82c07d31..67ac3a1e 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 959b2f61..482a9c7b 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 635f22ed..1fd09246 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 595e328c..a14c6fe5 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 1a2c2938..8325c37b 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/packet.go b/transport/fakeip/packet.go deleted file mode 100644 index 620acb92..00000000 --- a/transport/fakeip/packet.go +++ /dev/null @@ -1,55 +0,0 @@ -package fakeip - -import ( - "github.com/sagernet/sing/common/buf" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -var _ N.PacketConn = (*NATPacketConn)(nil) - -type NATPacketConn struct { - N.PacketConn - origin M.Socksaddr - destination M.Socksaddr -} - -func NewNATPacketConn(conn N.PacketConn, origin M.Socksaddr, destination M.Socksaddr) *NATPacketConn { - return &NATPacketConn{ - PacketConn: conn, - origin: socksaddrWithoutPort(origin), - destination: socksaddrWithoutPort(destination), - } -} - -func (c *NATPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { - destination, err = c.PacketConn.ReadPacket(buffer) - if socksaddrWithoutPort(destination) == c.origin { - destination = M.Socksaddr{ - Addr: c.destination.Addr, - Fqdn: c.destination.Fqdn, - Port: destination.Port, - } - } - return -} - -func (c *NATPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { - if socksaddrWithoutPort(destination) == c.destination { - destination = M.Socksaddr{ - Addr: c.origin.Addr, - Fqdn: c.origin.Fqdn, - Port: destination.Port, - } - } - return c.PacketConn.WritePacket(buffer, destination) -} - -func (c *NATPacketConn) Upstream() any { - return c.PacketConn -} - -func socksaddrWithoutPort(destination M.Socksaddr) M.Socksaddr { - destination.Port = 0 - return destination -} diff --git a/transport/fakeip/packet_wait.go b/transport/fakeip/packet_wait.go deleted file mode 100644 index 9fa4a5bd..00000000 --- a/transport/fakeip/packet_wait.go +++ /dev/null @@ -1,37 +0,0 @@ -package fakeip - -import ( - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -func (c *NATPacketConn) CreatePacketReadWaiter() (N.PacketReadWaiter, bool) { - waiter, created := bufio.CreatePacketReadWaiter(c.PacketConn) - if !created { - return nil, false - } - return &waitNATPacketConn{c, waiter}, true -} - -type waitNATPacketConn struct { - *NATPacketConn - readWaiter N.PacketReadWaiter -} - -func (c *waitNATPacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { - return c.readWaiter.InitializeReadWaiter(options) -} - -func (c *waitNATPacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) { - buffer, destination, err = c.readWaiter.WaitReadPacket() - if err == nil && socksaddrWithoutPort(destination) == c.origin { - destination = M.Socksaddr{ - Addr: c.destination.Addr, - Fqdn: c.destination.Fqdn, - Port: destination.Port, - } - } - return -} diff --git a/transport/fakeip/server.go b/transport/fakeip/server.go index 40149aa4..5e0c7eef 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 9d9b4549..7f57b7c7 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 }