Почему IPv6-прокси «просвечивает» реальный MAC через WebRTC: механизм утечки и как его закрыть
Содержание
- Как WebRTC добывает адреса в обход прокси
- При чём тут MAC и IPv6
- ICE, mDNS и что реально утекает в SDP
- Почему IPv6-прокси хуже IPv4 в этом сценарии
- Проверка: что реально видит сайт
- Что делать с EUI-64
- Закрываем WebRTC на уровне браузера
- Прокси-уровень: UDP и IPv6
- Где лекс.ml вписывается в схему
- Реальный сценарий: антифрод ловит по IPv6
- Чек-лист перед тем, как считать, что закрыто
WebRTC умеет обходить прокси. Это не баг браузера, а следствие того, как устроен ICE. И IPv6 тут добавляет отдельный слой проблем — от утечки локальных адресов до утечки идентификаторов интерфейса, которые часть сканеров трактует как «MAC-подобный» отпечаток. Разбираем механику.
Как WebRTC добывает адреса в обход прокси
Браузер не спрашивает у прокси, какой у тебя IP. Он собирает ICE-кандидатов напрямую через сетевые интерфейсы ОС: host-кандидаты (локальные адреса), srflx (через STUN), relay (через TURN). Host-кандидаты формируются из таблицы интерфейсов, а не из HTTP-стека. Прокси живёт на уровне сокета приложения, ICE — на уровне сетевого стека. Отсюда и разрыв.
Когда ты сидишь за SOCKS5-прокси, браузер всё равно видит физические интерфейсы: `eth0`, `wlan0`, `tun0`, `docker0`. Каждый из них отдаёт набор адресов. Для IPv4 это чаще всего RFC1918 (`192.168.x.x`, `10.x.x.x`). Для IPv6 — глобальные адреса, link-local (`fe80::/10`), и временные адреса из privacy extensions.
STUN-сервер возвращает srflx-кандидата — публичный адрес, который видит сервер. Если ты за прокси, srflx должен показывать IP прокси. Но если браузер отправляет STUN-запрос мимо прокси (а он может), srflx покажет твой настоящий адрес. Это и есть классическая утечка WebRTC.
При чём тут MAC и IPv6
Тут нужна точность. MAC-адрес сам по себе в WebRTC SDP не попадает. Браузеры его не отдают в ICE-кандидатах. Но IPv6-адреса содержат идентификатор интерфейса в младших 64 битах, и он может быть производным от MAC.
Формат EUI-64: MAC `AA:BB:CC:DD:EE:FF` превращается в `AABB:CCFF:FEDD:EEFF`. Вставляются байты `FF:FE` в середину, седьмой бит первого байта инвертируется. Этот адрес становится suffix'ом IPv6-адреса. Если ОС использует EUI-64 (а не stable-privacy или random), то MAC восстанавливается из IPv6-адреса обратной операцией — за секунды.
Проверить, что у тебя в интерфейсе:
```bash
ip -6 addr show dev eth0
```
Если видишь что-то вроде `2001:db8:1:2:aabb:ccff:fedd:eeff/64` — это EUI-64, и MAC внутри. Если `2001:db8:1:2:4a3f:9c2e:1b7d:5e88/64` — это stable-privacy, MAC не восстановить.
Вот и весь «просвет»: не WebRTC отдаёт MAC, а IPv6-адрес в host-кандидате содержит его производную. Сканеры и антифрод-системы это знают и раскручивают обратно.
ICE, mDNS и что реально утекает в SDP
С 2019 года Chrome и Firefox прячут host-кандидаты за mDNS: вместо `192.168.1.5` в SDP попадает `a1b2c3d4-....local`. Это снизило утечки локальных IPv4. Но mDNS-кандидаты — тоже идентификатор, и они стабильны в пределах сессии/профиля. А вот srflx-кандидаты по-прежнему содержат публичные адреса, включая IPv6.
Типичный SDP-блок выглядит так:
```
a=candidate:1 1 udp 2122260223 192.168.1.5 54321 typ host generation 0
a=candidate:2 1 udp 2122260223 fe80::1c2d:3e4f:5a6b:7c8d 54322 typ host generation 0
a=candidate:3 1 udp 1686052607 203.0.113.42 54323 typ srflx raddr 192.168.1.5
a=candidate:4 1 udp 1686052607 2001:db8:aaaa::42 54324 typ srflx raddr fe80::1c2d:3e4f:5a6b:7c8d
```
Четыре кандидата: два host (IPv4 + IPv6 link-local), два srflx. Если IPv6-прокси настроен только для TCP, а UDP идёт напрямую — srflx по IPv6 покажет твой реальный адрес. Прокси не при делах, трафик ушёл мимо.
Почему IPv6-прокси хуже IPv4 в этом сценарии
IPv4-прокси обычно перехватывает всё через NAT или TUN-интерфейс, и там один адрес. С IPv6 сложнее: у интерфейса несколько адресов одновременно — link-local, глобальный, временный (RFC 4941), иногда ULA (`fd00::/8`). ICE собирает их все. Каждый — потенциальная утечка.
Плюс privacy extensions: ОС меняет временный адрес раз в несколько часов (по умолчанию `temp_prefered_lft`, часто 86400 сек). Значит, отпечаток «плывёт», но не исчезает. Стабильный адрес (EUI-64 или stable-privacy) остаётся.
Ещё нюанс: IPv6-прокси часто настраивают как SOCKS5 с `ATYP=0x04` (IPv6-адрес). Но клиент может игнорировать это для UDP ASSOCIATE, и тогда UDP-трафик ICE идёт через дефолтный маршрут ОС. Прокси работает, а WebRTC — нет.
Проверка: что реально видит сайт
Быстрый тест — открыть `browserleaks.com/webrtc` или `ipleak.net` за прокси. Но лучше проверить программно, чтобы понимать, что уходит. Вот минимальный python-скрипт, который поднимает RTCPeerConnection через aiortc и печатает собранных кандидатов:
```python
import asyncio
from aiortc import RTCPeerConnection, RTCSessionDescription
async def probe():
pc = RTCPeerConnection()
await pc.setLocalDescription(await pc.createOffer())
await asyncio.sleep(2)
for line in pc.localDescription.sdp.splitlines():
if line.startswith("a=candidate"):
print(line)
await pc.close()
asyncio.run(probe())
```
Запусти с прокси и без. Разница в списке кандидатов — и есть твоя утечка. Если в выводе есть `typ host` с глобальным IPv6 или `typ srflx` с адресом, которого не должно быть — прокси не покрывает UDP/ICE.
Что делать с EUI-64
Первое и главное — отключить EUI-64 на всех интерфейсах. На Linux это делается через sysctl:
```bash
sysctl -w net.ipv6.conf.all.use_tempaddr=2
sysctl -w net.ipv6.conf.default.use_tempaddr=2
sysctl -w net.ipv6.conf.eth0.use_tempaddr=2
```
`use_tempaddr=2` включает privacy extensions с приоритетом временных адресов. Стабильный адрес остаётся, но исходящие соединения идут с временного. Однако `use_tempaddr` не убирает EUI-64 из стабильного адреса — он просто перестаёт быть исходящим.
Чтобы убрать EUI-64 совсем, нужен `addr_gen_mode`. На современных ядрах (5.x+) есть режим 3 — stable-privacy без EUI-64:
```bash
sysctl -w net.ipv6.conf.eth0.addr_gen_mode=3
ip link set dev eth0 down && ip link set dev eth0 up
```
После этого интерфейс сгенерирует адрес по RFC 7217 (stable-privacy), MAC из него не восстановить. Проверь `ip -6 addr` — suffix должен быть случайным.
Закрываем WebRTC на уровне браузера
Самый надёжный способ — выключить WebRTC там, где он не нужен. В Firefox это `media.peerconnection.enabled=false` в `about:config`. В Chrome — расширение вроде WebRTC Leak Prevent или запуск с флагами:
```bash
google-chrome --force-webrtc-ip-handling-policy=disable_non_proxied_udp \
--webrtc-ip-handling-policy=disable_non_proxied_udp
```
`disable_non_proxied_udp` заставляет ICE использовать только прокси-путь. Если прокси не поддерживает UDP — WebRTC просто не установит соединение. Это ожидаемо и правильно: лучше сломанный WebRTC, чем утёкший адрес.
В Firefox аналог — `media.peerconnection.ice.proxy_only=true`. Плюс `media.peerconnection.ice.default_address_only=true` ограничивает host-кандидатов одним интерфейсом.
Прокси-уровень: UDP и IPv6
Если WebRTC нужен (видеозвонки, стриминг), придётся гонять UDP через прокси. SOCKS5 умеет UDP ASSOCIATE, но поддержка в клиентах разная. Проверь, что твой клиент реально туннелирует UDP: `curl --socks5-hostname` работает только по TCP, для UDP нужен отдельный инструмент.
Для IPv6-специфики удобно держать TUN-интерфейс, который заворачивает весь трафик, включая IPv6, в прокси. Тогда ICE собирает кандидатов уже с адресов TUN, а не с физического интерфейса. Схема надёжнее, чем SOCKS5 на уровне приложения.
Вот пример конфига nginx как TURN-подобного релея для WebRTC (упрощённо, для сигналинга и медиа):
```nginx
stream {
upstream turn_backend {
server 127.0.0.1:3478;
}
server {
listen [::]:3478 udp;
listen 3478 udp;
proxy_pass turn_backend;
proxy_timeout 600s;
}
}
```
Релей сам по себе не решает утечку host-кандидатов, но гарантирует, что медиа пойдёт через твой сервер, а не напрямую. В связке с `ice.proxy_only` это закрывает дыру.
Где лекс.ml вписывается в схему
Когда нужен именно IPv6-прокси с поддержкой UDP и без EUI-64 в исходящих адресах, имеет смысл брать готовую инфраструктуру, а не собирать TUN руками. На lexic.ml IPv6-прокси работает с 2015 года, и там исходящие адреса генерируются по stable-privacy — MAC из них не вытащить. Это не отменяет настройку браузера (ICE всё равно надо ограничить), но снимает половину проблемы на сетевом уровне.
Проверить, что прокси не светит EUI-64, можно так:
```bash
curl -6 --socks5-hostname user:pass@proxy.example:1080 https://ifconfig.co
```
Сравни суффикс адреса с MAC своего интерфейса. Если `FF:FE` в середине нет и suffix не совпадает с производной от MAC — всё в порядке.
Реальный сценарий: антифрод ловит по IPv6
Пример: сервис с антифрод-проверкой на входе. Пользователь за IPv4-прокси, всё чисто по HTTP. Но браузер собирает IPv6 host-кандидата с EUI-64, и в SDP уходит `2001:db8:1:2:aabb:ccff:fedd:eeff`. Антифрод парсит SDP, восстанавливает MAC `aa:bb:cc:dd:ee:ff`, сверяет с базой устройств и видит, что этот MAC уже был на десяти других аккаунтах с других IP.
Причина: браузер не ограничил ICE, ОС использовала EUI-64, прокси не покрывал UDP. Три условия сошлись — утечка. Решение: `addr_gen_mode=3` + `ice.proxy_only=true` + UDP через TUN. После этого в SDP остаются только mDNS-кандидаты и srflx с адресом прокси. MAC не восстановить, потому что его в адресе нет.
Чек-лист перед тем, как считать, что закрыто
Проверь по пунктам. Отключён ли EUI-64 (`addr_gen_mode=3`)? Включены ли privacy extensions (`use_tempaddr=2`)? Ограничен ли ICE (`ice.proxy_only` или `disable_non_proxied_udp`)? Туннелируется ли UDP, включая IPv6? Нет ли в SDP глобальных адресов, кроме адреса прокси? Прогнал ли ты `aiortc`-скрипт до и после?
Если хоть один пункт не закрыт — считай, что утечка осталась. WebRTC не прощает полумер: он собирает всех кандидатов, каких видит, и отдаёт их в SDP. IPv6 с EUI-64 делает эту утечку особенно неприятной, потому что за адресом стоит физический идентификатор железа. Убрать EUI-64, ограничить ICE, завернуть UDP — и только тогда IPv6-прокси перестаёт «просвечивать».