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

Почему IPv6-прокси «плывёт» при dual-stack: как браузер выбирает стек и почему утечка реального IP происходит через Happy Eyeballs

Dual-stack — это когда у машины одновременно есть IPv4 и IPv6. Звучит как бонус. На практике — источник половины проблем с прокси. Браузер видит два маршрута до одного и того же сайта и решает сам, каким идти. Ваш прокси в этот момент может отдыхать.

Проблема глубже, чем «забыли отключить IPv6». Даже если прокси настроен правильно, клиент может уйти напрямую по IPv6 — и вы об этом не узнаете, пока не проверите заголовки на стороне сервера.

Что вообще происходит при dual-stack

У клиента два адреса. У сервера — тоже, если он публикует AAAA-запись. DNS отдаёт оба набора. Клиент получает список из N адресов и должен выбрать один. RFC 8305 описывает алгоритм Happy Eyeballs v2, который эту задачу решает.

До 2017 года использовалась версия v1 (RFC 6555), где задержка между попытками составляла 300 мс. Во второй версии её снизили до 250 мс. Логика простая: запускаем соединение по IPv6, ждём 250 мс, если не подключились — параллельно стартуем IPv4. Кто первый ответил, того и тапки.

Именно этот алгоритм ломает прокси-схемы, которые рассчитывают только на IPv4. Браузер не спрашивает прокси, какой стек использовать. Он просто гоняет два соединения параллельно.

Как браузер выбирает маршрут

Chrome, Firefox, Safari — все реализуют Happy Eyeballs. Отличия в деталях. Chrome исторически агрессивнее к IPv6: если AAAA-запись резолвится, он стартует IPv6-соединение первым. Firefox тоже, но с чуть большей задержкой fallback.

Ключевой момент: прокси-настройки применяются к протоколу HTTP или SOCKS, а не к сетевому стеку. Если браузер настроен на SOCKS5-прокси по IPv4-адресу, а целевой сайт имеет AAAA-запись, соединение всё равно пойдёт через прокси — но уже по IPv6-транспорту внутри туннеля. Или не пойдёт, если прокси не умеет IPv6.

Вот тут начинается зоопарк. Зависит от того, как именно настроен прокси: HTTP CONNECT, SOCKS5, PAC-файл, системный прокси.

Где именно утекает IP

Утечка происходит в трёх типовых местах.

Первое — PAC-файл с ошибкой в директиве. Например, `if (isInNet(host, "10.0.0.0", "255.0.0.0")) return "DIRECT"` — если хост резолвится в IPv6-адрес, `isInNet` не сработает, и трафик уйдёт напрямую.

Второе — WebRTC. Он не подчиняется прокси-настройкам браузера и использует STUN-запросы напрямую. При dual-stack WebRTC может отдать реальный IPv6-адрес даже при активном прокси. Chrome с 2015 года частично это починил, но утечки через IPv6-кандидатов встречаются до сих пор.

Третье — DNS. Если браузер резолвит имя через системный резолвер (а не через прокси), он получает AAAA-запись и пытается идти по ней напрямую. Особенно если прокси-сервер не поддерживает IPv6 и молча отваливается.

Проверка: что реально видит сервер

Самый честный способ — посмотреть, с какого адреса пришёл запрос. Простой эндпоинт на Python:

```python

from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):

def do_GET(self):

ip = self.client_address[0]

body = f"client_ip={ip}\n".encode()

self.send_response(200)

self.send_header("Content-Type", "text/plain")

self.send_header("Content-Length", str(len(body)))

self.end_headers()

self.wfile.write(body)

HTTPServer(("::", 8080), Handler).serve_forever()

```

Запустите на сервере с публичным IPv6 и IPv4. Затем с клиента через прокси:

```bash

curl -6 --proxy socks5h://proxy.example:1080 http://check.example:8080/

curl -4 --proxy socks5h://proxy.example:1080 http://check.example:8080/

```

`--proxy socks5h` — важная деталь. Буква `h` означает, что DNS-резолвинг делает прокси, а не клиент. Без неё браузер (и curl) резолвит имя локально, получает AAAA-запись и может уйти в обход. Разница между `socks5://` и `socks5h://` — это как раз та дыра, через которую утекает реальный IP.

Если в ответе видите свой настоящий адрес вместо адреса прокси — утечка. Если видите адрес прокси, но IPv4 вместо IPv6 (или наоборот) — прокси работает, но не в том стеке, который вы ждали.

Кейс: браузер игнорирует SOCKS5-прокси по IPv6

Пример: сервер на nginx 1.24 с конфигом, где `listen [::]:443 ssl;` и `listen 443 ssl;` идут параллельно. Клиент за SOCKS5-прокси, который слушает только IPv4 (`127.0.0.1:1080`). В логах nginx видим: 70% запросов пришли с IPv6-адресов клиентов, 30% — с IPv4-адресов прокси.

Причина: браузер резолвит домен через системный DNS, получает и A, и AAAA. Happy Eyeballs стартует IPv6-соединение первым. Прокси не участвует, потому что PAC-файл вернул DIRECT для этого домена — ошибка в исключении.

Решение: переписать PAC так, чтобы IPv6-хосты тоже шли через прокси. Или жёстче — отключить IPv6 на клиенте через sysctl:

```bash

sysctl -w net.ipv6.conf.all.disable_ipv6=1

sysctl -w net.ipv6.conf.default.disable_ipv6=1

```

Костыль, но работает. Правильнее — настроить прокси, который умеет оба стека, и убедиться, что PAC не делает исключений по IPv6-диапазонам.

Кейс: WebRTC отдаёт IPv6 в обход прокси

Пример: Chrome 118, SOCKS5-прокси настроен, HTTPS-трафик идёт через него. Проверка на `browserleaks.com/webrtc` показывает реальный IPv6-адрес клиента. Проблема в том, что WebRTC использует собственный ICE-стек и STUN-запросы, которые не проходят через SOCKS5.

Chrome с версии 110 частично ограничивает это через политику `WebRtcIPHandlingPolicy`. Значение `disable_non_proxied_udp` блокирует UDP вне прокси. В enterprise-развёртываниях это настраивается через GPO или `master_preferences`.

Без этого WebRTC отдаёт локальные и публичные адреса — и IPv4, и IPv6. Прокси тут ни при чём. Это архитектурная особенность WebRTC, а не баг браузера.

Кейс: прокси-сервер без IPv6 ломает соединение

Пример: клиент с dual-stack, SOCKS5-прокси на IPv4-only сервере. Целевой сайт имеет AAAA-запись. Клиент отправляет SOCKS5-запрос с доменным именем (socks5h). Прокси резолвит имя, получает AAAA, пытается подключиться по IPv6 — и падает, потому что у самого прокси IPv6 нет.

Хреново, когда это происходит молча. Клиент видит таймаут, браузер делает fallback на IPv4, но уже потерял 250–500 мс. При активном сёрфинге это заметно.

Решение: либо прокси с поддержкой IPv6 (например, сервисы вроде lexic.ml, где IPv6 — основной стек с 2015 года), либо принудительный резолвинг только A-записей на стороне прокси. Второе — костыль, который ломает сайты, доступные только по IPv6.

Настройка прокси, который не течёт

Если вы контролируете прокси-сервер, вот минимальный набор проверок.

SOCKS5 должен поддерживать и IPv4, и IPv6. Проверяется просто: `ss -tlnp | grep 1080` покажет, слушает ли он `0.0.0.0` или `::`. Если только IPv4 — IPv6-клиенты будут страдать.

HTTP-прокси (CONNECT) должен корректно обрабатывать заголовок `Host` с IPv6-литералом в квадратных скобках: `Host: [2001:db8::1]:443`. Многие самописные прокси на этом падают.

DNS внутри прокси должен резолвить оба типа записей. Если используете `getaddrinfo` с `AF_UNSPEC` — всё в порядке. Если хардкод `AF_INET` — IPv6-сайты недоступны.

Что делать клиенту

Простейшая диагностика — три команды.

```bash

curl -s https://ifconfig.co

curl -6 -s https://ifconfig.co

curl -4 -s https://ifconfig.co

```

Первая покажет, какой стек выбрал curl по умолчанию (обычно IPv4, если оба доступны, но зависит от версии curl и системы). Вторая и третья — принудительно по стеку. Если адреса разные и один из них ваш реальный — где-то утечка.

Для браузера — расширение, которое показывает реальные заголовки и WebRTC-кандидатов. Например, uBlock Origin в режиме отладки или отдельные чекеры. Главное — проверить оба стека, а не только IPv4.

Итог

Dual-stack не зло. Зло — считать, что прокси-настройки покрывают весь трафик. Happy Eyeballs работает на уровне приложения и не спрашивает разрешения у вашего SOCKS5. WebRTC живёт своей жизнью. DNS-резолвинг может происходить до того, как прокси вообще включится в процесс.

Правило простое: если у клиента есть IPv6, проверяйте утечки по IPv6 отдельно от IPv4. Один чекер на IPv4 ничего не докажет. Нужны оба.

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