Skip to content

Подробный разбор Fake DNS

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

Обзор архитектуры

mermaid
flowchart LR
    subgraph Разрешение DNS
        A[DNS-запрос клиента для example.com] --> B[FakeDNSServer]
        B --> C[Holder.GetFakeIPForDomain]
        C --> D[Возврат 198.18.0.42]
    end

    subgraph Фаза соединения
        E[Клиент подключается к 198.18.0.42:443] --> F[Сниффинг диспетчера]
        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).

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
}

Инициализация:

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

Размер LRU должен быть строго меньше размера подсети (проверяется через log2(lruSize) < rooms). Пул по умолчанию — 198.18.0.0/15 с 65535 записями.

Алгоритм выделения IP

Метод GetFakeIPForDomain() выделяет IP, используя подход на основе времени:

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

Алгоритм:

  1. Возвращает кешированный IP, если домен был замечен ранее
  2. Использует текущее время в миллисекундах по модулю размера пула как начальное смещение
  3. Выполняет линейный поиск вперёд до нахождения неиспользуемого IP
  4. Переходит к началу пула при достижении конца
  5. Сохраняет двунаправленное соответствие в LRU-кеше

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

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

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

go
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).

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

Все методы делегируются каждому holder:

  • GetFakeIPForDomain() возвращает IP из всех пулов
  • GetFakeIPForDomain3() фильтрует по включённости IPv4/IPv6 для каждого holder
  • GetDomainFromFakeDNS() возвращает первое совпадение среди пулов
  • IsIPInIPPool() возвращает true, если IP содержится в любом пуле

Фильтрация IPv4 и IPv6

Метод GetFakeIPForDomain3() у Holder проверяет, соответствует ли семейство IP пула запросу:

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

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-серверов:

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
}

TTL всегда равен 1 секунде, чтобы предотвратить кеширование поддельных IP downstream.

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

Файл: app/dispatcher/fakednssniffer.go

Когда сниффинг включён с "fakedns" в destOverride, диспетчер создаёт 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
}

Это сниффер метаданных — он работает без инспекции байтов полезной нагрузки. Он ищет IP назначения в движке fake DNS и возвращает сопоставленный домен.

Режим fakedns+others

Файл: app/dispatcher/fakednssniffer.go

Функция newFakeDNSThenOthers() реализует составной сниффер:

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

Этот режим обрабатывает случай, когда поддельный IP был назначен, но LRU вытеснил соответствие. Он откатывается к сниффингу TLS SNI или HTTP Host для восстановления домена.

Переопределение назначения в диспетчере

В app/dispatcher/default.go метод shouldOverride() проверяет:

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

Когда адрес назначения является поддельным IP и "fakedns" указан в списке переопределений, обнаруженный домен заменяет поддельный IP назначения независимо от того, какой сниффер нашёл домен.

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

Движок FakeDNS настраивается на верхнем уровне конфигурации Xray:

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

Или для двойного стека:

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

DNS-сервер затем ссылается на него:

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

Полный поток данных

mermaid
sequenceDiagram
    participant App as Приложение
    participant DNS as DNS Xray
    participant FakeDNS as FakeDNS Holder
    participant Disp as Диспетчер
    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. Ядро создаёт соответствующий вариант в зависимости от того, содержит ли конфигурация один пул или несколько.

  • FakeDNS добавляется в начало (не в конец) списка приложений конфигурации ядра: 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-трафик явно исключён из переопределения fake DNS в shouldOverride() для предотвращения проблем с трекерами.

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