Fake DNS 深入解析
Fake DNS(FakeDNS)是一种将临时、唯一的 IP 地址从私有地址池分配给域名的机制。当后续连接到达某个虚假 IP 时,系统会将其反向映射回原始域名。这使得透明代理环境中,在 DNS 查询先于实际连接到达的情况下,即使面对不透明的 IP 目的地址流量,也能实现基于域名的路由。
架构概述
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)。
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
}初始化:
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:
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}
}该算法:
- 如果域名之前已出现过,返回缓存的 IP
- 使用当前时间的毫秒值对地址池大小取模作为起始偏移量
- 线性探测直到找到未使用的 IP
- 如果到达末尾则回绕到地址池起始位置
- 在 LRU 缓存中存储双向映射
反向查找
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)
}地址池成员检查
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 地址池)。
type HolderMulti struct {
holders []*Holder
config *FakeDnsPoolMulti
}所有方法都委托给每个 holder:
GetFakeIPForDomain()返回所有地址池的 IPGetFakeIPForDomain3()按每个 holder 的 IPv4/IPv6 启用状态过滤GetDomainFromFakeDNS()返回所有地址池中的第一个匹配IsIPInIPPool()如果任何地址池包含该 IP 则返回 true
IPv4 与 IPv6 过滤
Holder 上的 GetFakeIPForDomain3() 方法检查地址池的 IP 族是否匹配请求:
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 服务器系统:
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:
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() 函数实现了组合嗅探器:
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() 方法检查:
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 配置的顶层配置:
{
"fakeDns": {
"ipPool": "198.18.0.0/15",
"poolSize": 65535
}
}或双栈配置:
{
"fakeDns": {
"pools": [
{ "ipPool": "198.18.0.0/15", "poolSize": 65535 },
{ "ipPool": "fc00::/18", "poolSize": 65535 }
]
}
}然后在 DNS 服务器中引用:
{
"dns": {
"servers": ["fakedns"]
}
}完整数据流
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 问题。