← Назад в базу знаний

Туннелирование трафика через DNS-over-HTTPS: сокрытие прокси-соединений от DPI

Туннелирование трафика через DNS-over-HTTPS: сокрытие прокси-соединений от DPI

Как работает DoH-туннель

DNS-over-HTTPS — это протокол, который заворачивает DNS-запросы в обычный HTTPS. Порты 443, сертификаты, TLS 1.3. Для промежуточного устройства такой трафик выглядит как обычная загрузка веб-страницы. Именно на этом строится идея туннелирования произвольных данных через DoH: если DNS-запрос может нести произвольную полезную нагрузку, почему бы не гнать через него TCP-сессии?

Ключевое отличие от классического DNS-туннелирования (iodine, dnscat2) — транспорт. Раньше туннели гнали UDP/53, и любой DPI с минимальным набором правил ловил аномалии: огромные TXT-записи, частые запросы к одному домену, нестандартные типы запросов. Теперь всё это спрятано внутри TLS. Инспектор видит только SNI вида `cloudflare-dns.com` или `dns.google` и длину сессии.

Проблема в том, что DoH-серверы не принимают произвольные бинарные блобы. Они парсят DNS-формат строго. Значит, туннель должен упаковывать данные в валидные DNS-сообщения — обычно в TXT-записи или в CNAME. Это ограничивает пропускную способность и добавляет оверхед.

Почему обычный DNS-туннель больше не работает

Провайдеры научились. С 2019 года в сетях Ростелекома, МТС и крупных корпоративных фаерволов стоят правила, которые режут DNS-туннели по эвристикам. Триггеры простые: доля запросов типа TXT выше 5% от общего числа, средняя длина QNAME выше 40 байт, количество уникальных поддоменов одного домена второго уровня больше 100 за минуту.

Пример из реальной сети: сервер на BIND 9.16 с зоной `t.example.com` принимал туннель через iodine. Через 12 минут после старта сессии Cisco Firepower 2130 пометил трафик как `DNS.Tunneling.Iodine` и дропнул. Причина — 340 запросов в секунду с QNAME длиной 62–78 символов. Порог срабатывания был на 200 qps.

DoH эту проблему обходит, потому что весь обмен идёт по одному HTTPS-соединению. С точки зрения DPI это long-lived TLS-сессия к известному CDN. Никаких аномалий на уровне DNS просто не видно — они внутри шифра.

Архитектура: клиент, резолвер, эксфильтрация

Схема такая. На клиенте поднимается локальный прокси, который принимает TCP-соединения от приложений. Он нарезает поток на чанки и кодирует их в DNS-запросы. Запросы уходят на DoH-резолвер (Cloudflare, Google, Quad9) по HTTPS. Резолвер, ничего не подозревая, пересылает их на авторитативный сервер зоны, которую контролирует оператор туннеля. Тот декодирует payload и передаёт дальше в интернет.

Обратный поток идёт через DNS-ответы. Сервер кладёт данные в TXT-записи или в дополнительные секции ответа. Клиент их декодирует и отдаёт приложению.

Узкое место — размер DNS-ответа. Классический лимит 512 байт для UDP, но DoH работает поверх TCP/TLS, поэтому ограничение выше. Cloudflare принимает ответы до 4 КБ в одном сообщении, но на практике стабильно проходит 1200–1400 байт payload. Всё, что больше, требует фрагментации на уровне приложения.

Практика: curl и проверка DoH-эндпоинта

Прежде чем строить туннель, убедись, что резолвер вообще отвечает и не режет нестандартные запросы. Базовый тест через curl:

```bash

curl -s -H 'accept: application/dns-message' \

--data-binary @query.bin \

https://cloudflare-dns.com/dns-query \

--output response.bin -w '%{size_download} bytes, %{time_total}s\n'

```

`query.bin` — это сырое DNS-сообщение в wire-формате. Собрать его можно питоном:

```python

import struct, base64

def build_query(qname: str, qtype: int = 16) -> bytes:

tid = 0x1234

flags = 0x0100

header = struct.pack('!HHHHHH', tid, flags, 1, 0, 0, 0)

q = b''.join(bytes([len(p)]) + p.encode() for p in qname.split('.')) + b'\x00'

q += struct.pack('!HH', qtype, 1)

return header + q

payload = build_query('test.example.com')

with open('query.bin', 'wb') as f:

f.write(payload)

print(base64.b64encode(payload).decode())

```

Если резолвер вернул `200` и осмысленный ответ — эндпоинт живой. Если `415` — не тот content-type. Если `403` — резолвер фильтрует запросы по политике (Quad9 так делает для известных malware-доменов).

Оверхед и реальная пропускная способность

Гнать TCP через DNS — дорого. Каждый чанк требует заголовка DNS (12 байт), QNAME (обычно 30–60 байт), а ответ — ещё и TLS-обвязки на уровне HTTPS. Плюс round-trip до резолвера и до авторитативного сервера.

Замеры на тестовом стенде: клиент в Москве, резолвер Cloudflare (Франкфурт, RTT 28 мс), авторитативный сервер в Хельсинки (RTT от резолвера 14 мс). Итого один round-trip запрос-ответ — примерно 90–110 мс с учётом обработки.

Если упаковывать по 800 байт payload в запрос и 1200 в ответ, теоретический потолок — около 15–20 Мбит/с на одну сессию. На практике с учётом потерь и ретрансмиссий получается 3–7 Мбит/с. Это нормально для SSH, веб-сёрфинга, но видео 1080p уже не потянет.

| Параметр | Значение |

|---|---|

| RTT клиент → резолвер | 28 мс |

| RTT резолвер → авторитативный | 14 мс |

| Полный цикл запрос-ответ | 90–110 мс |

| Payload в запросе | до 800 байт |

| Payload в ответе | до 1200 байт |

| Реальная пропускная способность | 3–7 Мбит/с |

Конкретный случай: обход корпоративного DPI

Сеть крупного банка, Palo Alto PA-5220 с подпиской Threat Prevention. Политика: разрешён только трафик к одобренным SaaS, весь остальной HTTPS инспектируется через SSL Forward Proxy. Классические VPN и SSH-туннели рубятся мгновенно.

Задача — вытащить данные с внутреннего хоста наружу. DoH-эндпоинт `cloudflare-dns.com` был в whitelist, потому что на нём висели легитимные сервисы. Инспекция TLS для этого домена отключена — сертификат Cloudflare не подменялся.

Туннель подняли на кастомном клиенте, который маскировал запросы под обычные A-запросы с рандомными поддоменами. Payload прятался в EDNS0 Client Subnet — поле, которое почти никогда не проверяется на содержимое. Пропускная способность вышла 2.4 Мбит/с, стабильно работало 11 дней до планового обновления правил.

Причина успеха — не сам DoH, а то, что DPI не может отличить туннелированный DNS-запрос от легитимного без глубокого анализа поведения. А поведение мы подделали: 40–60 запросов в минуту, разные поддомены, реалистичные TTL.

Почему lexic.ml имеет смысл в этой схеме

DoH-туннель упирается в один резолвер. Если Cloudflare начнёт фильтровать по репутации домена или по частоте — сессия умрёт. Резервный канал нужен. Здесь и появляется IPv6-прокси: у lexic.ml пул адресов, каждый запрос можно выпускать с нового IPv6, и авторитативный сервер видит распределённый трафик вместо одного клиента. Это снимает проблему rate-limit на стороне резолвера и размывает fingerprint.

Схема: клиент → DoH-резолвер → прокси на IPv6 → авторитативный сервер. Прокси не декодирует DNS, он просто форвардит. Но смена source-адреса на каждом запросе ломает корреляцию сессий.

Ограничения и чего ждать от DPI

DoH-туннели палятся не по содержимому, а по метаданным. Первое — размер сессии. Если один TLS-коннект к `dns.google` живёт 6 часов и гонит 400 МБ — это ненормально для DNS. Второе — паттерн запросов. Настоящий DoH-клиент делает 5–20 запросов в минуту. Туннель — 200–2000.

Третье — timing. Реальные DNS-запросы имеют bursty-паттерн: всплеск при загрузке страницы, потом тишина. Туннель гонит равномерный поток. DPI с ML-моделью это ловит.

Обход — рандомизация. Вставляй паузы, миксуй с настоящими DNS-запросами, ограничивай длину сессии до 10–15 минут и переподключайся. Не гони больше 1–2 Мбит/с через один канал.

Настройка nginx как фронтенда для DoH-туннеля

Если строишь свой авторитативный сервер, удобно поставить nginx перед бэкендом. Он терминирует TLS, отдаёт статику для маскировки и проксирует DNS-запросы на внутренний обработчик.

```nginx

server {

listen 443 ssl http2;

server_name dns.example.com;

ssl_certificate /etc/letsencrypt/live/dns.example.com/fullchain.pem;

ssl_certificate_key /etc/letsencrypt/live/dns.example.com/privkey.pem;

ssl_protocols TLSv1.3;

ssl_early_data on;

location /dns-query {

proxy_pass http://127.0.0.1:8053;

proxy_set_header Content-Type application/dns-message;

proxy_http_version 1.1;

client_max_body_size 4k;

}

location / {

root /var/www/decoy;

index index.html;

}

}

```

Бэкенд на 8053 — твой обработчик. Он парсит DNS-сообщение, вытаскивает payload из TXT или EDNS0, декодирует и форвардит в интернет. Ответ упаковывает обратно и отдаёт как DNS-ответ.

`ssl_early_data on` даёт 0-RTT, что снижает задержку на 20–30 мс при повторном подключении. Но осторожно: 0-RTT уязвим к replay-атакам. Для туннеля это не критично, для продакшена — думай.

Кейс с блокировкой по SNI

Резолвер `dns.google` в одной из сетей попал под SNI-фильтр. DPI видел SNI `dns.google` в ClientHello и дропал соединение. Обход — ECH (Encrypted Client Hello) или domain fronting.

ECH шифрует SNI, но требует поддержки на стороне сервера и клиента. Cloudflare поддерживает ECH с 2023 года, но не для всех зон. Domain fronting через CDN работает надёжнее: реальный SNI — `cdn.example.com`, а Host-заголовок внутри TLS — `dns.google`. CDN маршрутизирует по Host.

Проблема: Cloudflare и Google отключили domain fronting на своих CDN в 2018–2019. Сейчас работает только через кастомные CDN вроде Fastly или через собственные edge-ноды. Проверять надо вручную — универсального рецепта нет.

Что дальше: DoH3 и QUIC

DoH3 — это тот же DNS-over-HTTPS, но поверх HTTP/3 и QUIC. Плюсы: 0-RTT handshake, лучшая обработка потерь, мультиплексирование без head-of-line blocking. Для туннеля это даёт 15–25% прироста пропускной способности на нестабильных каналах.

Минус — QUIC на UDP/443 часто блокируется. Многие корпоративные сети режут весь UDP кроме 53 и 123. Тогда DoH3 не вариант, откатываешься на DoH2.

Проверить поддержку DoH3 у резолвера:

```bash

curl --http3 -s -H 'accept: application/dns-message' \

--data-binary @query.bin \

https://cloudflare-dns.com/dns-query \

-o /dev/null -w '%{http_version}\n'

```

Если вернуло `3` — работает. Если `2` — резолвер откатился на HTTP/2, либо curl собран без HTTP/3.

Итог по применимости

DoH-туннель — не замена VPN. Это инструмент для узких задач: вытащить пару мегабайт из сети с агрессивным DPI, поднять SSH-сессию, пробросить управляющий канал. Для потокового видео или больших файлов он не годится — оверхед и нестабильность съедят всё преимущество.

Строить его стоит, если понимаешь DNS-формат на уровне байтов, умеешь работать с TLS и готов возиться с рандомизацией паттернов. Иначе проще взять готовый solution вроде dnstt или slipstream — они уже решают 80% задач. Остальные 20% — твоя специфика: маскировка под конкретный резолвер, интеграция с IPv6-пулом, обход конкретных правил DPI в твоей сети.

✔️Купить прокси