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

Почему IPv6-прокси «протекает» через WebRTC в Chromium: механизм утечки и точечные флаги для её устранения

Почему IPv6-прокси «протекает» через WebRTC в 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::3478,udp

```

Если соединение устанавливается и 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` с вашим реальным адресом проскочил — конфигурация неполная. Ищите, где откатилось.

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