Skip to content

Интеграция Fake-IP

Fake-IP — это техника, при которой DNS-запросы возвращают временные, поддельные IP-адреса из зарезервированного пула. Когда приходит трафик к поддельному IP, система находит исходный домен и маршрутизирует на основе доменного имени, а не (бессмысленного) IP-адреса.

Зачем нужен Fake-IP?

Без Fake-IP прозрачное проксирование на основе TUN сталкивается с дилеммой:

Приложение → DNS-запрос "google.com" → Настоящий DNS → 142.250.80.14
Приложение → Подключение к 142.250.80.14 → TUN → Xray
  → Xray видит IP 142.250.80.14, но не знает домен "google.com"
  → Невозможна маршрутизация на основе доменов!

С Fake-IP:

Приложение → DNS-запрос "google.com" → Fake DNS → 198.18.0.42 (поддельный)
Приложение → Подключение к 198.18.0.42 → TUN → Xray
  → Xray определяет, что 198.18.0.42 находится в поддельном пуле
  → Поиск: 198.18.0.42 → "google.com"
  → Маршрутизация на основе домена "google.com"
  → Исходящее соединение разрешает настоящий IP и подключается

Архитектура

mermaid
flowchart TB
    App([Приложение]) -->|"DNS: google.com?"| DNS["Модуль DNS Xray"]
    DNS -->|"Возврат 198.18.0.42<br/>(поддельный IP)"| App

    App -->|"Подключение к<br/>198.18.0.42"| TUN["TUN-интерфейс"]
    TUN -->|"dest=198.18.0.42"| Handler["Обработчик TUN"]
    Handler -->|"DispatchLink(ctx,<br/>198.18.0.42:443)"| Dispatcher["Диспетчер"]

    Dispatcher --> Sniff["Сниффер"]
    Sniff --> FakeDNS["Сниффер FakeDNS"]
    FakeDNS -->|"198.18.0.42 в пуле?<br/>→ google.com"| Override["Переопределение dest:<br/>google.com:443"]
    Override --> Router["Маршрутизатор"]
    Router -->|"совпадение правила<br/>по домену"| Outbound["Исходящее соединение"]
    Outbound -->|"Разрешение google.com<br/>→ настоящий IP"| 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

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

Обратный поиск

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
}

Проверка принадлежности к пулу

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

Поддержка нескольких пулов

HolderMulti поддерживает раздельные пулы поддельных IP для IPv4 и IPv6:

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
}

Интеграция со сниффингом диспетчера

Интеграция Fake-IP в диспетчере состоит из трёх компонентов:

1. Сниффер метаданных FakeDNS

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

Когда Fake-IP разрешает домен, но также требуется определение протокола на основе содержимого:

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

3. Решение о переопределении

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

В пути диспетчеризации:

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

Конфигурация

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

Ключевые моменты конфигурации:

  • Блок fakedns настраивает IP-пул и размер LRU
  • DNS-сервер с адресом "fakedns" включает ответы Fake-IP
  • В сниффинге destOverride указывается "fakedns" или "fakedns+others"

Жизненный цикл Fake-IP-соединения

mermaid
sequenceDiagram
    participant App as Приложение
    participant DNS as DNS Xray
    participant FDH as FakeDNS Holder
    participant TUN
    participant D as Диспетчер
    participant S as Сниффер
    participant R as Маршрутизатор
    participant O as Исходящее соединение

    App->>DNS: Запрос 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 к 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: Да
    S->>FDH: GetDomainFromFakeDNS(198.18.1.42)
    FDH-->>S: "google.com"
    S-->>D: Результат FakeDNS: google.com

    D->>D: Переопределение цели: google.com:443
    D->>R: PickRoute(google.com:443)
    R-->>D: outbound-proxy

    D->>O: Dispatch(google.com:443)
    O->>O: Разрешение google.com → 142.250.80.14
    O->>O: Подключение к 142.250.80.14:443

Замечания по реализации

  1. Вытеснение LRU: Кеш поддельных IP основан на LRU. Когда кеш заполнен, самая старая запись вытесняется. Если приложение всё ещё имеет открытое соединение с вытесненным поддельным IP, обратный поиск не сработает. Размер LRU следует подбирать правильно (по умолчанию 65535).

  2. Генерация IP на основе времени: Поддельный IP вычисляется как currentTimeMillis % poolSize. Это обеспечивает распределение по пулу, но может вызвать коллизии. Цикл разрешения коллизий обрабатывает такие ситуации.

  3. Сниффинг только по метаданным: Сниффер Fake-IP не нуждается в чтении каких-либо байтов полезной нагрузки. Он проверяет IP назначения по пулу до прихода каких-либо данных. Это делает его мгновенным.

  4. Всегда полное переопределение для поддельных IP: Даже при routeOnly: true поддельные IP должны быть полностью переопределены (не только для маршрутизации). Поддельный IP не имеет реального значения — подключение к нему потерпит неудачу.

  5. Пулы IPv4 + IPv6: Используйте HolderMulti с одним пулом IPv4 и одним пулом IPv6. Модуль DNS будет возвращать соответствующий тип в зависимости от запроса (A или AAAA).

  6. DNS должен быть перехвачен: Чтобы Fake-IP работал, DNS-запросы приложения должны попадать в модуль DNS Xray. Обычно для этого требуется:

    • TUN с правилами маршрутизации dns.hijack
    • Или системный DNS, указывающий на DNS-слушатель Xray

Технический анализ для целей повторной реализации.