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() для предотвращения проблем с трекерами.

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