Skip to content

Fake-IP Integration

Fake-IP is a technique where DNS queries return temporary, fake IP addresses from a reserved pool. When traffic to a fake IP arrives, the system looks up the original domain and routes based on the domain name rather than the (meaningless) IP.

Why Fake-IP?

Without Fake-IP, TUN-based transparent proxying faces a dilemma:

App → DNS Query "google.com" → Real DNS → 142.250.80.14
App → Connect to 142.250.80.14 → TUN → Xray
  → Xray sees IP 142.250.80.14, but doesn't know domain "google.com"
  → Cannot do domain-based routing!

With Fake-IP:

App → DNS Query "google.com" → Fake DNS → 198.18.0.42 (fake)
App → Connect to 198.18.0.42 → TUN → Xray
  → Xray detects 198.18.0.42 is in fake pool
  → Looks up: 198.18.0.42 → "google.com"
  → Routes based on domain "google.com"
  → Outbound resolves real IP and connects

Architecture

mermaid
flowchart TB
    App([Application]) -->|"DNS: google.com?"| DNS["Xray DNS Module"]
    DNS -->|"Return 198.18.0.42<br/>(fake IP)"| App

    App -->|"Connect to<br/>198.18.0.42"| TUN["TUN Interface"]
    TUN -->|"dest=198.18.0.42"| Handler["TUN Handler"]
    Handler -->|"DispatchLink(ctx,<br/>198.18.0.42:443)"| Dispatcher["Dispatcher"]

    Dispatcher --> Sniff["Sniffer"]
    Sniff --> FakeDNS["FakeDNS Sniffer"]
    FakeDNS -->|"198.18.0.42 in pool?<br/>→ google.com"| Override["Override dest:<br/>google.com:443"]
    Override --> Router["Router"]
    Router -->|"domain-based<br/>rule match"| Outbound["Outbound"]
    Outbound -->|"Resolve google.com<br/>→ real IP"| Server([Real Server])

FakeDNS Holder (app/dns/fakedns/fake.go)

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

IP Allocation

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

    // Check cache first
    if v, ok := fkdns.domainToIP.Get(domain); ok {
        return []net.Address{v.(net.Address)}
    }

    // Generate new fake IP based on current time
    currentTimeMillis := uint64(time.Now().UnixNano() / 1e6)
    ones, bits := fkdns.ipRange.Mask.Size()
    rooms := bits - ones
    if rooms < 64 {
        currentTimeMillis %= (uint64(1) << rooms)
    }

    bigIntIP := big.NewInt(0).SetBytes(fkdns.ipRange.IP)
    bigIntIP.Add(bigIntIP, new(big.Int).SetUint64(currentTimeMillis))

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

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

Reverse Lookup

go
func (fkdns *Holder) GetDomainFromFakeDNS(ip net.Address) string {
    if !fkdns.ipRange.Contains(ip.IP()) {
        return ""  // not a fake IP
    }
    if k, ok := fkdns.domainToIP.GetKeyFromValue(ip); ok {
        return k.(string)
    }
    return ""  // fake IP but domain expired from LRU cache
}

Pool Check

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

Multi-Pool Support

HolderMulti supports separate IPv4 and IPv6 fake pools:

go
type HolderMulti struct {
    holders []*Holder  // e.g., [0]=198.18.0.0/15, [1]=fc00::/18
}

func (h *HolderMulti) GetFakeIPForDomain3(domain string, ipv4, ipv6 bool) []net.Address {
    var ret []net.Address
    for _, v := range h.holders {
        ret = append(ret, v.GetFakeIPForDomain3(domain, ipv4, ipv6)...)
    }
    return ret  // may return both IPv4 and IPv6 fake IPs
}

Integration with Dispatcher Sniffing

The Fake-IP integration in the dispatcher has three components:

1. FakeDNS Metadata Sniffer

go
// app/dispatcher/fakednssniffer.go
func newFakeDNSSniffer(ctx) (protocolSnifferWithMetadata, error) {
    return protocolSnifferWithMetadata{
        protocolSniffer: func(ctx, _ []byte) (SniffResult, error) {
            ob := session.OutboundsFromContext(ctx)
            dest := ob[len(ob)-1].Target

            if fkr0.IsIPInIPPool(dest.Address) {
                domain := fkr0.GetDomainFromFakeDNS(dest.Address)
                if domain != "" {
                    return &fakeDNSSniffResult{domain: domain}, nil
                }
            }
            return nil, common.ErrNoClue
        },
        metadataSniffer: true,   // No payload needed
        network:         net.Network_TCP,
    }
}

2. FakeDNS+Others Composite

When Fake-IP resolves the domain, but we also want content-based protocol detection:

go
// "fakedns+others" sniffer
// Returns Fake-IP domain as the domain result
// But uses content sniffing (TLS/HTTP) for protocol detection

3. Override Decision

go
func (d *DefaultDispatcher) shouldOverride(ctx, result, request, destination) bool {
    for _, p := range request.OverrideDestinationForProtocol {
        // Always override fake IPs, regardless of RouteOnly
        if fkr0.IsIPInIPPool(destination.Address) && p == "fakedns" {
            return true
        }
    }
}

In the dispatch path:

go
// After sniffing:
isFakeIP := fkr0.IsIPInIPPool(ob.Target.Address)
if sniffingRequest.RouteOnly && !isFakeIP {
    ob.RouteTarget = destination  // route-only: don't change target
} else {
    ob.Target = destination       // full override (always for fake IPs)
}

Configuration

json
{
  "dns": {
    "servers": [
      {
        "address": "fakedns",
        "domains": ["geosite:cn"]
      },
      "8.8.8.8"
    ]
  },
  "fakedns": {
    "ipPool": "198.18.0.0/15",
    "poolSize": 65535
  },
  "inbounds": [{
    "tag": "tun-in",
    "protocol": "tun",
    "sniffing": {
      "enabled": true,
      "destOverride": ["fakedns+others"]
    }
  }]
}

Key config points:

  • fakedns block configures the IP pool and LRU size
  • DNS server with "fakedns" address enables Fake-IP responses
  • Sniffing destOverride includes "fakedns" or "fakedns+others"

Lifecycle of a Fake-IP Connection

mermaid
sequenceDiagram
    participant App
    participant DNS as Xray DNS
    participant FDH as FakeDNS Holder
    participant TUN
    participant D as Dispatcher
    participant S as Sniffer
    participant R as Router
    participant O as Outbound

    App->>DNS: Query A google.com
    DNS->>FDH: GetFakeIPForDomain("google.com")
    FDH-->>DNS: 198.18.1.42
    DNS-->>App: A 198.18.1.42

    App->>TUN: TCP SYN to 198.18.1.42:443
    TUN->>D: DispatchLink(ctx, 198.18.1.42:443)
    D->>S: SniffMetadata(ctx)
    S->>FDH: IsIPInIPPool(198.18.1.42)?
    FDH-->>S: Yes
    S->>FDH: GetDomainFromFakeDNS(198.18.1.42)
    FDH-->>S: "google.com"
    S-->>D: FakeDNS result: google.com

    D->>D: Override target: google.com:443
    D->>R: PickRoute(google.com:443)
    R-->>D: outbound-proxy

    D->>O: Dispatch(google.com:443)
    O->>O: Resolve google.com → 142.250.80.14
    O->>O: Connect to 142.250.80.14:443

Implementation Notes

  1. LRU eviction: The fake IP cache is LRU-based. When the cache is full, the oldest entry is evicted. If the app still has an open connection to the evicted fake IP, the reverse lookup will fail. Size the LRU appropriately (65535 is the default).

  2. Time-based IP generation: The fake IP is derived from currentTimeMillis % poolSize. This provides distribution across the pool but can collide. The collision resolution loop handles this.

  3. Metadata-only sniffing: The Fake-IP sniffer doesn't need to read any payload bytes. It checks the destination IP against the pool before any data arrives. This makes it instant.

  4. Always full override for fake IPs: Even with routeOnly: true, fake IPs must be fully overridden (not just for routing). The fake IP has no real meaning — connecting to it would fail.

  5. IPv4 + IPv6 pools: Use HolderMulti with one IPv4 pool and one IPv6 pool. The DNS module will return the appropriate type based on the query (A vs AAAA).

  6. DNS must be hijacked: For Fake-IP to work, the application's DNS queries must reach Xray's DNS module. This typically requires:

    • TUN with dns.hijack routing rules
    • Or system DNS pointed to Xray's DNS listener

Technical analysis for re-implementation purposes.