From 3f6f879780ed64268c2b9258b26f9682f502b392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 12 May 2024 15:06:21 +0800 Subject: [PATCH] Add custom prefix support in EDNS0 client subnet options --- adapter/router.go | 2 +- docs/configuration/dns/index.md | 4 +++- docs/configuration/dns/index.zh.md | 6 +++-- docs/configuration/dns/rule.md | 8 ++++--- docs/configuration/dns/rule.zh.md | 8 ++++--- docs/configuration/dns/server.md | 4 +++- docs/configuration/dns/server.zh.md | 4 +++- docs/manual/proxy/client.md | 2 +- experimental/cachefile/cache.go | 1 + experimental/cachefile/rdrc.go | 28 +++++++++++++++--------- option/dns.go | 4 ++-- option/rule_dns.go | 16 +++++++------- option/types.go | 34 +++++++++++++++++++++++++++++ route/router.go | 4 ++-- route/rule_dns.go | 12 +++++----- 15 files changed, 96 insertions(+), 41 deletions(-) diff --git a/adapter/router.go b/adapter/router.go index 0d771dee..73849b97 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -86,7 +86,7 @@ type DNSRule interface { Rule DisableCache() bool RewriteTTL() *uint32 - ClientSubnet() *netip.Addr + ClientSubnet() *netip.Prefix WithAddressLimit() bool MatchAddressLimit(metadata *InboundContext) bool } diff --git a/docs/configuration/dns/index.md b/docs/configuration/dns/index.md index 71219dbb..c0eafccc 100644 --- a/docs/configuration/dns/index.md +++ b/docs/configuration/dns/index.md @@ -73,6 +73,8 @@ problematic in environments such as macOS, where DNS is proxied and cached by th !!! question "Since sing-box 1.9.0" -Append a `edns0-subnet` OPT extra record with the specified IP address to every query by default. +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 164c37cd..ba390cef 100644 --- a/docs/configuration/dns/index.zh.md +++ b/docs/configuration/dns/index.zh.md @@ -71,8 +71,10 @@ icon: material/new-box !!! question "自 sing-box 1.9.0 起" -默认情况下,将带有指定 IP 地址的 `edns0-subnet` OPT 附加记录附加到每个查询。 - +默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 + 可以被 `servers.[].client_subnet` 或 `rules.[].client_subnet` 覆盖。 #### fakeip diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 40dce7fd..22b5d872 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -125,7 +125,7 @@ icon: material/new-box "server": "local", "disable_cache": false, "rewrite_ttl": 100, - "client_subnet": "127.0.0.1" + "client_subnet": "127.0.0.1/24" }, { "type": "logical", @@ -134,7 +134,7 @@ icon: material/new-box "server": "local", "disable_cache": false, "rewrite_ttl": 100, - "client_subnet": "127.0.0.1" + "client_subnet": "127.0.0.1/24" } ] } @@ -339,7 +339,9 @@ Rewrite TTL in DNS responses. !!! question "Since sing-box 1.9.0" -Append a `edns0-subnet` OPT extra record with the specified IP address to every query by default. +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`. diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index f27aac9a..9b77bd17 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -124,7 +124,7 @@ icon: material/new-box ], "server": "local", "disable_cache": false, - "client_subnet": "127.0.0.1" + "client_subnet": "127.0.0.1/24" }, { "type": "logical", @@ -132,7 +132,7 @@ icon: material/new-box "rules": [], "server": "local", "disable_cache": false, - "client_subnet": "127.0.0.1" + "client_subnet": "127.0.0.1/24" } ] } @@ -337,7 +337,9 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 !!! question "自 sing-box 1.9.0 起" -默认情况下,将带有指定 IP 地址的 `edns0-subnet` OPT 附加记录附加到每个查询。 +默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 将覆盖 `dns.client_subnet` 与 `servers.[].client_subnet`。 diff --git a/docs/configuration/dns/server.md b/docs/configuration/dns/server.md index e4d93544..3c524581 100644 --- a/docs/configuration/dns/server.md +++ b/docs/configuration/dns/server.md @@ -100,7 +100,9 @@ Default outbound will be used if empty. !!! question "Since sing-box 1.9.0" -Append a `edns0-subnet` OPT extra record with the specified IP address to every query by default. +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`. diff --git a/docs/configuration/dns/server.zh.md b/docs/configuration/dns/server.zh.md index a15fdfd3..baa11751 100644 --- a/docs/configuration/dns/server.zh.md +++ b/docs/configuration/dns/server.zh.md @@ -100,7 +100,9 @@ DNS 服务器的地址。 !!! question "自 sing-box 1.9.0 起" -默认情况下,将带有指定 IP 地址的 `edns0-subnet` OPT 附加记录附加到每个查询。 +默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 可以被 `rules.[].client_subnet` 覆盖。 diff --git a/docs/manual/proxy/client.md b/docs/manual/proxy/client.md index 12a83039..7a65248f 100644 --- a/docs/manual/proxy/client.md +++ b/docs/manual/proxy/client.md @@ -441,7 +441,7 @@ flowchart TB { "rule_set": "geoip-cn", "server": "google", - "client_subnet": "114.114.114.114" // Any China client IP address + "client_subnet": "114.114.114.114/24" // Any China client IP address } ] }, diff --git a/experimental/cachefile/cache.go b/experimental/cachefile/cache.go index 9d45ea8e..1027588f 100644 --- a/experimental/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -57,6 +57,7 @@ type CacheFile struct { type saveRDRCCacheKey struct { TransportName string QuestionName string + QType uint16 } func New(ctx context.Context, options option.CacheFileOptions) *CacheFile { diff --git a/experimental/cachefile/rdrc.go b/experimental/cachefile/rdrc.go index 836beba1..c4800951 100644 --- a/experimental/cachefile/rdrc.go +++ b/experimental/cachefile/rdrc.go @@ -9,7 +9,7 @@ import ( "github.com/sagernet/sing/common/logger" ) -var bucketRDRC = []byte("rdrc") +var bucketRDRC = []byte("rdrc2") func (c *CacheFile) StoreRDRC() bool { return c.storeRDRC @@ -19,13 +19,17 @@ func (c *CacheFile) RDRCTimeout() time.Duration { return c.rdrcTimeout } -func (c *CacheFile) LoadRDRC(transportName string, qName string) (rejected bool) { +func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (rejected bool) { c.saveRDRCAccess.RLock() - rejected, cached := c.saveRDRC[saveRDRCCacheKey{transportName, qName}] + 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) @@ -36,7 +40,7 @@ func (c *CacheFile) LoadRDRC(transportName string, qName string) (rejected bool) if bucket == nil { return nil } - content := bucket.Get([]byte(qName)) + content := bucket.Get(key) if content == nil { return nil } @@ -61,13 +65,13 @@ func (c *CacheFile) LoadRDRC(transportName string, qName string) (rejected bool) if bucket == nil { return nil } - return bucket.Delete([]byte(qName)) + return bucket.Delete(key) }) } return } -func (c *CacheFile) SaveRDRC(transportName string, qName string) error { +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 { @@ -77,20 +81,24 @@ func (c *CacheFile) SaveRDRC(transportName string, qName string) error { 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([]byte(qName), expiresAt) + return bucket.Put(key, expiresAt) }) } -func (c *CacheFile) SaveRDRCAsync(transportName string, qName string, logger logger.Logger) { - saveKey := saveRDRCCacheKey{transportName, qName} +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) + err := c.SaveRDRC(transportName, qName, qType) if err != nil { logger.Warn("save RDRC: ", err) } diff --git a/option/dns.go b/option/dns.go index 15201343..be947583 100644 --- a/option/dns.go +++ b/option/dns.go @@ -19,7 +19,7 @@ type DNSServerOptions struct { AddressFallbackDelay Duration `json:"address_fallback_delay,omitempty"` Strategy DomainStrategy `json:"strategy,omitempty"` Detour string `json:"detour,omitempty"` - ClientSubnet *ListenAddress `json:"client_subnet,omitempty"` + ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"` } type DNSClientOptions struct { @@ -27,7 +27,7 @@ type DNSClientOptions struct { DisableCache bool `json:"disable_cache,omitempty"` DisableExpire bool `json:"disable_expire,omitempty"` IndependentCache bool `json:"independent_cache,omitempty"` - ClientSubnet *ListenAddress `json:"client_subnet,omitempty"` + ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"` } type DNSFakeIPOptions struct { diff --git a/option/rule_dns.go b/option/rule_dns.go index ababea41..c5994e1c 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -101,7 +101,7 @@ type DefaultDNSRule struct { Server string `json:"server,omitempty"` DisableCache bool `json:"disable_cache,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` - ClientSubnet *ListenAddress `json:"client_subnet,omitempty"` + ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"` } func (r DefaultDNSRule) IsValid() bool { @@ -115,13 +115,13 @@ func (r DefaultDNSRule) IsValid() bool { } 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"` - ClientSubnet *ListenAddress `json:"client_subnet,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/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/route/router.go b/route/router.go index e9807bd4..484216ee 100644 --- a/route/router.go +++ b/route/router.go @@ -27,7 +27,7 @@ import ( "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" @@ -235,7 +235,7 @@ func NewRouter( return nil, E.New("parse dns server[", tag, "]: missing address_resolver") } } - var clientSubnet netip.Addr + var clientSubnet netip.Prefix if server.ClientSubnet != nil { clientSubnet = server.ClientSubnet.Build() } else if dnsOptions.ClientSubnet != nil { diff --git a/route/rule_dns.go b/route/rule_dns.go index 7501349f..955526fc 100644 --- a/route/rule_dns.go +++ b/route/rule_dns.go @@ -40,7 +40,7 @@ type DefaultDNSRule struct { abstractDefaultRule disableCache bool rewriteTTL *uint32 - clientSubnet *netip.Addr + clientSubnet *netip.Prefix } func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) { @@ -51,7 +51,7 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options }, disableCache: options.DisableCache, rewriteTTL: options.RewriteTTL, - clientSubnet: (*netip.Addr)(options.ClientSubnet), + clientSubnet: (*netip.Prefix)(options.ClientSubnet), } if len(options.Inbound) > 0 { item := NewInboundRule(options.Inbound) @@ -234,7 +234,7 @@ func (r *DefaultDNSRule) RewriteTTL() *uint32 { return r.rewriteTTL } -func (r *DefaultDNSRule) ClientSubnet() *netip.Addr { +func (r *DefaultDNSRule) ClientSubnet() *netip.Prefix { return r.clientSubnet } @@ -272,7 +272,7 @@ type LogicalDNSRule struct { abstractLogicalRule disableCache bool rewriteTTL *uint32 - clientSubnet *netip.Addr + clientSubnet *netip.Prefix } func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { @@ -284,7 +284,7 @@ func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options }, disableCache: options.DisableCache, rewriteTTL: options.RewriteTTL, - clientSubnet: (*netip.Addr)(options.ClientSubnet), + clientSubnet: (*netip.Prefix)(options.ClientSubnet), } switch options.Mode { case C.LogicalTypeAnd: @@ -312,7 +312,7 @@ func (r *LogicalDNSRule) RewriteTTL() *uint32 { return r.rewriteTTL } -func (r *LogicalDNSRule) ClientSubnet() *netip.Addr { +func (r *LogicalDNSRule) ClientSubnet() *netip.Prefix { return r.clientSubnet }