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 注册为 FakeDnsPool 和 FakeDnsPoolMulti 两种配置类型。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 问题。

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