Почему IPv6-прокси «протекает» через WebRTC в Chromium: механизм утечки и точечные флаги для её устранения
Содержание
- Как устроен ICE и где именно течёт
- Почему IPv6 хуже IPv4 в этой истории
- Флаг `WebRtcIPHandlingPolicy` — что он реально делает
- Политика через реестр и enterprise-конфиг
- `WebRtcAllowLegacyTLSProtocols` и другие тонкости
- Прокси-сервер: SOCKS5 с UDP ASSOCIATE
- Реальный кейс: корпоративный терминал и утечка префикса
- Кейс: Firefox ведёт себя иначе
- Кейс: Electron-приложение тащит утечку в прод
- Проверка: как убедиться, что утечки нет
- Что делать, если прокси не поддерживает UDP
- Итоговая конфигурация для Chromium
Chromium при старте WebRTC-соединения собирает ICE-кандидатов. Это не один адрес, а набор: host-кандидаты со всех сетевых интерфейсов, srflx через STUN, relay через TURN. И вот здесь начинается веселье. Если ваш SOCKS5 или HTTP-прокси настроен только для HTTP-трафика, WebRTC идёт мимо него. UDP-пакеты уходят напрямую с реального интерфейса.
С IPv4 это уже неприятно. С IPv6 — катастрофа. Потому что IPv6-адреса почти всегда глобально маршрутизируемы. Нет NAT. Нет маскировки. Ваш `2001:db8:abcd::1` виден собеседнику как есть, вместе с префиксом провайдера.
Как устроен ICE и где именно течёт
ICE (Interactive Connectivity Establishment, RFC 8445) собирает кандидатов в три этапа. Сначала host-кандидаты — прямое перечисление адресов с интерфейсов. Потом srflx — через STUN-сервер узнаём внешний адрес. Потом relay — через TURN.
Host-кандидаты собираются через `getifaddrs()` на POSIX или `GetAdaptersAddresses()` на Windows. Chromium вызывает `rtc::NetworkManager::GetNetworks()`. Тот перебирает все интерфейсы и возвращает каждый адрес. IPv6-адреса попадают в список наравне с IPv4.
Дальше `BasicPortAllocator` создаёт UDP-сокеты на каждом интерфейсе. Пакеты STUN Binding Request уходят с того адреса, который попал в кандидата. Прокси тут не участвует. SOCKS5 работает на уровне TCP, а WebRTC по умолчанию гоняет UDP.
Проверить просто. Откройте `chrome://webrtc-internals`, запустите любой звонок. В разделе ICE candidate grid увидите строки вида:
```
candidate:1 1 udp 2122260223 2001:db8:abcd::1 54321 typ host generation 0
```
Это ваш реальный IPv6. Если прокси был настроен — он всё равно протёк.
Почему IPv6 хуже IPv4 в этой истории
С IPv4 host-кандидат часто бесполезен: адрес `192.168.1.5` за NAT ничего не говорит собеседнику. Соединение установится через srflx или relay. Утечка есть, но она показывает внутренний адрес — не критично.
IPv6 ломает эту логику. Адрес `2001:db8:abcd::1` глобально достижим. ICE попробует установить прямое соединение по нему. Оно может даже установиться — если у обеих сторон есть IPv6 без файрвола. И тогда весь медиапоток идёт напрямую, минуя прокси, минуя всё.
Ещё нюанс: Chromium по умолчанию предпочитает IPv6 при выборе candidate pair (RFC 6724, правило предпочтения). Если host-кандидат IPv6 доступен — он выиграет у relay-кандидата IPv4. Приоритет host-кандидата 2122260223, relay — 16777215. Разница в разы.
Флаг `WebRtcIPHandlingPolicy` — что он реально делает
Главный рычаг — политика обработки IP. Задаётся через enterprise policy или командную строку. Значения:
| Значение | Поведение |
|---|---|
| `default` | Все интерфейсы, все адреса |
| `default_public_and_private_interfaces` | Отсекает loopback, оставляет остальное |
| `default_public_interface_only` | Один интерфейс по умолчанию |
| `disable_non_proxied_udp` | UDP только через прокси, иначе TCP через прокси |
Последнее значение — то, что нужно. `disable_non_proxied_udp` заставляет Chromium использовать только проксированные маршруты. Если прокси не поддерживает UDP — WebRTC переключается на TCP или вовсе не устанавливает соединение.
Флаг в командной строке:
```bash
chromium --proxy-server="socks5://127.0.0.1:1080" \
--force-webrtc-ip-handling-policy=disable_non_proxied_udp
```
Проверить, что применилось: `chrome://policy`. В списке увидите `WebRtcIPHandlingPolicy` со значением `disable_non_proxied_udp`.
Политика через реестр и enterprise-конфиг
На Windows — реестр. Ключ `HKLM\SOFTWARE\Policies\Google\Chrome`, значение `WebRtcIPHandlingPolicy` типа REG_SZ:
```powershell
New-Item -Path "HKLM:\SOFTWARE\Policies\Google\Chrome" -Force
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Google\Chrome" `
-Name "WebRtcIPHandlingPolicy" -Value "disable_non_proxied_udp"
```
На Linux — JSON в `/etc/opt/chrome/policies/managed/webrtc.json`:
```json
{
"WebRtcIPHandlingPolicy": "disable_non_proxied_udp",
"WebRtcUdpPortRange": ""
}
```
Пустой `WebRtcUdpPortRange` убирает ограничение диапазона портов — иногда полезно, если корпоративный файрвол режет всё вне 10000-10100.
Корпоративные политики применяются раньше пользовательских настроек. Пользователь не сможет их переопределить через `chrome://flags`. Это важно: если вы раздаёте браузер сотрудникам, политика — единственный надёжный способ.
`WebRtcAllowLegacyTLSProtocols` и другие тонкости
Есть ещё несколько флагов, которые влияют на утечки косвенно. `WebRtcAllowLegacyTLSProtocols` — не про IP, а про DTLS. Оставьте `false`. `WebRtcEventLogCollectionAllowed` — логирование WebRTC-событий, лучше `false` в продакшене.
Флаг `--disable-features=WebRtcHideLocalIpsWithMdns` отключает mDNS-обфускацию host-кандидатов. Chromium с версии 76 по умолчанию заменяет локальные адреса на `.local` mDNS-имена. Это защита от утечек в локальной сети, но не от IPv6-утечек наружу. mDNS-имя резолвится через multicast, и если собеседник в той же сети — он увидит реальный адрес.
Не отключайте mDNS. Наоборот, проверьте, что он включён:
```bash
chromium --proxy-server="socks5://127.0.0.1:1080" \
--enable-features=WebRtcHideLocalIpsWithMdns \
--force-webrtc-ip-handling-policy=disable_non_proxied_udp
```
Прокси-сервер: SOCKS5 с UDP ASSOCIATE
`disable_non_proxied_udp` работает только если прокси умеет UDP. SOCKS5 поддерживает UDP через команду `UDP ASSOCIATE` (RFC 1928, раздел 7). Клиент открывает TCP-соединение к прокси, отправляет `UDP ASSOCIATE`, получает адрес relay-сокета на стороне прокси. Дальше UDP-пакеты инкапсулируются и идут через этот сокет.
Не все SOCKS5-серверы это умеют. Dante, 3proxy, ssh -D — только TCP. Если прокси не поддерживает UDP ASSOCIATE, Chromium при `disable_non_proxied_udp` просто не установит WebRTC-соединение. Иногда это желаемое поведение, иногда — нет.
Проверить поддержку UDP у прокси:
```bash
curl --socks5-hostname 127.0.0.1:1080 \
--proxytunnel \
-v https://api.ipify.org 2>&1 | grep -i udp
```
Точнее — через `socat`:
```bash
socat - SOCKS5:127.0.0.1:1080:
```
Если соединение устанавливается и STUN-ответ приходит — UDP ASSOCIATE работает.
Реальный кейс: корпоративный терминал и утечка префикса
Была задача: 40 рабочих станций в офисе, весь трафик через IPv6-прокси. Пользователи жалуются — на видеоконференциях собеседники видят их корпоративные IPv6-адреса из диапазона `2001:db8:1234::/48`. Проверка через `chrome://webrtc-internals` подтвердила: host-кандидаты с реальных интерфейсов.
Причина: политика не применялась. Браузер запускался с ярлыка, где был только `--proxy-server`. Флаг `--force-webrtc-ip-handling-policy` отсутствовал. Плюс версия Chromium 118 — в ней `disable_non_proxied_udp` игнорируется, если прокси не поддерживает UDP, и Chromium молча откатывается к прямому соединению.
Решение: обновили прокси до сборки с UDP ASSOCIATE (на базе `3proxy` с патчем), раскатали enterprise policy через GPO. Через сутки в `webrtc-internals` остались только relay-кандидаты с адресами прокси. Утечка закрыта.
Кейс: Firefox ведёт себя иначе
Firefox не использует `WebRtcIPHandlingPolicy`. У него свой ключ `media.peerconnection.ice.default_address_only` и `media.peerconnection.ice.no_host`. Первый оставляет только дефолтный интерфейс, второй вообще убирает host-кандидаты.
В `about:config`:
```
media.peerconnection.ice.no_host = true
media.peerconnection.ice.proxy_only_if_behind_proxy = true
```
Второй параметр — критичный. Он заставляет Firefox использовать только проксированные интерфейсы для ICE. Если прокси не настроен — WebRTC не работает вовсе.
Разница с Chromium: Firefox не течёт через IPv6 при `no_host = true`, потому что host-кандидаты не собираются вообще. Остаются только srflx и relay. Проверено на Firefox 121 — в `about:webrtc` host-кандидатов нет.
Кейс: Electron-приложение тащит утечку в прод
Приложение на Electron 28, внутри — WebRTC для звонков. Пользователи за IPv6-прокси. Утечка воспроизводится стабильно. `--force-webrtc-ip-handling-policy` в `app.commandLine.appendSwitch()` не помогал.
Причина: Electron собирает Chromium с флагами, где `WebRtcIPHandlingPolicy` обрабатывается только через policy-менеджер, а не через командную строку. `appendSwitch` пишет в `base::CommandLine`, но `WebRtcIPHandlingPolicy` читается из `PrefService`, а не из `CommandLine`. Классические грабли.
Решение — установить политику через `session.defaultSession.setPermissionRequestHandler` не получится, это не тот уровень. Пришлось использовать `app.commandLine.appendSwitch('force-webrtc-ip-handling-policy', 'disable_non_proxied_udp')` вместе с патчем `webrtc_ip_handling_policy` в `preferences`:
```javascript
const { app, session } = require('electron');
app.commandLine.appendSwitch('force-webrtc-ip-handling-policy', 'disable_non_proxied_udp');
app.whenReady().then(() => {
session.defaultSession.setProxy({
proxyRules: 'socks5://127.0.0.1:1080',
proxyBypassRules: '<-loopback>'
});
});
```
Плюс `<-loopback>` в bypass — иначе localhost-трафик идёт мимо прокси, и WebRTC находит host-кандидат на loopback-интерфейсе.
Проверка: как убедиться, что утечки нет
Откройте `chrome://webrtc-internals` до начала звонка. Начните звонок. Смотрите вкладку `Ice candidates`. Если видите `typ host` с адресом, который не принадлежит прокси — утечка есть. Если только `typ relay` — всё чисто.
Альтернатива — веб-сервис `browserleaks.com/webrtc`. Он покажет публичный IP, который видит JavaScript через `RTCPeerConnection`. С IPv6-прокси и правильной политикой там должен быть адрес прокси, не ваш.
Ещё способ — tcpdump на интерфейсе:
```bash
sudo tcpdump -i eth0 -n 'udp and portrange 3478-3479' -c 20
```
STUN идёт на порт 3478. Если видите пакеты с вашего реального IPv6 на внешний STUN-сервер — политика не применилась.
Что делать, если прокси не поддерживает UDP
Вариант первый: `disable_non_proxied_udp` + отказ от WebRTC. Видеозвонки не работают, зато утечек нет. Подходит для параноидальных сценариев.
Вариант второй: TURN-сервер через TCP. Настройте `iceServers` с `transport: 'tcp'`:
```javascript
const pc = new RTCPeerConnection({
iceServers: [{
urls: 'turn:turn.example.com:443?transport=tcp',
username: 'user',
credential: 'pass'
}],
iceTransportPolicy: 'relay'
});
```
`iceTransportPolicy: 'relay'` заставляет ICE использовать только relay-кандидаты. Host и srflx игнорируются. Весь трафик идёт через TURN, который можно завернуть в прокси.
Третий вариант — коммерческий IPv6-прокси с UDP ASSOCIATE. Именно так работает lexic.ml — SOCKS5 с поддержкой UDP на стороне сервера, что позволяет держать `disable_non_proxied_udp` без потери видеозвонков. Проверка простая: запустите `chrome://webrtc-internals` с флагом и убедитесь, что relay-кандидаты идут с адреса прокси, а не с вашего интерфейса.
Итоговая конфигурация для Chromium
Соберите всё вместе. Командная строка:
```bash
chromium \
--proxy-server="socks5://127.0.0.1:1080" \
--force-webrtc-ip-handling-policy=disable_non_proxied_udp \
--enable-features=WebRtcHideLocalIpsWithMdns \
--disable-features=WebRtcAllowInputVolumeAdjustment
```
Плюс policy-файл в `/etc/opt/chrome/policies/managed/`:
```json
{
"WebRtcIPHandlingPolicy": "disable_non_proxied_udp",
"WebRtcUdpPortRange": "10000-10100"
}
```
Проверка: `chrome://policy` показывает применённую политику, `chrome://webrtc-internals` — только relay-кандидаты. Если хоть один `typ host` с вашим реальным адресом проскочил — конфигурация неполная. Ищите, где откатилось.