Skip to content

Fake DNS 深入解析

Fake DNS(FakeDNS)是一种将临时、唯一的 IP 地址从私有地址池分配给域名的机制。当后续连接到达某个虚假 IP 时,系统会将其反向映射回原始域名。这使得透明代理环境中,在 DNS 查询先于实际连接到达的情况下,即使面对不透明的 IP 目的地址流量,也能实现基于域名的路由。

架构概述

mermaid
flowchart LR
    subgraph DNS 解析
        A[客户端查询 example.com] --> B[FakeDNSServer]
        B --> C[Holder.GetFakeIPForDomain]
        C --> D[返回 198.18.0.42]
    end

    subgraph 连接阶段
        E[客户端连接 198.18.0.42:443] --> F[Dispatcher 嗅探]
        F --> G[fakeDNSSniffer]
        G --> H[Holder.GetDomainFromFakeDNS]
        H --> I[返回 example.com]
        I --> J[覆盖目的地为 example.com:443]
    end

核心组件

Holder -- 单地址池

文件: app/dns/fakedns/fake.go

Holder 管理单个虚假 IP 地址池(IPv4 或 IPv6)。

go
type Holder struct {
    domainToIP cache.Lru    // bidirectional LRU: domain <-> IP
    ipRange    *net.IPNet   // the CIDR pool (e.g., 198.18.0.0/15)
    mu         *sync.Mutex
    config     *FakeDnsPool
}

初始化:

go
func (fkdns *Holder) initialize(ipPoolCidr string, lruSize int) error {
    _, ipRange, _ := net.ParseCIDR(ipPoolCidr)
    ones, bits := ipRange.Mask.Size()
    rooms := bits - ones
    if math.Log2(float64(lruSize)) >= float64(rooms) {
        return errors.New("LRU size is bigger than subnet size")
    }
    fkdns.domainToIP = cache.NewLru(lruSize)
    fkdns.ipRange = ipRange
    fkdns.mu = new(sync.Mutex)
}

LRU 大小必须严格小于子网大小(通过 log2(lruSize) < rooms 检查)。默认地址池为 198.18.0.0/15,包含 65535 个条目。

IP 分配算法

GetFakeIPForDomain() 方法使用基于时间种子的方式分配 IP:

go
func (fkdns *Holder) GetFakeIPForDomain(domain string) []net.Address {
    fkdns.mu.Lock()
    defer fkdns.mu.Unlock()

    // 1. Check if domain already has an IP
    if v, ok := fkdns.domainToIP.Get(domain); ok {
        return []net.Address{v.(net.Address)}
    }

    // 2. Seed from current time (milliseconds)
    currentTimeMillis := uint64(time.Now().UnixNano() / 1e6)
    ones, bits := fkdns.ipRange.Mask.Size()
    rooms := bits - ones
    if rooms < 64 {
        currentTimeMillis %= (uint64(1) << rooms)
    }

    // 3. Compute candidate IP: base + offset
    bigIntIP := big.NewInt(0).SetBytes(fkdns.ipRange.IP)
    bigIntIP = bigIntIP.Add(bigIntIP, new(big.Int).SetUint64(currentTimeMillis))

    // 4. Linear probe for unused IP
    for {
        ip = net.IPAddress(bigIntIP.Bytes())
        if _, ok := fkdns.domainToIP.PeekKeyFromValue(ip); !ok {
            break  // IP not in use
        }
        bigIntIP = bigIntIP.Add(bigIntIP, big.NewInt(1))
        if !fkdns.ipRange.Contains(bigIntIP.Bytes()) {
            bigIntIP = big.NewInt(0).SetBytes(fkdns.ipRange.IP)  // wrap around
        }
    }

    // 5. Store and return
    fkdns.domainToIP.Put(domain, ip)
    return []net.Address{ip}
}

该算法:

  1. 如果域名之前已出现过,返回缓存的 IP
  2. 使用当前时间的毫秒值对地址池大小取模作为起始偏移量
  3. 线性探测直到找到未使用的 IP
  4. 如果到达末尾则回绕到地址池起始位置
  5. 在 LRU 缓存中存储双向映射

反向查找

go
func (fkdns *Holder) GetDomainFromFakeDNS(ip net.Address) string {
    if !ip.Family().IsIP() || !fkdns.ipRange.Contains(ip.IP()) {
        return ""
    }
    if k, ok := fkdns.domainToIP.GetKeyFromValue(ip); ok {
        return k.(string)
    }
    return ""  // IP in pool but no mapping (evicted from LRU)
}

地址池成员检查

go
func (fkdns *Holder) IsIPInIPPool(ip net.Address) bool {
    if ip.Family().IsDomain() { return false }
    return fkdns.ipRange.Contains(ip.IP())
}

HolderMulti -- 双栈支持

文件: app/dns/fakedns/fake.go

HolderMulti 包装多个 Holder 实例(通常是一个 IPv4 地址池和一个 IPv6 地址池)。

go
type HolderMulti struct {
    holders []*Holder
    config  *FakeDnsPoolMulti
}

所有方法都委托给每个 holder:

  • GetFakeIPForDomain() 返回所有地址池的 IP
  • GetFakeIPForDomain3() 按每个 holder 的 IPv4/IPv6 启用状态过滤
  • GetDomainFromFakeDNS() 返回所有地址池中的第一个匹配
  • IsIPInIPPool() 如果任何地址池包含该 IP 则返回 true

IPv4 与 IPv6 过滤

Holder 上的 GetFakeIPForDomain3() 方法检查地址池的 IP 族是否匹配请求:

go
func (fkdns *Holder) GetFakeIPForDomain3(domain string, ipv4, ipv6 bool) []net.Address {
    isIPv6 := fkdns.ipRange.IP.To4() == nil
    if (isIPv6 && ipv6) || (!isIPv6 && ipv4) {
        return fkdns.GetFakeIPForDomain(domain)
    }
    return []net.Address{}
}

LRU 缓存

cache.Lru 类型是一个双向 LRU 缓存,支持:

  • Get(key) -- 标准键查找
  • Put(key, value) -- 插入并淘汰最旧条目
  • GetKeyFromValue(value) -- 反向查找(值到键)
  • PeekKeyFromValue(value) -- 反向查找但不更新访问顺序

这种双向能力至关重要:正向映射(域名 -> IP)用于 DNS 解析阶段,反向映射(IP -> 域名)用于连接嗅探阶段。

当 LRU 淘汰一个条目时,该 IP 可被不同域名复用。这意味着到被淘汰虚假 IP 的连接无法进行反向映射,会记录一条信息性日志。

FakeDNS 服务器集成

文件: app/dns/nameserver_fakedns.go

FakeDNSServer 接入 DNS 服务器系统:

go
type FakeDNSServer struct {
    fakeDNSEngine dns.FakeDNSEngine
}

func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, opt dns.IPOption) ([]net.IP, uint32, error) {
    var ips []net.Address
    if fkr0, ok := f.fakeDNSEngine.(dns.FakeDNSEngineRev0); ok {
        ips = fkr0.GetFakeIPForDomain3(domain, opt.IPv4Enable, opt.IPv6Enable)
    } else {
        ips = f.fakeDNSEngine.GetFakeIPForDomain(domain)
    }
    // ... convert to net.IP
    return netIP, 1, nil  // TTL = 1
}

TTL 始终为 1 秒,以防止下游缓存虚假 IP。

Dispatcher 集成 -- Fake DNS 嗅探

文件: app/dispatcher/fakednssniffer.go

当嗅探功能启用并在 destOverride 中包含 "fakedns" 时,调度器会创建 fakeDNSSniffer

go
func newFakeDNSSniffer(ctx context.Context) (protocolSnifferWithMetadata, error) {
    fakeDNSEngine := core.MustFromContext(ctx).GetFeature((*dns.FakeDNSEngine)(nil))

    return protocolSnifferWithMetadata{
        protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) {
            ob := outbounds[len(outbounds)-1]
            domainFromFakeDNS := fakeDNSEngine.GetDomainFromFakeDNS(ob.Target.Address)
            if domainFromFakeDNS != "" {
                return &fakeDNSSniffResult{domainName: domainFromFakeDNS}, nil
            }
            // Check if IP is in pool (for fakedns+others mode)
            return nil, common.ErrNoClue
        },
        metadataSniffer: true,  // Can sniff without payload data
    }, nil
}

这是一个元数据嗅探器——它不需要检查有效载荷字节。它在虚假 DNS 引擎中查找目的 IP 并返回映射的域名。

fakedns+others 模式

文件: app/dispatcher/fakednssniffer.go

newFakeDNSThenOthers() 函数实现了组合嗅探器:

go
func newFakeDNSThenOthers(ctx context.Context, fakeDNSSniffer, others []protocolSnifferWithMetadata) {
    return protocolSnifferWithMetadata{
        protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) {
            // 1. Try fakeDNS lookup first
            result, err := fakeDNSSniffer.protocolSniffer(ctx, bytes)
            if err == nil { return result, nil }

            // 2. If IP is in fake pool but domain not found, try other sniffers
            if ipInRange {
                for _, v := range others {
                    if result, err := v.protocolSniffer(ctx, bytes); err == nil {
                        return DNSThenOthersSniffResult{
                            domainName: result.Domain(),
                            protocolOriginalName: result.Protocol(),
                        }, nil
                    }
                }
            }
            return nil, common.ErrNoClue
        },
    }
}

这处理了虚假 IP 已分配但 LRU 已淘汰映射的情况。它回退到 TLS SNI 或 HTTP Host 嗅探来恢复域名。

Dispatcher 中的目的地覆盖

app/dispatcher/default.go 中,shouldOverride() 方法检查:

go
func (d *DefaultDispatcher) shouldOverride(ctx context.Context, result SniffResult, ...) bool {
    for _, p := range request.OverrideDestinationForProtocol {
        if fkr0, ok := d.fdns.(dns.FakeDNSEngineRev0); ok &&
           protocolString != "bittorrent" && p == "fakedns" &&
           fkr0.IsIPInIPPool(destination.Address) {
            return true  // Override even for non-fakedns protocols if IP is fake
        }
    }
}

当目的地址是虚假 IP 且覆盖列表中包含 "fakedns" 时,嗅探到的域名会替换虚假 IP 目的地址,无论是哪个嗅探器发现的域名。

配置

FakeDNS 引擎在 Xray 配置的顶层配置:

json
{
    "fakeDns": {
        "ipPool": "198.18.0.0/15",
        "poolSize": 65535
    }
}

或双栈配置:

json
{
    "fakeDns": {
        "pools": [
            { "ipPool": "198.18.0.0/15", "poolSize": 65535 },
            { "ipPool": "fc00::/18", "poolSize": 65535 }
        ]
    }
}

然后在 DNS 服务器中引用:

json
{
    "dns": {
        "servers": ["fakedns"]
    }
}

完整数据流

mermaid
sequenceDiagram
    participant App as 应用程序
    participant DNS as Xray DNS
    participant FakeDNS as FakeDNS Holder
    participant Disp as Dispatcher
    participant Sniffer as FakeDNS 嗅探器
    participant Router as 路由器

    App->>DNS: 解析 example.com
    DNS->>FakeDNS: GetFakeIPForDomain("example.com")
    FakeDNS-->>DNS: 198.18.0.42
    DNS-->>App: 198.18.0.42

    App->>Disp: 连接 198.18.0.42:443
    Disp->>Sniffer: 嗅探元数据
    Sniffer->>FakeDNS: GetDomainFromFakeDNS(198.18.0.42)
    FakeDNS-->>Sniffer: "example.com"
    Sniffer-->>Disp: domain = example.com
    Disp->>Disp: 覆盖目标为 example.com:443
    Disp->>Router: 路由 example.com:443
    Router-->>Disp: 选择出站

实现要点

  • Holder 注册为 FakeDnsPoolFakeDnsPoolMulti 两种配置类型。core 根据配置是单地址池还是多地址池来创建相应的实例。

  • FakeDNS 被前置(而非追加)到 core 配置的应用列表中:config.App = append([]*serial.TypedMessage{serial.ToTypedMessage(r)}, config.App...)。这确保它在 DNS 系统尝试解析 core.RequireFeatures() 之前完成初始化。

  • FakeDNSEngineRev0 接口扩展了 FakeDNSEngine,增加了 GetFakeIPForDomain3()IsIPInIPPool()。所有当前实现都满足这两个接口。

  • LRU 淘汰是静默的——当某个域名的映射被淘汰后,后续到该虚假 IP 的连接无法进行反向映射。fakedns+others 嗅探模式通过回退到 TLS/HTTP 嗅探来缓解这一问题。

  • Holder 上的互斥锁是普通的 sync.Mutex(而非 RWMutex),因为 GetFakeIPForDomain() 可能写入(分配新 IP),即使 Get() 操作也会更新 LRU 的访问顺序。

  • BitTorrent 流量在 shouldOverride() 中被显式排除在 Fake DNS 覆盖之外,以防止 tracker 问题。

用于重新实现目的的技术分析。