Browse Source

Add fakeip support

世界 2 years ago
parent
commit
9bca5a517f

+ 2 - 0
adapter/experimental.go

@@ -13,6 +13,7 @@ type ClashServer interface {
 	PreStarter
 	PreStarter
 	Mode() string
 	Mode() string
 	StoreSelected() bool
 	StoreSelected() bool
+	StoreFakeIP() bool
 	CacheFile() ClashCacheFile
 	CacheFile() ClashCacheFile
 	HistoryStorage() *urltest.HistoryStorage
 	HistoryStorage() *urltest.HistoryStorage
 	RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
 	RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
@@ -22,6 +23,7 @@ type ClashServer interface {
 type ClashCacheFile interface {
 type ClashCacheFile interface {
 	LoadSelected(group string) string
 	LoadSelected(group string) string
 	StoreSelected(group string, selected string) error
 	StoreSelected(group string, selected string) error
+	FakeIPStorage
 }
 }
 
 
 type Tracker interface {
 type Tracker interface {

+ 23 - 0
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
+}

+ 50 - 0
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
+}

+ 2 - 0
adapter/router.go

@@ -21,6 +21,8 @@ type Router interface {
 	Outbound(tag string) (Outbound, bool)
 	Outbound(tag string) (Outbound, bool)
 	DefaultOutbound(network string) Outbound
 	DefaultOutbound(network string) Outbound
 
 
+	FakeIPStore() FakeIPStore
+
 	RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
 	RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
 	RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
 	RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
 
 

+ 25 - 0
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.

+ 25 - 0
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 地址范围。

+ 9 - 2
docs/configuration/dns/index.md

@@ -11,7 +11,8 @@
     "strategy": "",
     "strategy": "",
     "disable_cache": false,
     "disable_cache": false,
     "disable_expire": false,
     "disable_expire": false,
-    "reverse_mapping": false
+    "reverse_mapping": false,
+    "fakeip": {}
   }
   }
 }
 }
 
 
@@ -23,6 +24,7 @@
 |----------|--------------------------------|
 |----------|--------------------------------|
 | `server` | List of [DNS Server](./server) |
 | `server` | List of [DNS Server](./server) |
 | `rules`  | List of [DNS Rule](./rule)     |
 | `rules`  | List of [DNS Rule](./rule)     |
+| `fakeip` | [FakeIP](./fakeip)             |
 
 
 #### final
 #### 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.
 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.

+ 6 - 1
docs/configuration/dns/index.zh.md

@@ -11,7 +11,8 @@
     "strategy": "",
     "strategy": "",
     "disable_cache": false,
     "disable_cache": false,
     "disable_expire": false,
     "disable_expire": false,
-    "reverse_mapping": false
+    "reverse_mapping": false,
+    "fakeip": {}
   }
   }
 }
 }
 
 
@@ -51,3 +52,7 @@
 在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。
 在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。
 
 
 由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。
 由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。
+
+#### fakeip
+
+[FakeIP](./fakeip) 设置。

+ 8 - 2
docs/configuration/dns/rule.md

@@ -84,14 +84,16 @@
           "direct"
           "direct"
         ],
         ],
         "server": "local",
         "server": "local",
-        "disable_cache": false
+        "disable_cache": false,
+        "rewrite_ttl": 100
       },
       },
       {
       {
         "type": "logical",
         "type": "logical",
         "mode": "and",
         "mode": "and",
         "rules": [],
         "rules": [],
         "server": "local",
         "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.
 Disable cache and save cache in this query.
 
 
+#### rewrite_ttl
+
+Rewrite TTL in DNS responses.
+
 ### Logical Fields
 ### Logical Fields
 
 
 #### type
 #### type

+ 4 - 0
docs/configuration/dns/rule.zh.md

@@ -243,6 +243,10 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
 
 
 在此查询中禁用缓存。
 在此查询中禁用缓存。
 
 
+#### rewrite_ttl
+
+重写 DNS 回应中的 TTL。
+
 ### 逻辑字段
 ### 逻辑字段
 
 
 #### type
 #### type

+ 12 - 11
docs/configuration/dns/server.md

@@ -30,17 +30,18 @@ The tag of the dns server.
 
 
 The address 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 ""
 !!! warning ""
 
 

+ 12 - 11
docs/configuration/dns/server.zh.md

@@ -30,17 +30,18 @@ DNS 服务器的标签。
 
 
 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 ""
 !!! warning ""
 
 

+ 18 - 0
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.

+ 17 - 0
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 被缓存的环境中丢失记录。

+ 0 - 6
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
 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.
 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
 #### Naive outbound
 
 
 NaïveProxy's main function is chromium's network stack, and it makes no sense to implement only its transport protocol.
 NaïveProxy's main function is chromium's network stack, and it makes no sense to implement only its transport protocol.

+ 0 - 5
docs/faq/index.zh.md

@@ -9,11 +9,6 @@
 
 
 如果 sing-box 缺少功能 X 让您感到困扰,请原谅我们并调查 sing-box 确实有的功能。 您可能会发现它们以有趣的方式弥补了 X 的缺失。
 如果 sing-box 缺少功能 X 让您感到困扰,请原谅我们并调查 sing-box 确实有的功能。 您可能会发现它们以有趣的方式弥补了 X 的缺失。
 
 
-#### Fake IP
-
-Fake IP(也称 Fake DNS)是一种侵入性和不完善的 DNS 解决方案,它打破了预期的行为,导致 DNS 泄漏并使许多软件无法使用。
-一些缺乏 DNS 处理和缓存的软件推荐使用它,但 sing-box 不需要。
-
 #### Naive 出站
 #### Naive 出站
 
 
 NaïveProxy 的主要功能是 chromium 的网络栈,仅实现它的传输协议是舍本逐末的。
 NaïveProxy 的主要功能是 chromium 的网络栈,仅实现它的传输协议是舍本逐末的。

+ 16 - 9
experimental/clashapi/cache.go

@@ -3,21 +3,28 @@ package clashapi
 import (
 import (
 	"net/http"
 	"net/http"
 
 
+	"github.com/sagernet/sing-box/adapter"
+
 	"github.com/go-chi/chi/v5"
 	"github.com/go-chi/chi/v5"
 	"github.com/go-chi/render"
 	"github.com/go-chi/render"
 )
 )
 
 
-func cacheRouter() http.Handler {
+func cacheRouter(router adapter.Router) http.Handler {
 	r := chi.NewRouter()
 	r := chi.NewRouter()
-	r.Post("/fakeip/flush", flushFakeip)
+	r.Post("/fakeip/flush", flushFakeip(router))
 	return r
 	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)
+	}
 }
 }

+ 77 - 0
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)
+	})
+}

+ 9 - 3
experimental/clashapi/server.go

@@ -44,6 +44,7 @@ type Server struct {
 	urlTestHistory *urltest.HistoryStorage
 	urlTestHistory *urltest.HistoryStorage
 	mode           string
 	mode           string
 	storeSelected  bool
 	storeSelected  bool
+	storeFakeIP    bool
 	cacheFilePath  string
 	cacheFilePath  string
 	cacheFile      adapter.ClashCacheFile
 	cacheFile      adapter.ClashCacheFile
 }
 }
@@ -61,12 +62,13 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options
 		trafficManager: trafficManager,
 		trafficManager: trafficManager,
 		urlTestHistory: urltest.NewHistoryStorage(),
 		urlTestHistory: urltest.NewHistoryStorage(),
 		mode:           strings.ToLower(options.DefaultMode),
 		mode:           strings.ToLower(options.DefaultMode),
+		storeSelected:  options.StoreSelected,
+		storeFakeIP:    options.StoreFakeIP,
 	}
 	}
 	if server.mode == "" {
 	if server.mode == "" {
 		server.mode = "rule"
 		server.mode = "rule"
 	}
 	}
-	if options.StoreSelected {
-		server.storeSelected = true
+	if options.StoreSelected || options.StoreFakeIP {
 		cachePath := os.ExpandEnv(options.CacheFile)
 		cachePath := os.ExpandEnv(options.CacheFile)
 		if cachePath == "" {
 		if cachePath == "" {
 			cachePath = "cache.db"
 			cachePath = "cache.db"
@@ -99,7 +101,7 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options
 		r.Mount("/providers/rules", ruleProviderRouter())
 		r.Mount("/providers/rules", ruleProviderRouter())
 		r.Mount("/script", scriptRouter())
 		r.Mount("/script", scriptRouter())
 		r.Mount("/profile", profileRouter())
 		r.Mount("/profile", profileRouter())
-		r.Mount("/cache", cacheRouter())
+		r.Mount("/cache", cacheRouter(router))
 		r.Mount("/dns", dnsRouter(router))
 		r.Mount("/dns", dnsRouter(router))
 	})
 	})
 	if options.ExternalUI != "" {
 	if options.ExternalUI != "" {
@@ -156,6 +158,10 @@ func (s *Server) StoreSelected() bool {
 	return s.storeSelected
 	return s.storeSelected
 }
 }
 
 
+func (s *Server) StoreFakeIP() bool {
+	return s.storeFakeIP
+}
+
 func (s *Server) CacheFile() adapter.ClashCacheFile {
 func (s *Server) CacheFile() adapter.ClashCacheFile {
 	return s.cacheFile
 	return s.cacheFile
 }
 }

+ 3 - 0
mkdocs.yml

@@ -48,6 +48,7 @@ nav:
           - configuration/dns/index.md
           - configuration/dns/index.md
           - DNS Server: configuration/dns/server.md
           - DNS Server: configuration/dns/server.md
           - DNS Rule: configuration/dns/rule.md
           - DNS Rule: configuration/dns/rule.md
+          - FakeIP: configuration/dns/fakeip.md
       - NTP:
       - NTP:
           - configuration/ntp/index.md
           - configuration/ntp/index.md
       - Route:
       - Route:
@@ -102,6 +103,7 @@ nav:
           - URLTest: configuration/outbound/urltest.md
           - URLTest: configuration/outbound/urltest.md
   - FAQ:
   - FAQ:
       - faq/index.md
       - faq/index.md
+      - FakeIP: faq/fakeip.md
       - Known Issues: faq/known-issues.md
       - Known Issues: faq/known-issues.md
   - Examples:
   - Examples:
       - examples/index.md
       - examples/index.md
@@ -111,6 +113,7 @@ nav:
       - Shadowsocks: examples/shadowsocks.md
       - Shadowsocks: examples/shadowsocks.md
       - ShadowTLS: examples/shadowtls.md
       - ShadowTLS: examples/shadowtls.md
       - Clash API: examples/clash-api.md
       - Clash API: examples/clash-api.md
+      - WireGuard Direct: examples/wireguard-direct.md
   - Contributing:
   - Contributing:
       - contributing/index.md
       - contributing/index.md
       - Developing:
       - Developing:

+ 1 - 0
option/clash.go

@@ -6,6 +6,7 @@ type ClashAPIOptions struct {
 	Secret             string `json:"secret,omitempty"`
 	Secret             string `json:"secret,omitempty"`
 	DefaultMode        string `json:"default_mode,omitempty"`
 	DefaultMode        string `json:"default_mode,omitempty"`
 	StoreSelected      bool   `json:"store_selected,omitempty"`
 	StoreSelected      bool   `json:"store_selected,omitempty"`
+	StoreFakeIP        bool   `json:"store_fakeip,omitempty"`
 	CacheFile          string `json:"cache_file,omitempty"`
 	CacheFile          string `json:"cache_file,omitempty"`
 }
 }
 
 

+ 13 - 6
option/dns.go

@@ -5,15 +5,10 @@ type DNSOptions struct {
 	Rules          []DNSRule          `json:"rules,omitempty"`
 	Rules          []DNSRule          `json:"rules,omitempty"`
 	Final          string             `json:"final,omitempty"`
 	Final          string             `json:"final,omitempty"`
 	ReverseMapping bool               `json:"reverse_mapping,omitempty"`
 	ReverseMapping bool               `json:"reverse_mapping,omitempty"`
+	FakeIP         *DNSFakeIPOptions  `json:"fakeip,omitempty"`
 	DNSClientOptions
 	DNSClientOptions
 }
 }
 
 
-type DNSClientOptions struct {
-	Strategy      DomainStrategy `json:"strategy,omitempty"`
-	DisableCache  bool           `json:"disable_cache,omitempty"`
-	DisableExpire bool           `json:"disable_expire,omitempty"`
-}
-
 type DNSServerOptions struct {
 type DNSServerOptions struct {
 	Tag                  string         `json:"tag,omitempty"`
 	Tag                  string         `json:"tag,omitempty"`
 	Address              string         `json:"address"`
 	Address              string         `json:"address"`
@@ -23,3 +18,15 @@ type DNSServerOptions struct {
 	Strategy             DomainStrategy `json:"strategy,omitempty"`
 	Strategy             DomainStrategy `json:"strategy,omitempty"`
 	Detour               string         `json:"detour,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"`
+}

+ 63 - 6
route/router.go

@@ -24,6 +24,7 @@ import (
 	"github.com/sagernet/sing-box/ntp"
 	"github.com/sagernet/sing-box/ntp"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/outbound"
 	"github.com/sagernet/sing-box/outbound"
+	"github.com/sagernet/sing-box/transport/fakeip"
 	"github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing-tun"
 	"github.com/sagernet/sing-vmess"
 	"github.com/sagernet/sing-vmess"
@@ -66,6 +67,7 @@ type Router struct {
 	transportMap                       map[string]dns.Transport
 	transportMap                       map[string]dns.Transport
 	transportDomainStrategy            map[dns.Transport]dns.DomainStrategy
 	transportDomainStrategy            map[dns.Transport]dns.DomainStrategy
 	dnsReverseMapping                  *DNSReverseMapping
 	dnsReverseMapping                  *DNSReverseMapping
+	fakeIPStore                        adapter.FakeIPStore
 	interfaceFinder                    myInterfaceFinder
 	interfaceFinder                    myInterfaceFinder
 	autoDetectInterface                bool
 	autoDetectInterface                bool
 	defaultInterface                   string
 	defaultInterface                   string
@@ -178,12 +180,8 @@ func NewRouter(
 					} else {
 					} else {
 						continue
 						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)
 			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()
 		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()
 	usePlatformDefaultInterfaceMonitor := platformInterface != nil && platformInterface.UsePlatformDefaultInterfaceMonitor()
 	needInterfaceMonitor := options.AutoDetectInterface || common.Any(inbounds, func(inbound option.Inbound) bool {
 	needInterfaceMonitor := options.AutoDetectInterface || common.Any(inbounds, func(inbound option.Inbound) bool {
 		return inbound.HTTPOptions.SetSystemProxy || inbound.MixedOptions.SetSystemProxy || inbound.TunOptions.AutoRoute
 		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, "]")
 			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 {
 	for i, transport := range r.transports {
 		err := transport.Start()
 		err := transport.Start()
 		if err != nil {
 		if err != nil {
@@ -517,6 +533,12 @@ func (r *Router) Close() error {
 			return E.Cause(err, "close time service")
 			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
 	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 {
 func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	if metadata.InboundDetour != "" {
 	if metadata.InboundDetour != "" {
 		if metadata.LastInbound == 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()}
 		metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()}
 		return r.RoutePacketConnection(ctx, uot.NewConn(conn, uot.Request{}), metadata)
 		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 {
 	if metadata.InboundOptions.SniffEnabled {
 		buffer := buf.NewPacket()
 		buffer := buf.NewPacket()
 		buffer.FullReset()
 		buffer.FullReset()
@@ -675,6 +714,21 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
 		return nil
 		return nil
 	}
 	}
 	metadata.Network = N.NetworkUDP
 	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 {
 	if metadata.InboundOptions.SniffEnabled {
 		buffer := buf.NewPacket()
 		buffer := buf.NewPacket()
 		buffer.FullReset()
 		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)
 			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)
 	return detour.NewPacketConnection(ctx, conn, metadata)
 }
 }
 
 

+ 4 - 0
route/rule_abstract.go

@@ -57,6 +57,10 @@ func (r *abstractDefaultRule) UpdateGeosite() error {
 }
 }
 
 
 func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
 func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
+	if len(r.allItems) == 0 {
+		return true
+	}
+
 	for _, item := range r.items {
 	for _, item := range r.items {
 		if !item.Match(metadata) {
 		if !item.Match(metadata) {
 			return r.invert
 			return r.invert

+ 44 - 0
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
+}

+ 55 - 0
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
+}

+ 83 - 0
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
+}

+ 108 - 0
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()
+}