Почему IPv6 прокси утекает через WebRTC в Chrome и как это закрыть
Содержание
- Что вообще происходит
- Почему именно IPv6
- Как выглядит утечка снаружи
- Почему прокси не спасает
- Отключаем WebRTC целиком
- Настройка политики правильно
- Отключаем IPv6 на уровне ОС
- Маршрутизация IPv6 через прокси
- /etc/redsocks.conf
- Кейс: корпоративный ноутбук в гостевой сети
- Кейс: анонимный парсинг
- Кейс: тестирование геолокации
- Firefox и Safari — не лучше
- Проверка после настройки
- Что делать прямо сейчас
Что вообще происходит
WebRTC в Chrome — это не один протокол, а зоопарк из ICE, STUN, TURN, DTLS и SRTP. Чтобы установить peer-to-peer соединение, браузеру нужно узнать свой публичный адрес. Он опрашивает STUN-серверы, те отвечают: «твой адрес такой-то». И вот тут начинается веселье.
Проблема в том, что Chrome собирает ICE-кандидатов со всех сетевых интерфейсов. Не только с того, через который идёт прокси. Если у машины есть IPv6-адрес — глобальный или через туннель — он попадёт в список кандидатов. Даже если весь остальной трафик идёт через SOCKS5.
STUN-запрос уходит по UDP. SOCKS5 поддерживает UDP ASSOCIATE, но многие клиенты и библиотеки это игнорируют. Chrome в том числе — он шлёт STUN напрямую, минуя прокси, если системный прокси не настроен на уровне ОС.
Почему именно IPv6
С IPv4 проще: NAT. Домашний роутер подменяет адрес, и STUN видит только внешний IP шлюза. Утечка есть, но она показывает адрес провайдера, а не машины.
С IPv6 NAT нет по определению. Каждое устройство получает глобально маршрутизируемый адрес. `/64` на домашнюю сеть — норма. И этот адрес уходит в STUN-запрос как есть. Плюс Chrome добавляет host-кандидатов — локальные адреса интерфейсов, включая link-local `fe80::/10` и уникальные локальные `fc00::/7`.
STUN-сервер видит запрос с source-адресом. Если IPv6 идёт мимо прокси — утечка. Причём mDNS-обфускация, которую Chrome включает для host-кандидатов, работает только для приватных IPv4 и link-local. Глобальный IPv6 утекает открытым текстом.
Как выглядит утечка снаружи
Любой сайт может запустить RTCPeerConnection и собрать кандидатов. Код на JavaScript — пять строк:
```javascript
const pc = new RTCPeerConnection({iceServers: [{urls: 'stun:stun.l.google.com:19302'}]});
pc.createDataChannel('x');
pc.onicecandidate = e => console.log(e.candidate?.candidate);
pc.createOffer().then(o => pc.setLocalDescription(o));
```
Через секунду в консоли появятся строки вида `candidate:842163049 1 udp 1677729535 2a00:1234:5678::1 54321 typ srflx`. Адрес `2a00:1234:5678::1` — ваш реальный. Прокси тут ни при чём.
Проверить можно на `browserleaks.com/webrtc` или `ipleak.net`. Если видите IPv6, которого не должно быть — утечка.
Почему прокси не спасает
Корпоративный прокси или расширение вроде FoxyProxy настраивают HTTP/HTTPS трафик. SOCKS5 чуть шире. Но WebRTC использует UDP. HTTP-прокси UDP не умеет вообще. SOCKS5 умеет через UDP ASSOCIATE, но:
1. Chrome не отправляет STUN через SOCKS5 UDP ASSOCIATE — это открытый баг с 2015 года.
2. Расширения-прокси не перехватывают WebRTC API.
3. Системный прокси в Windows/macOS на WebRTC не влияет.
Итого: браузер шлёт UDP-пакеты напрямую в сеть. Если IPv6-маршрут есть — пакет уйдёт.
Отключаем WebRTC целиком
Самый тупой и надёжный способ. В Chrome: `chrome://flags/#disable-webrtc` → Enabled. Перезапуск. Всё, WebRTC мёртв. Минус — видеозвонки в Google Meet, Discord в браузере, Jitsi перестанут работать.
Политикой через реестр или `policy.json`:
```json
{
"WebRtcIPHandlingPolicy": "disable_non_proxied_udp",
"WebRtcUdpPortRange": ""
}
```
`disable_non_proxied_udp` — компромисс. WebRTC работает, но только через прокси. Если прокси UDP не поддерживает — соединение не установится. Для большинства задач это ок.
Настройка политики правильно
`WebRtcIPHandlingPolicy` принимает четыре значения. Разберём:
| Значение | Поведение | Утечка IPv6 |
|---|---|---|
| `default` | Все интерфейсы | Да |
| `default_public_and_private_interfaces` | Все, но маскирует приватные | Частично |
| `default_public_interface_only` | Только публичный | Нет, если маршрут один |
| `disable_non_proxied_udp` | Только через прокси | Нет |
Для Linux — файл `/etc/opt/chrome/policies/managed/webrtc.json`. Для Windows — `HKLM\SOFTWARE\Policies\Google\Chrome\WebRtcIPHandlingPolicy` = `disable_non_proxied_udp`. Для macOS — `defaults write com.google.Chrome WebRtcIPHandlingPolicy -string disable_non_proxied_udp`.
Проверить: `chrome://policy`. Политика должна быть в списке с source `Platform`.
Отключаем IPv6 на уровне ОС
Радикально, но работает. Linux:
```bash
sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
```
Постоянно — в `/etc/sysctl.d/99-no-ipv6.conf`. Windows: снять галочку в свойствах адаптера или `Disable-NetAdapterBinding -Name "Ethernet" -ComponentID ms_tcpip6`.
Минус очевиден: IPv6-only ресурсы станут недоступны. Google, Cloudflare, Hetzner давно отдают AAAA. Часть CDN на IPv6 быстрее. Но если задача — не светить адрес, это самый простой путь.
Маршрутизация IPv6 через прокси
Если IPv6 нужен, но утечка недопустима — маршрутизируем весь IPv6 через туннель. SOCKS5 с IPv6 в lexic.ml работает через `udp associate`. Настраиваем системно через tun2socks или аналоги.
Пример с `redsocks` для TCP и отдельный UDP-форвардер:
```bash
/etc/redsocks.conf
redsocks {
local_ip = 127.0.0.1;
local_port = 12345;
ip = 2001:db8::1;
port = 1080;
type = socks5;
}
```
Плюс iptables/ip6tables:
```bash
ip6tables -t nat -A OUTPUT -p udp --dport 19302 -j REDIRECT --to-port 12345
```
Тут важно: STUN по умолчанию на порту 3478 и 19302 (Google). Блокируем или редиректим. Если редиректим — STUN-сервер увидит адрес прокси, что нам и нужно.
Кейс: корпоративный ноутбук в гостевой сети
Инженер подключается к клиентскому Wi-Fi с IPv6 от провайдера. Прокси настроен в браузере. Заходит на внутренний портал — тот палит его реальный IPv6 через WebRTC и логирует. Причина: политика `default`, IPv6-маршрут есть, STUN ушёл напрямую на `stun.l.google.com:19302`. Решение: развернули GPO с `WebRtcIPHandlingPolicy=disable_non_proxied_udp` на 400 машин. Через сутки проверили — утечек нет. Побочка: часть сотрудников не смогла звонить в Teams через браузер, перешли на десктопный клиент.
Кейс: анонимный парсинг
Сервис парсит сайты через пул IPv6-прокси. Каждый воркер — отдельный Chrome в Docker. Через неделю заметили: часть сайтов блокирует по реальному адресу хоста, а не прокси. Причина: Chrome в контейнере видел `eth0` с глобальным IPv6 (Docker с `--ipv6`), слал host-кандидатов. Решение: в `docker-compose.yml` добавили `sysctls: net.ipv6.conf.all.disable_ipv6=1` для контейнеров. Плюс флаг `--disable-features=WebRtcHideLocalIpsWithMdns` убрали, оставили дефолтную mDNS-обфускацию. Утечки прекратились, скорость парсинга выросла на 12% — меньше таймаутов на STUN.
Кейс: тестирование геолокации
QA-команда проверяет выдачу для разных стран. Используют прокси, но результаты плавают. Причина: WebRTC отдавал реальный IPv6, сайт определял страну по нему, а не по прокси. Разбирались через Wireshark: видели STUN Binding Request с source-адресом офисной сети. Решение: подняли локальный STUN-сервер, который отвечает фиктивным адресом, и завернули весь UDP:3478/19302 на него через nftables. Дополнительно — `WebRtcIPHandlingPolicy=default_public_interface_only`. Тесты стали воспроизводимыми.
Firefox и Safari — не лучше
Firefox исторически утекает меньше: `media.peerconnection.ice.default_address_only` и `media.peerconnection.ice.no_host` в `about:config`. Но по умолчанию обе выключены. Safari на macOS отдаёт IPv6 неохотнее, но всё равно может. Не думайте, что смена браузера решает проблему.
В Firefox политику можно задать через `policies.json`:
```json
{
"policies": {
"Preferences": {
"media.peerconnection.ice.no_host": true,
"media.peerconnection.ice.default_address_only": true
}
}
}
```
Положите в `/etc/firefox/policies/policies.json` на Linux или в `distribution/` рядом с бинарём на Windows.
Проверка после настройки
Не верьте на слово. Открывайте `chrome://webrtc-internals` — там видно все ICE-кандидаты в реальном времени. Если в списке есть IPv6, которого быть не должно — настройка не сработала.
Второй способ — внешний сервис. `browserleaks.com/webrtc` покажет и публичный, и локальные адреса. Запустите тест трижды: до настройки, после, и после перезапуска браузера. Политики применяются при старте, не на лету.
Третий — tcpdump на интерфейсе:
```bash
sudo tcpdump -i any -n 'udp port 3478 or udp port 19302'
```
Если видите пакеты с вашего реального адреса — утечка жива. Если пусто — либо всё завернуто, либо WebRTC отключён.
Что делать прямо сейчас
Чек-лист без воды:
1. Откройте `browserleaks.com/webrtc`. Запишите, что видно.
2. Если IPv6 совпадает с реальным — ставьте `disable_non_proxied_udp`.
3. Перезапустите браузер. Проверьте снова.
4. Если не помогло — отключайте IPv6 на интерфейсе.
5. Для парка машин — GPO или MDM-профиль.
6. Для контейнеров — `disable_ipv6=1` в sysctl.
7. Для тестов — локальный STUN с подменой.
Утечка WebRTC через IPv6 — не баг, а фича архитектуры. Chrome честно пытается установить соединение кратчайшим путём. Прокси для него — деталь реализации HTTP-стека, не более. Пока это не осознать, любые настройки будут костылём.