Skip to content

Fake DNS Deep Dive

Fake DNS (FakeDNS) is a mechanism that assigns temporary, unique IP addresses from a private pool to domain names. When a connection is later made to one of these fake IPs, the system reverse-maps it back to the original domain. This enables transparent proxy setups where DNS queries arrive before the actual connection, allowing domain-based routing even for opaque IP-destination traffic.

Architecture Overview

mermaid
flowchart LR
    subgraph DNS Resolution
        A[Client DNS query for example.com] --> B[FakeDNSServer]
        B --> C[Holder.GetFakeIPForDomain]
        C --> D[Return 198.18.0.42]
    end

    subgraph Connection Phase
        E[Client connects to 198.18.0.42:443] --> F[Dispatcher sniffing]
        F --> G[fakeDNSSniffer]
        G --> H[Holder.GetDomainFromFakeDNS]
        H --> I[Returns example.com]
        I --> J[Override destination to example.com:443]
    end

Core Components

Holder -- Single Pool

File: app/dns/fakedns/fake.go

The Holder manages a single fake IP pool (either IPv4 or 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
}

Initialization:

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

The LRU size must be strictly smaller than the subnet size (checked via log2(lruSize) < rooms). Default pool is 198.18.0.0/15 with 65535 entries.

IP Allocation Algorithm

The GetFakeIPForDomain() method allocates IPs using a time-seeded approach:

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

The algorithm:

  1. Returns the cached IP if the domain was seen before
  2. Uses current time in milliseconds modulo pool size as the starting offset
  3. Linear probes forward until an unused IP is found
  4. Wraps around to the pool base if the end is reached
  5. Stores the bidirectional mapping in the LRU cache

Reverse Lookup

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

Pool Membership Check

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

HolderMulti -- Dual-Stack Support

File: app/dns/fakedns/fake.go

HolderMulti wraps multiple Holder instances (typically one IPv4 and one IPv6 pool).

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

All methods delegate to each holder:

  • GetFakeIPForDomain() returns IPs from all pools
  • GetFakeIPForDomain3() filters by IPv4/IPv6 enablement per holder
  • GetDomainFromFakeDNS() returns the first match across pools
  • IsIPInIPPool() returns true if any pool contains the IP

IPv4 vs IPv6 Filtering

The GetFakeIPForDomain3() method on Holder checks whether the pool's IP family matches the request:

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

The LRU Cache

The cache.Lru type is a bidirectional LRU cache that supports:

  • Get(key) -- standard key lookup
  • Put(key, value) -- insert with eviction of oldest entry
  • GetKeyFromValue(value) -- reverse lookup (value to key)
  • PeekKeyFromValue(value) -- reverse lookup without updating recency

This bidirectional capability is essential: forward mapping (domain -> IP) is used during DNS resolution, while reverse mapping (IP -> domain) is used during connection sniffing.

When the LRU evicts an entry, the IP becomes available for reuse by a different domain. This means connections to evicted fake IPs cannot be reverse-mapped, which is logged as an informational message.

FakeDNS Server Integration

File: app/dns/nameserver_fakedns.go

The FakeDNSServer plugs into the DNS server system:

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
}

The TTL is always 1 second to prevent downstream caching of fake IPs.

Dispatcher Integration -- Fake DNS Sniffing

File: app/dispatcher/fakednssniffer.go

When sniffing is enabled with "fakedns" in destOverride, the dispatcher creates a 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
}

This is a metadata sniffer -- it works without inspecting payload bytes. It looks up the destination IP in the fake DNS engine and returns the mapped domain.

The fakedns+others Mode

File: app/dispatcher/fakednssniffer.go

The newFakeDNSThenOthers() function implements a composite sniffer:

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

This handles the case where a fake IP was assigned but the LRU evicted the mapping. It falls back to TLS SNI or HTTP Host sniffing to recover the domain.

Destination Override in Dispatcher

In app/dispatcher/default.go, the shouldOverride() method checks:

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

When the destination is a fake IP and "fakedns" is in the override list, the sniffed domain replaces the fake IP destination regardless of which sniffer found the domain.

Configuration

The FakeDNS engine is configured at the top level of the Xray config:

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

Or for dual-stack:

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

The DNS server then references it:

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

Complete Data Flow

mermaid
sequenceDiagram
    participant App as Application
    participant DNS as Xray DNS
    participant FakeDNS as FakeDNS Holder
    participant Disp as Dispatcher
    participant Sniffer as FakeDNS Sniffer
    participant Router as Router

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

    App->>Disp: Connect to 198.18.0.42:443
    Disp->>Sniffer: Sniff metadata
    Sniffer->>FakeDNS: GetDomainFromFakeDNS(198.18.0.42)
    FakeDNS-->>Sniffer: "example.com"
    Sniffer-->>Disp: domain = example.com
    Disp->>Disp: Override target to example.com:443
    Disp->>Router: Route example.com:443
    Router-->>Disp: Select outbound

Implementation Notes

  • The Holder is registered as both FakeDnsPool and FakeDnsPoolMulti config types. The core creates the appropriate one based on whether the config has a single pool or multiple pools.

  • FakeDNS is prepended (not appended) to the core config app list: config.App = append([]*serial.TypedMessage{serial.ToTypedMessage(r)}, config.App...). This ensures it initializes before the DNS system tries to resolve core.RequireFeatures().

  • The FakeDNSEngineRev0 interface extends FakeDNSEngine with GetFakeIPForDomain3() and IsIPInIPPool(). All current implementations satisfy both interfaces.

  • The LRU eviction is silent -- when a domain's mapping is evicted, subsequent connections to that fake IP cannot be reverse-mapped. The fakedns+others sniffing mode mitigates this by falling back to TLS/HTTP sniffing.

  • The mutex on Holder is a regular sync.Mutex (not RWMutex) because GetFakeIPForDomain() may write (allocate new IPs), and even Get() on the LRU updates access order.

  • BitTorrent traffic is explicitly excluded from fake DNS override in shouldOverride() to prevent tracker issues.

Technical analysis for re-implementation purposes.