Почему WebRTC сливает реальный IP через SOCKS5 и как его глушить на уровне браузера
Содержание
- Что вообще происходит под капотом WebRTC
- Почему SOCKS5 не спасает
- Как выглядит утечка в цифрах
- Отключение WebRTC в Chrome и Chromium
- Firefox: настройки about:config
- Когда прокси всё-таки держит WebRTC
- Проверка утечек: скрипт на Python
- Кейс: корпоративный терминальный сервер
- Кейс: антидетект-браузер и IPv6-прокси
- Кейс: видеоконференция через корпоративный прокси
- Что делать, если WebRTC нужен
- Итоговая таблица методов
SOCKS5 проксирует TCP и UDP, но браузеру плевать на ваши настройки прокси, когда речь заходит о WebRTC. ICE-кандидаты собираются напрямую через сетевые интерфейсы. Хост-кандидаты содержат локальные адреса, а вот srflx-кандидаты через STUN уже палят публичный IP. И если UDP идёт мимо прокси — привет, деанонимизация.
Что вообще происходит под капотом WebRTC
WebRTC строит соединение через ICE (Interactive Connectivity Establishment). Браузер собирает кандидатов трёх типов: host (локальный интерфейс), srflx (публичный адрес через STUN-сервер), relay (через TURN). Для сбора srflx браузер шлёт STUN Binding Request на UDP-порт 3478. Пакет уходит с того интерфейса, который выбрала операционка по таблице маршрутизации.
SOCKS5 умеет UDP ASSOCIATE, но браузеры эту возможность почти не используют для WebRTC. Chrome, Firefox, Edge — все они игнорируют системный прокси в контексте ICE-сбора. Точнее, частично: TCP-кандидаты (редкость) могут пойти через прокси, но UDP-трафик STUN уходит напрямую. Отсюда и утечка.
Механика простая: STUN-сервер отвечает с вашим публичным адресом и портом. Браузер включает эту пару в SDP как srflx-кандидат. Любой, кто получит SDP (а его получает вторая сторона соединения и часто — сигнальный сервер), узнаёт ваш реальный IP.
Почему SOCKS5 не спасает
Ключевая проблема — уровень абстракции. SOCKS5 работает на уровне приложений, проксируя соединения по запросу клиента. Но WebRTC-стек в браузере общается с сетевым слоем напрямую через сокеты ОС. Он не спрашивает разрешения у прокси-настроек. Он просто открывает UDP-сокет и шлёт пакет.
Chrome с флагом `--proxy-server="socks5://127.0.0.1:1080"` всё равно собирает srflx-кандидаты напрямую. Проверялось на версиях 118-124. Firefox с `network.proxy.socks_remote_dns=true` ведёт себя чуть аккуратнее с DNS, но ICE-сбор всё равно идёт мимо.
Есть нюанс: если в системе нет маршрута наружу кроме прокси (например, в изолированном network namespace), то STUN просто не получит ответ. Но это уже не про прокси, а про сетевую изоляцию.
Как выглядит утечка в цифрах
Возьмём типичный сценарий. Пользователь сидит за SOCKS5 на 127.0.0.1:1080, реальный IP 203.0.113.45, прокси-выход 198.51.100.7. Открывает страницу с WebRTC-звонком. Замеры на Chrome 122:
| Кандидат | Адрес | Источник | Виден второй стороне |
|---|---|---|---|
| host | 192.168.1.42 | локальный интерфейс | да |
| host | fe80::1c2b:3f4a | IPv6 link-local | да |
| srflx | 203.0.113.45:54321 | STUN напрямую | да |
| relay | 198.51.100.7:49152 | TURN через прокси | да |
Второй и третий — катастрофа. Локальный адрес выдаёт топологию сети, srflx — публичный IP. Даже если сигнальный сервер HTTPS, SDP с кандидатами уходит в открытом виде внутри signalling-протокола. Многие приложения логируют SDP целиком.
Отключение WebRTC в Chrome и Chromium
Самый чистый способ — policy или флаг. Для управляемых окружений есть `WebRtcIPHandlingPolicy`. Значения: `default`, `default_public_interface_only`, `default_public_and_private_interfaces`, `disable_non_proxied_udp`. Последнее — то, что нужно.
Через реестр Windows:
```
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome]
"WebRtcIPHandlingPolicy"="disable_non_proxied_udp"
```
Через командную строку при запуске:
```bash
chromium \
--proxy-server="socks5://127.0.0.1:1080" \
--force-webrtc-ip-handling-policy=disable_non_proxied_udp \
--webrtc-ip-handling-policy=disable_non_proxied_udp
```
Флаг `disable_non_proxied_udp` заставляет браузер использовать только проксируемые UDP-пути. Если прокси не поддерживает UDP ASSOCIATE — WebRTC просто не поднимется. Это компромисс: звонки не работают, зато IP не течёт.
Есть ещё `--disable-features=WebRtcHideLocalIpsWithMdns` — наоборот, включает mDNS-обфускацию локальных адресов. Chrome по умолчанию с версии 76 маскирует host-кандидаты через mDNS (`.local` имена). Но srflx всё равно палится.
Firefox: настройки about:config
В Firefox управление через `media.peerconnection.ice.*`. Ключевые параметры:
```
media.peerconnection.ice.default_address_only = true
media.peerconnection.ice.no_host = true
media.peerconnection.ice.proxy_only_if_behind_proxy = true
media.peerconnection.enabled = false
```
`proxy_only_if_behind_proxy` заставляет ICE использовать только прокси-интерфейс, если прокси настроен. `no_host` убирает host-кандидаты. Полное отключение `peerconnection.enabled=false` убивает WebRTC целиком.
Проверка через `about:webrtc` показывает собранные кандидаты в реальном времени. Запустите тест на `browserleaks.com/webrtc` — увидите, что утекает.
Когда прокси всё-таки держит WebRTC
Сценарий: SOCKS5 с поддержкой UDP ASSOCIATE плюс браузер, который умеет в него ходить. На практике это редкость. Chromium не поддерживает UDP через SOCKS5 для WebRTC. Firefox тоже. Единственный рабочий путь — TURN-сервер, который сам ходит наружу через нужный интерфейс.
Схема такая: браузер настраивается на TURN по TCP (порт 443, чтобы пробивать файрволы), TURN-сервер стоит на VPS с нужным IP, весь relay-трафик идёт через него. Тогда srflx-кандидаты либо не собираются (если STUN заблокирован), либо указывают на TURN. Настройка в Firefox:
```
media.peerconnection.ice.relay_only = true
```
Это заставляет использовать только relay-кандидаты. Медиа пойдёт через TURN, задержка вырастет на 30-80 мс в зависимости от расстояния до сервера, зато IP чистый. Для голосовых звонков терпимо, для игр — уже чувствуется.
Проверка утечек: скрипт на Python
Соберём автоматическую проверку. Скрипт поднимает headless Chrome с прокси и парсит кандидатов через CDP.
```python
import asyncio
import json
import websockets
async def check_webrtc(ws_url):
async with websockets.connect(ws_url) as ws:
await ws.send(json.dumps({
"id": 1,
"method": "Runtime.evaluate",
"params": {
"expression": """
(async () => {
const pc = new RTCPeerConnection({
iceServers: [{urls: 'stun:stun.l.google.com:19302'}]
});
pc.createDataChannel('probe');
const cands = [];
pc.onicecandidate = e => {
if (e.candidate) cands.push(e.candidate.candidate);
};
await pc.setLocalDescription(await pc.createOffer());
await new Promise(r => setTimeout(r, 3000));
pc.close();
return cands;
})()
""",
"awaitPromise": True,
"returnByValue": True
}
}))
while True:
msg = json.loads(await ws.recv())
if msg.get("id") == 1:
return msg["result"]["result"]["value"]
if __name__ == "__main__":
import sys
print(asyncio.run(check_webrtc(sys.argv[1])))
```
Запуск Chrome с remote debugging:
```bash
chromium --headless=new --remote-debugging-port=9222 \
--proxy-server="socks5://127.0.0.1:1080" \
--force-webrtc-ip-handling-policy=disable_non_proxied_udp \
about:blank
```
Скрипт покажет список кандидатов. Если в выводе есть srflx с адресом, отличным от IP прокси — утечка. `disable_non_proxied_udp` должен оставить только relay-кандидаты или пустой список.
Кейс: корпоративный терминальный сервер
Терминальный сервер на Windows Server 2019, 40 пользователей, все ходят через SOCKS5 на шлюзе. Один пользователь открыл личную почту с веб-камерой — WebRTC-виджет отправил srflx-кандидат с внутренним адресом 10.20.5.17 и публичным IP офиса. Проблема всплыла при аудите: в логах внешнего сервиса засветился публичный IP компании рядом с учёткой сотрудника.
Причина: Chrome 119 на терминале без групповых политик. Пользовательские настройки прокси не применялись к WebRTC, потому что политика `WebRtcIPHandlingPolicy` не была задана. Решение: раскатали GPO с `disable_non_proxied_udp`, плюс отключили WebRTC на уровне расширения для тех, кому звонки не нужны. Через неделю проверили — srflx-кандидатов ноль, только mDNS-имена вида `a1b2c3d4-....local`.
Кейс: антидетект-браузер и IPv6-прокси
Связка антидетект-браузера с SOCKS5 поверх IPv6. Пользователь думал, что прокси скрывает всё. Но WebRTC собрал host-кандидат с IPv6 link-local `fe80::a4b5:c6d7` и srflx через STUN с IPv4-адресом провайдера. Второй адрес совпал с реальным — деанонимизация за две минуты.
Причина: антидетект-браузер на базе Chromium 117 не применял флаг `--force-webrtc-ip-handling-policy` к дочерним процессам. WebRTC-стек работал в renderer-процессе с дефолтными настройками. Решение: добавили флаг в аргументы всех процессов через `--webrtc-ip-handling-policy=disable_non_proxied_udp` и проверили через `chrome://version` — флаг виден в командной строке. Плюс настроили IPv6-прокси через lexic.ml, где UDP-трафик изолирован на уровне маршрутизации, и STUN-запросы просто не находят маршрут наружу. После этого тест на `browserleaks.com/webrtc` показал пустой список кандидатов.
Кейс: видеоконференция через корпоративный прокси
Компания на 200 человек, все через SOCKS5 на порту 1080. Zoom в браузере работал, но качество падало. При разборе выяснилось: браузер собирал srflx-кандидаты напрямую, а relay-кандидаты через TURN-сервер в другом регионе. ICE выбирал прямой путь (srflx), трафик шёл мимо прокси. Задержка 15 мс, но весь медиапоток — вне корпоративного контроля.
Причина: политика `default` в Chrome. Браузер предпочитает прямые пути. Решение: включили `relay_only` через политику `WebRtcIPHandlingPolicy=default_public_interface_only` плюс принудительный TURN через `iceTransportPolicy: 'relay'` в конфиге приложения. Задержка выросла до 55 мс, зато весь трафик пошёл через корпоративный TURN на 443/TCP. Пропускная способность TURN-сервера — 500 Мбит/с, хватило с запасом.
Что делать, если WebRTC нужен
Полное отключение — грубо. Если звонки критичны, есть компромисс: TURN-only режим с релеем на нужном IP. Настройка в приложении:
```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. Задержка растёт, но IP не течёт. TURN-сервер на 443/TCP пробивает почти любые файрволы, включая корпоративные.
Для тех, кому нужен именно SOCKS5 и звонки — только сеть без прямого маршрута наружу. Network namespace, где единственный выход — прокси. Тогда STUN-пакеты просто дропаются, ICE остаётся без srflx. Жёстко, но работает без правок браузера.
Итоговая таблица методов
| Метод | Chrome | Firefox | Звонки работают | Сложность |
|---|---|---|---|---|
| `disable_non_proxied_udp` | да | нет | нет | низкая |
| `peerconnection.enabled=false` | через флаг | да | нет | низкая |
| TURN-only (`relay`) | да | да | да, с задержкой | средняя |
| Сетевая изоляция | да | да | нет | высокая |
| mDNS-обфускация | по умолчанию | нет | да | нулевая |
mDNS прячет только локальные адреса. srflx он не трогает. Полагаться на него как на защиту — ошибка.