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 connectsArchitecture
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)
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
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
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
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:
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
// 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:
// "fakedns+others" sniffer
// Returns Fake-IP domain as the domain result
// But uses content sniffing (TLS/HTTP) for protocol detection3. Override Decision
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:
// 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
{
"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:
fakednsblock configures the IP pool and LRU size- DNS server with
"fakedns"address enables Fake-IP responses - Sniffing
destOverrideincludes"fakedns"or"fakedns+others"
Lifecycle of a Fake-IP Connection
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:443Implementation Notes
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).
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.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.
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.IPv4 + IPv6 pools: Use
HolderMultiwith one IPv4 pool and one IPv6 pool. The DNS module will return the appropriate type based on the query (A vs AAAA).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.hijackrouting rules - Or system DNS pointed to Xray's DNS listener
- TUN with