Подробный разбор Fake DNS
Fake DNS (FakeDNS) — это механизм, который назначает временные, уникальные IP-адреса из приватного пула доменным именам. Когда впоследствии устанавливается соединение с одним из этих поддельных IP, система выполняет обратное сопоставление к исходному домену. Это позволяет настройкам прозрачного прокси, где DNS-запросы приходят до фактического соединения, выполнять маршрутизацию на основе доменов даже для непрозрачного трафика с IP-назначением.
Обзор архитектуры
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).
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
}Инициализация:
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, используя подход на основе времени:
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}
}Алгоритм:
- Возвращает кешированный IP, если домен был замечен ранее
- Использует текущее время в миллисекундах по модулю размера пула как начальное смещение
- Выполняет линейный поиск вперёд до нахождения неиспользуемого IP
- Переходит к началу пула при достижении конца
- Сохраняет двунаправленное соответствие в LRU-кеше
Обратный поиск
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)
}Проверка принадлежности к пулу
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).
type HolderMulti struct {
holders []*Holder
config *FakeDnsPoolMulti
}Все методы делегируются каждому holder:
GetFakeIPForDomain()возвращает IP из всех пуловGetFakeIPForDomain3()фильтрует по включённости IPv4/IPv6 для каждого holderGetDomainFromFakeDNS()возвращает первое совпадение среди пуловIsIPInIPPool()возвращает true, если IP содержится в любом пуле
Фильтрация IPv4 и IPv6
Метод GetFakeIPForDomain3() у Holder проверяет, соответствует ли семейство IP пула запросу:
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-серверов:
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:
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() реализует составной сниффер:
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() проверяет:
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:
{
"fakeDns": {
"ipPool": "198.18.0.0/15",
"poolSize": 65535
}
}Или для двойного стека:
{
"fakeDns": {
"pools": [
{ "ipPool": "198.18.0.0/15", "poolSize": 65535 },
{ "ipPool": "fc00::/18", "poolSize": 65535 }
]
}
}DNS-сервер затем ссылается на него:
{
"dns": {
"servers": ["fakedns"]
}
}Полный поток данных
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()для предотвращения проблем с трекерами.