diff --git a/adapter/experimental.go b/adapter/experimental.go index d290a8f2..17f2379a 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -13,6 +13,7 @@ type ClashServer interface { PreStarter Mode() string StoreSelected() bool + StoreFakeIP() bool CacheFile() ClashCacheFile HistoryStorage() *urltest.HistoryStorage RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker) @@ -22,6 +23,7 @@ type ClashServer interface { type ClashCacheFile interface { LoadSelected(group string) string StoreSelected(group string, selected string) error + FakeIPStorage } type Tracker interface { diff --git a/adapter/fakeip.go b/adapter/fakeip.go new file mode 100644 index 00000000..6153f8ce --- /dev/null +++ b/adapter/fakeip.go @@ -0,0 +1,23 @@ +package adapter + +import ( + "net/netip" + + "github.com/sagernet/sing-dns" +) + +type FakeIPStore interface { + Service + Contains(address netip.Addr) bool + Create(domain string, strategy dns.DomainStrategy) (netip.Addr, error) + Lookup(address netip.Addr) (string, bool) + Reset() error +} + +type FakeIPStorage interface { + FakeIPMetadata() *FakeIPMetadata + FakeIPSaveMetadata(metadata *FakeIPMetadata) error + FakeIPStore(address netip.Addr, domain string) error + FakeIPLoad(address netip.Addr) (string, bool) + FakeIPReset() error +} diff --git a/adapter/fakeip_metadata.go b/adapter/fakeip_metadata.go new file mode 100644 index 00000000..7df77d42 --- /dev/null +++ b/adapter/fakeip_metadata.go @@ -0,0 +1,50 @@ +package adapter + +import ( + "bytes" + "encoding" + "encoding/binary" + "io" + "net/netip" + + "github.com/sagernet/sing/common" +) + +type FakeIPMetadata struct { + Inet4Range netip.Prefix + Inet6Range netip.Prefix + Inet4Current netip.Addr + Inet6Current netip.Addr +} + +func (m *FakeIPMetadata) MarshalBinary() (data []byte, err error) { + var buffer bytes.Buffer + for _, marshaler := range []encoding.BinaryMarshaler{m.Inet4Range, m.Inet6Range, m.Inet4Current, m.Inet6Current} { + data, err = marshaler.MarshalBinary() + if err != nil { + return + } + common.Must(binary.Write(&buffer, binary.BigEndian, uint16(len(data)))) + buffer.Write(data) + } + data = buffer.Bytes() + return +} + +func (m *FakeIPMetadata) UnmarshalBinary(data []byte) error { + reader := bytes.NewReader(data) + for _, unmarshaler := range []encoding.BinaryUnmarshaler{&m.Inet4Range, &m.Inet6Range, &m.Inet4Current, &m.Inet6Current} { + var length uint16 + common.Must(binary.Read(reader, binary.BigEndian, &length)) + element := make([]byte, length) + _, err := io.ReadFull(reader, element) + if err != nil { + return err + } + err = unmarshaler.UnmarshalBinary(element) + if err != nil { + return err + } + } + return nil +} diff --git a/adapter/router.go b/adapter/router.go index b9eace99..29b157f0 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -21,6 +21,8 @@ type Router interface { Outbound(tag string) (Outbound, bool) DefaultOutbound(network string) Outbound + FakeIPStore() FakeIPStore + RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error diff --git a/docs/configuration/dns/fakeip.md b/docs/configuration/dns/fakeip.md new file mode 100644 index 00000000..51db1f42 --- /dev/null +++ b/docs/configuration/dns/fakeip.md @@ -0,0 +1,25 @@ +# FakeIP + +### Structure + +```json +{ + "enabled": true, + "inet4_range": "198.18.0.0/15", + "inet6_range": "fc00::/18" +} +``` + +### Fields + +#### enabled + +Enable FakeIP service. + +#### inet4_range + +IPv4 address range for FakeIP. + +#### inet6_address + +IPv6 address range for FakeIP. diff --git a/docs/configuration/dns/fakeip.zh.md b/docs/configuration/dns/fakeip.zh.md new file mode 100644 index 00000000..3d9a814a --- /dev/null +++ b/docs/configuration/dns/fakeip.zh.md @@ -0,0 +1,25 @@ +# FakeIP + +### 结构 + +```json +{ + "enabled": true, + "inet4_range": "198.18.0.0/15", + "inet6_range": "fc00::/18" +} +``` + +### 字段 + +#### enabled + +启用 FakeIP 服务。 + +#### inet4_range + +用于 FakeIP 的 IPv4 地址范围。 + +#### inet6_range + +用于 FakeIP 的 IPv6 地址范围。 diff --git a/docs/configuration/dns/index.md b/docs/configuration/dns/index.md index 4f85942f..40b26473 100644 --- a/docs/configuration/dns/index.md +++ b/docs/configuration/dns/index.md @@ -11,7 +11,8 @@ "strategy": "", "disable_cache": false, "disable_expire": false, - "reverse_mapping": false + "reverse_mapping": false, + "fakeip": {} } } @@ -23,6 +24,7 @@ |----------|--------------------------------| | `server` | List of [DNS Server](./server) | | `rules` | List of [DNS Rule](./rule) | +| `fakeip` | [FakeIP](./fakeip) | #### final @@ -50,4 +52,9 @@ Disable dns cache expire. Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing. -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. +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 + +[FakeIP](./fakeip) settings. diff --git a/docs/configuration/dns/index.zh.md b/docs/configuration/dns/index.zh.md index c721ed52..034d8dd3 100644 --- a/docs/configuration/dns/index.zh.md +++ b/docs/configuration/dns/index.zh.md @@ -11,7 +11,8 @@ "strategy": "", "disable_cache": false, "disable_expire": false, - "reverse_mapping": false + "reverse_mapping": false, + "fakeip": {} } } @@ -51,3 +52,7 @@ 在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。 由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。 + +#### fakeip + +[FakeIP](./fakeip) 设置。 diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 256408b2..f7c46db3 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -84,14 +84,16 @@ "direct" ], "server": "local", - "disable_cache": false + "disable_cache": false, + "rewrite_ttl": 100 }, { "type": "logical", "mode": "and", "rules": [], "server": "local", - "disable_cache": false + "disable_cache": false, + "rewrite_ttl": 100 } ] } @@ -244,6 +246,10 @@ Tag of the target dns server. Disable cache and save cache in this query. +#### rewrite_ttl + +Rewrite TTL in DNS responses. + ### Logical Fields #### type diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 45e1e9c3..9af71a40 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -243,6 +243,10 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 在此查询中禁用缓存。 +#### rewrite_ttl + +重写 DNS 回应中的 TTL。 + ### 逻辑字段 #### type diff --git a/docs/configuration/dns/server.md b/docs/configuration/dns/server.md index 7ffe1b06..93d6bdd7 100644 --- a/docs/configuration/dns/server.md +++ b/docs/configuration/dns/server.md @@ -30,17 +30,18 @@ The tag of the dns server. The address of the dns server. -| Protocol | Format | -|----------|-------------------------------| -| `System` | `local` | -| `TCP` | `tcp://1.0.0.1` | -| `UDP` | `8.8.8.8` `udp://8.8.4.4` | -| `TLS` | `tls://dns.google` | -| `HTTPS` | `https://1.1.1.1/dns-query` | -| `QUIC` | `quic://dns.adguard.com` | -| `HTTP3` | `h3://8.8.8.8/dns-query` | -| `RCode` | `rcode://refused` | -| `DHCP` | `dhcp://auto` or `dhcp://en0` | +| Protocol | Format | +|---------------------|-------------------------------| +| `System` | `local` | +| `TCP` | `tcp://1.0.0.1` | +| `UDP` | `8.8.8.8` `udp://8.8.4.4` | +| `TLS` | `tls://dns.google` | +| `HTTPS` | `https://1.1.1.1/dns-query` | +| `QUIC` | `quic://dns.adguard.com` | +| `HTTP3` | `h3://8.8.8.8/dns-query` | +| `RCode` | `rcode://refused` | +| `DHCP` | `dhcp://auto` or `dhcp://en0` | +| [FakeIP](./fakeip) | `fakeip` | !!! warning "" diff --git a/docs/configuration/dns/server.zh.md b/docs/configuration/dns/server.zh.md index e62e74dd..585da0b4 100644 --- a/docs/configuration/dns/server.zh.md +++ b/docs/configuration/dns/server.zh.md @@ -30,17 +30,18 @@ DNS 服务器的标签。 DNS 服务器的地址。 -| 协议 | 格式 | -|----------|------------------------------| -| `System` | `local` | -| `TCP` | `tcp://1.0.0.1` | -| `UDP` | `8.8.8.8` `udp://8.8.4.4` | -| `TLS` | `tls://dns.google` | -| `HTTPS` | `https://1.1.1.1/dns-query` | -| `QUIC` | `quic://dns.adguard.com` | -| `HTTP3` | `h3://8.8.8.8/dns-query` | -| `RCode` | `rcode://refused` | -| `DHCP` | `dhcp://auto` 或 `dhcp://en0` | +| 协议 | 格式 | +|--------------------|------------------------------| +| `System` | `local` | +| `TCP` | `tcp://1.0.0.1` | +| `UDP` | `8.8.8.8` `udp://8.8.4.4` | +| `TLS` | `tls://dns.google` | +| `HTTPS` | `https://1.1.1.1/dns-query` | +| `QUIC` | `quic://dns.adguard.com` | +| `HTTP3` | `h3://8.8.8.8/dns-query` | +| `RCode` | `rcode://refused` | +| `DHCP` | `dhcp://auto` 或 `dhcp://en0` | +| [FakeIP](./fakeip) | `fakeip` | !!! warning "" diff --git a/docs/faq/fakeip.md b/docs/faq/fakeip.md new file mode 100644 index 00000000..89fcd0c9 --- /dev/null +++ b/docs/faq/fakeip.md @@ -0,0 +1,18 @@ +# FakeIP + +FakeIP refers to a type of behavior in a program that simultaneously hijacks both DNS and connection requests. It +responds to DNS requests with virtual results and restores mapping when accepting connections. + +#### Advantage + +* + +#### Limitation + +* Its mechanism breaks applications that depend on returning correct remote addresses. +* Only A and AAAA (IP) requests are supported, which may break applications that rely on other requests. + +#### Recommendation + +* If using tun, make sure FakeIP ranges is included in the tun's routes. +* Enable `experimental.clash_api.store_fakeip` to persist FakeIP records, or use `dns.rules.rewrite_ttl` to avoid losing records after program restart in DNS cached environments. diff --git a/docs/faq/fakeip.zh.md b/docs/faq/fakeip.zh.md new file mode 100644 index 00000000..4a323fb7 --- /dev/null +++ b/docs/faq/fakeip.zh.md @@ -0,0 +1,17 @@ +# FakeIP + +FakeIP 是指同时劫持 DNS 和连接请求的程序中的一种行为。它通过虚拟结果响应 DNS 请求,在接受连接时恢复映射。 + +#### 优点 + +* + +#### 限制 + +* 它的机制会破坏依赖于返回正确远程地址的应用程序。 +* 仅支持 A 和 AAAA(IP)请求,这可能会破坏依赖于其他请求的应用程序。 + +#### 建议 + +* 如果使用 tun,请确保 tun 路由中包含 FakeIP 地址范围。 +* 启用 `experimental.clash_api.store_fakeip` 以持久化 FakeIP 记录,或者使用 `dns.rules.rewrite_ttl` 避免程序重启后在 DNS 被缓存的环境中丢失记录。 diff --git a/docs/faq/index.md b/docs/faq/index.md index 5b75b5ac..c7ce30ad 100644 --- a/docs/faq/index.md +++ b/docs/faq/index.md @@ -11,12 +11,6 @@ it doesn't fit, because it compromises performance or design clarity, or because If it bothers you that sing-box is missing feature X, please forgive us and investigate the features that sing-box does have. You might find that they compensate in interesting ways for the lack of X. -#### Fake IP - -Fake IP (also called Fake DNS) is an invasive and imperfect DNS solution that breaks expected behavior, causes DNS leaks -and makes many software unusable. It is recommended by some software that lacks DNS processing and caching, but sing-box -does not need this. - #### Naive outbound NaïveProxy's main function is chromium's network stack, and it makes no sense to implement only its transport protocol. diff --git a/docs/faq/index.zh.md b/docs/faq/index.zh.md index 9f8a45ac..557c53c7 100644 --- a/docs/faq/index.zh.md +++ b/docs/faq/index.zh.md @@ -9,11 +9,6 @@ 如果 sing-box 缺少功能 X 让您感到困扰,请原谅我们并调查 sing-box 确实有的功能。 您可能会发现它们以有趣的方式弥补了 X 的缺失。 -#### Fake IP - -Fake IP(也称 Fake DNS)是一种侵入性和不完善的 DNS 解决方案,它打破了预期的行为,导致 DNS 泄漏并使许多软件无法使用。 -一些缺乏 DNS 处理和缓存的软件推荐使用它,但 sing-box 不需要。 - #### Naive 出站 NaïveProxy 的主要功能是 chromium 的网络栈,仅实现它的传输协议是舍本逐末的。 diff --git a/experimental/clashapi/cache.go b/experimental/clashapi/cache.go index a9b75bc6..7582fde5 100644 --- a/experimental/clashapi/cache.go +++ b/experimental/clashapi/cache.go @@ -3,21 +3,28 @@ package clashapi import ( "net/http" + "github.com/sagernet/sing-box/adapter" + "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) -func cacheRouter() http.Handler { +func cacheRouter(router adapter.Router) http.Handler { r := chi.NewRouter() - r.Post("/fakeip/flush", flushFakeip) + r.Post("/fakeip/flush", flushFakeip(router)) return r } -func flushFakeip(w http.ResponseWriter, r *http.Request) { - /*if err := cachefile.Cache().FlushFakeip(); err != nil { - render.Status(r, http.StatusInternalServerError) - render.JSON(w, r, newError(err.Error())) - return - }*/ - render.NoContent(w, r) +func flushFakeip(router adapter.Router) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if cacheFile := router.ClashServer().CacheFile(); cacheFile != nil { + err := cacheFile.FakeIPReset() + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, newError(err.Error())) + return + } + } + render.NoContent(w, r) + } } diff --git a/experimental/clashapi/cachefile/fakeip.go b/experimental/clashapi/cachefile/fakeip.go new file mode 100644 index 00000000..2c096556 --- /dev/null +++ b/experimental/clashapi/cachefile/fakeip.go @@ -0,0 +1,77 @@ +package cachefile + +import ( + "net/netip" + "os" + + "github.com/sagernet/sing-box/adapter" + + "go.etcd.io/bbolt" +) + +var ( + bucketFakeIP = []byte("fakeip") + keyMetadata = []byte("metadata") +) + +func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata { + var metadata adapter.FakeIPMetadata + err := c.DB.View(func(tx *bbolt.Tx) error { + bucket := tx.Bucket(bucketFakeIP) + if bucket == nil { + return nil + } + metadataBinary := bucket.Get(keyMetadata) + if len(metadataBinary) == 0 { + return os.ErrInvalid + } + return metadata.UnmarshalBinary(metadataBinary) + }) + if err != nil { + return nil + } + return &metadata +} + +func (c *CacheFile) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) error { + return c.DB.Batch(func(tx *bbolt.Tx) error { + bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP) + if err != nil { + return err + } + metadataBinary, err := metadata.MarshalBinary() + if err != nil { + return err + } + return bucket.Put(keyMetadata, metadataBinary) + }) +} + +func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error { + return c.DB.Batch(func(tx *bbolt.Tx) error { + bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP) + if err != nil { + return err + } + return bucket.Put(address.AsSlice(), []byte(domain)) + }) +} + +func (c *CacheFile) FakeIPLoad(address netip.Addr) (string, bool) { + var domain string + _ = c.DB.View(func(tx *bbolt.Tx) error { + bucket := tx.Bucket(bucketFakeIP) + if bucket == nil { + return nil + } + domain = string(bucket.Get(address.AsSlice())) + return nil + }) + return domain, domain != "" +} + +func (c *CacheFile) FakeIPReset() error { + return c.DB.Batch(func(tx *bbolt.Tx) error { + return tx.DeleteBucket(bucketFakeIP) + }) +} diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 981de8c4..9d377280 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -44,6 +44,7 @@ type Server struct { urlTestHistory *urltest.HistoryStorage mode string storeSelected bool + storeFakeIP bool cacheFilePath string cacheFile adapter.ClashCacheFile } @@ -61,12 +62,13 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options trafficManager: trafficManager, urlTestHistory: urltest.NewHistoryStorage(), mode: strings.ToLower(options.DefaultMode), + storeSelected: options.StoreSelected, + storeFakeIP: options.StoreFakeIP, } if server.mode == "" { server.mode = "rule" } - if options.StoreSelected { - server.storeSelected = true + if options.StoreSelected || options.StoreFakeIP { cachePath := os.ExpandEnv(options.CacheFile) if cachePath == "" { cachePath = "cache.db" @@ -99,7 +101,7 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/script", scriptRouter()) r.Mount("/profile", profileRouter()) - r.Mount("/cache", cacheRouter()) + r.Mount("/cache", cacheRouter(router)) r.Mount("/dns", dnsRouter(router)) }) if options.ExternalUI != "" { @@ -156,6 +158,10 @@ func (s *Server) StoreSelected() bool { return s.storeSelected } +func (s *Server) StoreFakeIP() bool { + return s.storeFakeIP +} + func (s *Server) CacheFile() adapter.ClashCacheFile { return s.cacheFile } diff --git a/mkdocs.yml b/mkdocs.yml index f686a7e3..af0a15ad 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -48,6 +48,7 @@ nav: - configuration/dns/index.md - DNS Server: configuration/dns/server.md - DNS Rule: configuration/dns/rule.md + - FakeIP: configuration/dns/fakeip.md - NTP: - configuration/ntp/index.md - Route: @@ -102,6 +103,7 @@ nav: - URLTest: configuration/outbound/urltest.md - FAQ: - faq/index.md + - FakeIP: faq/fakeip.md - Known Issues: faq/known-issues.md - Examples: - examples/index.md @@ -111,6 +113,7 @@ nav: - Shadowsocks: examples/shadowsocks.md - ShadowTLS: examples/shadowtls.md - Clash API: examples/clash-api.md + - WireGuard Direct: examples/wireguard-direct.md - Contributing: - contributing/index.md - Developing: diff --git a/option/clash.go b/option/clash.go index 6d30bb3d..7f040ac0 100644 --- a/option/clash.go +++ b/option/clash.go @@ -6,6 +6,7 @@ type ClashAPIOptions struct { Secret string `json:"secret,omitempty"` DefaultMode string `json:"default_mode,omitempty"` StoreSelected bool `json:"store_selected,omitempty"` + StoreFakeIP bool `json:"store_fakeip,omitempty"` CacheFile string `json:"cache_file,omitempty"` } diff --git a/option/dns.go b/option/dns.go index df8e7e70..d700252c 100644 --- a/option/dns.go +++ b/option/dns.go @@ -5,15 +5,10 @@ type DNSOptions struct { Rules []DNSRule `json:"rules,omitempty"` Final string `json:"final,omitempty"` ReverseMapping bool `json:"reverse_mapping,omitempty"` + FakeIP *DNSFakeIPOptions `json:"fakeip,omitempty"` DNSClientOptions } -type DNSClientOptions struct { - Strategy DomainStrategy `json:"strategy,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - DisableExpire bool `json:"disable_expire,omitempty"` -} - type DNSServerOptions struct { Tag string `json:"tag,omitempty"` Address string `json:"address"` @@ -23,3 +18,15 @@ type DNSServerOptions struct { Strategy DomainStrategy `json:"strategy,omitempty"` Detour string `json:"detour,omitempty"` } + +type DNSClientOptions struct { + Strategy DomainStrategy `json:"strategy,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + DisableExpire bool `json:"disable_expire,omitempty"` +} + +type DNSFakeIPOptions struct { + Enabled bool `json:"enabled,omitempty"` + Inet4Range *ListenPrefix `json:"inet4_range,omitempty"` + Inet6Range *ListenPrefix `json:"inet6_range,omitempty"` +} diff --git a/route/router.go b/route/router.go index 515d2402..795e06dc 100644 --- a/route/router.go +++ b/route/router.go @@ -24,6 +24,7 @@ import ( "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" "github.com/sagernet/sing-tun" "github.com/sagernet/sing-vmess" @@ -66,6 +67,7 @@ type Router struct { transportMap map[string]dns.Transport transportDomainStrategy map[dns.Transport]dns.DomainStrategy dnsReverseMapping *DNSReverseMapping + fakeIPStore adapter.FakeIPStore interfaceFinder myInterfaceFinder autoDetectInterface bool defaultInterface string @@ -178,12 +180,8 @@ func NewRouter( } else { continue } - } else if notIpAddress != nil { - switch serverURL.Scheme { - case "rcode", "dhcp": - default: - return nil, E.New("parse dns server[", tag, "]: missing address_resolver") - } + } else if notIpAddress != nil && strings.Contains(server.Address, ".") { + 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) @@ -239,6 +237,18 @@ func NewRouter( router.dnsReverseMapping = NewDNSReverseMapping() } + if fakeIPOptions := dnsOptions.FakeIP; fakeIPOptions != nil && dnsOptions.FakeIP.Enabled { + var inet4Range netip.Prefix + var inet6Range netip.Prefix + if fakeIPOptions.Inet4Range != nil { + inet4Range = fakeIPOptions.Inet4Range.Build() + } + if fakeIPOptions.Inet6Range != nil { + inet6Range = fakeIPOptions.Inet6Range.Build() + } + router.fakeIPStore = fakeip.NewStore(router, inet4Range, inet6Range) + } + usePlatformDefaultInterfaceMonitor := platformInterface != nil && platformInterface.UsePlatformDefaultInterfaceMonitor() needInterfaceMonitor := options.AutoDetectInterface || common.Any(inbounds, func(inbound option.Inbound) bool { return inbound.HTTPOptions.SetSystemProxy || inbound.MixedOptions.SetSystemProxy || inbound.TunOptions.AutoRoute @@ -452,6 +462,12 @@ func (r *Router) Start() error { return E.Cause(err, "initialize DNS rule[", i, "]") } } + if r.fakeIPStore != nil { + err := r.fakeIPStore.Start() + if err != nil { + return err + } + } for i, transport := range r.transports { err := transport.Start() if err != nil { @@ -517,6 +533,12 @@ func (r *Router) Close() error { return E.Cause(err, "close time service") }) } + if r.fakeIPStore != nil { + r.logger.Trace("closing fakeip store") + err = E.Append(err, r.fakeIPStore.Close(), func(err error) error { + return E.Cause(err, "close fakeip store") + }) + } return err } @@ -533,6 +555,10 @@ func (r *Router) DefaultOutbound(network string) adapter.Outbound { } } +func (r *Router) FakeIPStore() adapter.FakeIPStore { + return r.fakeIPStore +} + func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if metadata.InboundDetour != "" { if metadata.LastInbound == metadata.InboundDetour { @@ -585,6 +611,19 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()} return r.RoutePacketConnection(ctx, uot.NewConn(conn, uot.Request{}), metadata) } + + if r.fakeIPStore != nil && r.fakeIPStore.Contains(metadata.Destination.Addr) { + domain, loaded := r.fakeIPStore.Lookup(metadata.Destination.Addr) + if !loaded { + return E.New("missing fakeip context") + } + metadata.Destination = M.Socksaddr{ + Fqdn: domain, + Port: metadata.Destination.Port, + } + r.logger.DebugContext(ctx, "found fakeip domain: ", domain) + } + if metadata.InboundOptions.SniffEnabled { buffer := buf.NewPacket() buffer.FullReset() @@ -675,6 +714,21 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m return nil } metadata.Network = N.NetworkUDP + + var originAddress M.Socksaddr + if r.fakeIPStore != nil && r.fakeIPStore.Contains(metadata.Destination.Addr) { + domain, loaded := r.fakeIPStore.Lookup(metadata.Destination.Addr) + if !loaded { + return E.New("missing fakeip context") + } + originAddress = metadata.Destination + metadata.Destination = M.Socksaddr{ + Fqdn: domain, + Port: metadata.Destination.Port, + } + r.logger.DebugContext(ctx, "found fakeip domain: ", domain) + } + if metadata.InboundOptions.SniffEnabled { buffer := buf.NewPacket() buffer.FullReset() @@ -733,6 +787,9 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m conn = statsService.RoutedPacketConnection(metadata.Inbound, detour.Tag(), metadata.User, conn) } } + if originAddress.IsValid() { + conn = fakeip.NewNATPacketConn(conn, originAddress, metadata.Destination) + } return detour.NewPacketConnection(ctx, conn, metadata) } diff --git a/route/rule_abstract.go b/route/rule_abstract.go index be41401f..38d4d57d 100644 --- a/route/rule_abstract.go +++ b/route/rule_abstract.go @@ -57,6 +57,10 @@ func (r *abstractDefaultRule) UpdateGeosite() error { } func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool { + if len(r.allItems) == 0 { + return true + } + for _, item := range r.items { if !item.Match(metadata) { return r.invert diff --git a/transport/fakeip/memory.go b/transport/fakeip/memory.go new file mode 100644 index 00000000..61674f4c --- /dev/null +++ b/transport/fakeip/memory.go @@ -0,0 +1,44 @@ +package fakeip + +import ( + "net/netip" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common/cache" +) + +var _ adapter.FakeIPStorage = (*MemoryStorage)(nil) + +type MemoryStorage struct { + metadata *adapter.FakeIPMetadata + domainCache *cache.LruCache[netip.Addr, string] +} + +func NewMemoryStorage() *MemoryStorage { + return &MemoryStorage{ + domainCache: cache.New[netip.Addr, string](), + } +} + +func (s *MemoryStorage) FakeIPMetadata() *adapter.FakeIPMetadata { + return s.metadata +} + +func (s *MemoryStorage) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) error { + s.metadata = metadata + return nil +} + +func (s *MemoryStorage) FakeIPStore(address netip.Addr, domain string) error { + s.domainCache.Store(address, domain) + return nil +} + +func (s *MemoryStorage) FakeIPLoad(address netip.Addr) (string, bool) { + return s.domainCache.Load(address) +} + +func (s *MemoryStorage) FakeIPReset() error { + s.domainCache = cache.New[netip.Addr, string]() + return nil +} diff --git a/transport/fakeip/packet.go b/transport/fakeip/packet.go new file mode 100644 index 00000000..620acb92 --- /dev/null +++ b/transport/fakeip/packet.go @@ -0,0 +1,55 @@ +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/server.go b/transport/fakeip/server.go new file mode 100644 index 00000000..058a2192 --- /dev/null +++ b/transport/fakeip/server.go @@ -0,0 +1,83 @@ +package fakeip + +import ( + "context" + "net/netip" + "os" + + "github.com/sagernet/sing-box/adapter" + "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" +) + +var _ dns.Transport = (*Server)(nil) + +func init() { + dns.RegisterTransport([]string{"fakeip"}, NewTransport) +} + +type Server struct { + name string + router adapter.Router + store adapter.FakeIPStore + 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) + if router == nil { + return nil, E.New("missing router in context") + } + return &Server{ + name: name, + router: router, + logger: logger, + }, nil +} + +func (s *Server) Name() string { + return s.name +} + +func (s *Server) Start() error { + s.store = s.router.FakeIPStore() + if s.store == nil { + return E.New("fakeip not enabled") + } + return nil +} + +func (s *Server) Close() error { + return nil +} + +func (s *Server) Raw() bool { + return false +} + +func (s *Server) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + return nil, os.ErrInvalid +} + +func (s *Server) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) { + var addresses []netip.Addr + if strategy != dns.DomainStrategyUseIPv6 { + inet4Address, err := s.store.Create(domain, dns.DomainStrategyUseIPv4) + if err != nil { + return nil, err + } + addresses = append(addresses, inet4Address) + } + if strategy != dns.DomainStrategyUseIPv4 { + inet6Address, err := s.store.Create(domain, dns.DomainStrategyUseIPv6) + if err != nil { + return nil, err + } + addresses = append(addresses, inet6Address) + } + return addresses, nil +} diff --git a/transport/fakeip/store.go b/transport/fakeip/store.go new file mode 100644 index 00000000..b2f4c898 --- /dev/null +++ b/transport/fakeip/store.go @@ -0,0 +1,108 @@ +package fakeip + +import ( + "net/netip" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-dns" + E "github.com/sagernet/sing/common/exceptions" +) + +var _ adapter.FakeIPStore = (*Store)(nil) + +type Store struct { + router adapter.Router + inet4Range netip.Prefix + inet6Range netip.Prefix + storage adapter.FakeIPStorage + inet4Current netip.Addr + inet6Current netip.Addr +} + +func NewStore(router adapter.Router, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store { + return &Store{ + router: router, + inet4Range: inet4Range, + inet6Range: inet6Range, + } +} + +func (s *Store) Start() error { + var storage adapter.FakeIPStorage + if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreFakeIP() { + if cacheFile := clashServer.CacheFile(); cacheFile != nil { + storage = cacheFile + } + } + if storage == nil { + storage = NewMemoryStorage() + } + metadata := storage.FakeIPMetadata() + if metadata != nil && metadata.Inet4Range == s.inet4Range && metadata.Inet6Range == s.inet6Range { + s.inet4Current = metadata.Inet4Current + s.inet6Current = metadata.Inet6Current + } else { + if s.inet4Range.IsValid() { + s.inet4Current = s.inet4Range.Addr().Next().Next() + } + if s.inet6Range.IsValid() { + s.inet6Current = s.inet6Range.Addr().Next().Next() + } + } + s.storage = storage + return nil +} + +func (s *Store) Contains(address netip.Addr) bool { + return s.inet4Range.Contains(address) || s.inet6Range.Contains(address) +} + +func (s *Store) Close() error { + if s.storage == nil { + return nil + } + return s.storage.FakeIPSaveMetadata(&adapter.FakeIPMetadata{ + Inet4Range: s.inet4Range, + Inet6Range: s.inet6Range, + Inet4Current: s.inet4Current, + Inet6Current: s.inet6Current, + }) +} + +func (s *Store) Create(domain string, strategy dns.DomainStrategy) (netip.Addr, error) { + var address netip.Addr + if strategy == dns.DomainStrategyUseIPv4 { + if !s.inet4Current.IsValid() { + return netip.Addr{}, E.New("missing IPv4 fakeip address range") + } + nextAddress := s.inet4Current.Next() + if !s.inet4Range.Contains(nextAddress) { + nextAddress = s.inet4Range.Addr().Next().Next() + } + s.inet4Current = nextAddress + address = nextAddress + } else { + if !s.inet6Current.IsValid() { + return netip.Addr{}, E.New("missing IPv6 fakeip address range") + } + nextAddress := s.inet6Current.Next() + if !s.inet6Range.Contains(nextAddress) { + nextAddress = s.inet6Range.Addr().Next().Next() + } + s.inet6Current = nextAddress + address = nextAddress + } + err := s.storage.FakeIPStore(address, domain) + if err != nil { + return netip.Addr{}, err + } + return address, nil +} + +func (s *Store) Lookup(address netip.Addr) (string, bool) { + return s.storage.FakeIPLoad(address) +} + +func (s *Store) Reset() error { + return s.storage.FakeIPReset() +}