Add fakeip support

This commit is contained in:
世界 2023-03-25 12:03:23 +08:00
parent 58c4fd745a
commit b484d9bca6
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
29 changed files with 693 additions and 62 deletions

View file

@ -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
adapter/fakeip.go Normal file
View file

@ -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
}

View file

@ -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
}

View file

@ -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
RouteIPConnection(ctx context.Context, conn tun.RouteContext, metadata InboundContext) tun.RouteAction RouteIPConnection(ctx context.Context, conn tun.RouteContext, metadata InboundContext) tun.RouteAction

View file

@ -0,0 +1,25 @@
# FakeIP
### Structure
```json
{
"enabled": true,
"inet4_range": "172.19.0.1/15",
"inet6_range": "fdfe:dcba:9876::1/18"
}
```
### Fields
#### enabled
Enable FakeIP service.
#### inet4_range
IPv4 address range for FakeIP.
#### inet6_address
IPv6 address range for FakeIP.

View file

@ -0,0 +1,25 @@
# FakeIP
### 结构
```json
{
"enabled": true,
"inet4_range": "172.19.0.1/15",
"inet6_range": "fdfe:dcba:9876::1/18"
}
```
### 字段
#### enabled
启用 FakeIP 服务。
#### inet4_range
用于 FakeIP 的 IPv4 地址范围。
#### inet6_range
用于 FakeIP 的 IPv6 地址范围。

View file

@ -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.

View file

@ -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) 设置。

View file

@ -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

View file

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

View file

@ -30,17 +30,18 @@ The tag of the dns server.
The address of the dns server. The address of the dns server.
| Protocol | Format | | Protocol | Format |
|----------|-------------------------------| |---------------------|-------------------------------|
| `System` | `local` | | `System` | `local` |
| `TCP` | `tcp://1.0.0.1` | | `TCP` | `tcp://1.0.0.1` |
| `UDP` | `8.8.8.8` `udp://8.8.4.4` | | `UDP` | `8.8.8.8` `udp://8.8.4.4` |
| `TLS` | `tls://dns.google` | | `TLS` | `tls://dns.google` |
| `HTTPS` | `https://1.1.1.1/dns-query` | | `HTTPS` | `https://1.1.1.1/dns-query` |
| `QUIC` | `quic://dns.adguard.com` | | `QUIC` | `quic://dns.adguard.com` |
| `HTTP3` | `h3://8.8.8.8/dns-query` | | `HTTP3` | `h3://8.8.8.8/dns-query` |
| `RCode` | `rcode://refused` | | `RCode` | `rcode://refused` |
| `DHCP` | `dhcp://auto` or `dhcp://en0` | | `DHCP` | `dhcp://auto` or `dhcp://en0` |
| [FakeIP](./fakeip) | `fakeip` |
!!! warning "" !!! warning ""

View file

@ -30,17 +30,18 @@ DNS 服务器的标签。
DNS 服务器的地址。 DNS 服务器的地址。
| 协议 | 格式 | | 协议 | 格式 |
|----------|------------------------------| |--------------------|------------------------------|
| `System` | `local` | | `System` | `local` |
| `TCP` | `tcp://1.0.0.1` | | `TCP` | `tcp://1.0.0.1` |
| `UDP` | `8.8.8.8` `udp://8.8.4.4` | | `UDP` | `8.8.8.8` `udp://8.8.4.4` |
| `TLS` | `tls://dns.google` | | `TLS` | `tls://dns.google` |
| `HTTPS` | `https://1.1.1.1/dns-query` | | `HTTPS` | `https://1.1.1.1/dns-query` |
| `QUIC` | `quic://dns.adguard.com` | | `QUIC` | `quic://dns.adguard.com` |
| `HTTP3` | `h3://8.8.8.8/dns-query` | | `HTTP3` | `h3://8.8.8.8/dns-query` |
| `RCode` | `rcode://refused` | | `RCode` | `rcode://refused` |
| `DHCP` | `dhcp://auto``dhcp://en0` | | `DHCP` | `dhcp://auto``dhcp://en0` |
| [FakeIP](./fakeip) | `fakeip` |
!!! warning "" !!! warning ""

20
docs/faq/fakeip.md Normal file
View file

@ -0,0 +1,20 @@
# 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
* Retrieve the requested domain in places like IP routing (L3) where traffic detection is not possible to assist with routing.
* Decrease an RTT on the first TCP request to a domain (the most common reason).
#### 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
* Do not use if you do not need L3 routing.
* 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.

19
docs/faq/fakeip.zh.md Normal file
View file

@ -0,0 +1,19 @@
# FakeIP
FakeIP 是指同时劫持 DNS 和连接请求的程序中的一种行为。它通过虚拟结果响应 DNS 请求,在接受连接时恢复映射。
#### 优点
* 在像 L3 路由这样无法进行流量探测的地方检索所请求的域名,以协助路由。
* 减少对一个域的第一个 TCP 请求的 RTT这是最常见的原因
#### 限制
* 它的机制会破坏依赖于返回正确远程地址的应用程序。
* 仅支持 A 和 AAAAIP请求这可能会破坏依赖于其他请求的应用程序。
#### 建议
* 如果不需要 L3 路由,请勿使用。
* 如果使用 tun请确保 tun 路由中包含 FakeIP 地址范围。
* 启用 `experimental.clash_api.store_fakeip` 以持久化 FakeIP 记录,或者使用 `dns.rules.rewrite_ttl` 避免程序重启后在 DNS 被缓存的环境中丢失记录。

View file

@ -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.

View file

@ -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 的网络栈,仅实现它的传输协议是舍本逐末的。

View file

@ -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) { func flushFakeip(router adapter.Router) func(w http.ResponseWriter, r *http.Request) {
/*if err := cachefile.Cache().FlushFakeip(); err != nil { return func(w http.ResponseWriter, r *http.Request) {
render.Status(r, http.StatusInternalServerError) if cacheFile := router.ClashServer().CacheFile(); cacheFile != nil {
render.JSON(w, r, newError(err.Error())) err := cacheFile.FakeIPReset()
return if err != nil {
}*/ render.Status(r, http.StatusInternalServerError)
render.NoContent(w, r) render.JSON(w, r, newError(err.Error()))
return
}
}
render.NoContent(w, r)
}
} }

View file

@ -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)
})
}

View file

@ -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 { if options.StoreSelected || options.StoreFakeIP {
server.storeSelected = true
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
} }

View file

@ -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:
@ -103,6 +104,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
@ -112,6 +114,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:

View file

@ -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"`
} }

View file

@ -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"`
}

View file

@ -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"
@ -88,6 +89,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
@ -218,12 +220,8 @@ func NewRouter(
} else { } else {
continue continue
} }
} else if notIpAddress != nil { } else if notIpAddress != nil && strings.Contains(server.Address, ".") {
switch serverURL.Scheme { return nil, E.New("parse dns server[", tag, "]: missing address_resolver")
case "rcode", "dhcp":
default:
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)
@ -279,6 +277,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)
}
needInterfaceMonitor := platformInterface == nil && (options.AutoDetectInterface || common.Any(inbounds, func(inbound option.Inbound) bool { needInterfaceMonitor := platformInterface == nil && (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
})) }))
@ -485,6 +495,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 {
@ -550,6 +566,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
} }
@ -566,6 +588,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 {
@ -618,6 +644,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()
@ -706,6 +745,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()
@ -764,6 +818,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)
} }

View file

@ -8,9 +8,22 @@ import (
"github.com/sagernet/sing-dns" "github.com/sagernet/sing-dns"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
M "github.com/sagernet/sing/common/metadata"
) )
func (r *Router) RouteIPConnection(ctx context.Context, conn tun.RouteContext, metadata adapter.InboundContext) tun.RouteAction { func (r *Router) RouteIPConnection(ctx context.Context, conn tun.RouteContext, metadata adapter.InboundContext) tun.RouteAction {
if r.fakeIPStore != nil && r.fakeIPStore.Contains(metadata.Destination.Addr) {
domain, loaded := r.fakeIPStore.Lookup(metadata.Destination.Addr)
if !loaded {
r.logger.ErrorContext(ctx, "missing fakeip context")
return (*tun.ActionReturn)(nil)
}
metadata.Destination = M.Socksaddr{
Fqdn: domain,
Port: metadata.Destination.Port,
}
r.logger.DebugContext(ctx, "found fakeip domain: ", domain)
}
if r.dnsReverseMapping != nil && metadata.Domain == "" { if r.dnsReverseMapping != nil && metadata.Domain == "" {
domain, loaded := r.dnsReverseMapping.Query(metadata.Destination.Addr) domain, loaded := r.dnsReverseMapping.Query(metadata.Destination.Addr)
if loaded { if loaded {

View file

@ -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

View file

@ -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
}

View file

@ -0,0 +1,42 @@
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: origin,
destination: destination,
}
}
func (c *NATPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
destination, err = c.PacketConn.ReadPacket(buffer)
if destination == c.origin {
destination = c.destination
}
return
}
func (c *NATPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
if destination == c.destination {
destination = c.origin
}
return c.PacketConn.WritePacket(buffer, destination)
}
func (c *NATPacketConn) Upstream() any {
return c.PacketConn
}

View file

@ -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
transport/fakeip/store.go Normal file
View file

@ -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()
}