Как антифрод-системы маркетплейсов связывают аккаунты через WebRTC-утечки при использовании прокси
Содержание
WebRTC — это протокол для передачи медиа в реальном времени, встроенный во все современные браузеры. Он умеет обходить прокси на уровне сетевого стека. Это не баг, а архитектурное решение: для VoIP и видео нужны прямые UDP-соединения, иначе задержка вырастает с 20 мс до 300+. Антифрод маркетплейсов давно научился этим пользоваться.
Почему WebRTC игнорирует HTTP-прокси
Обычный HTTP/SOCKS-прокси работает на уровне приложений или транспорта. Браузер отправляет запросы через него, потому что так настроено. WebRTC живёт ниже — он использует ICE (Interactive Connectivity Establishment) для сбора кандидатов: локальных IP, STUN-рефлексов, TURN-релеев. Локальные кандидаты собираются через `getifaddrs()` или его аналоги, минуя прокси-конфигурацию полностью.
Если у машины есть реальный сетевой интерфейс с адресом `192.168.1.45`, WebRTC его увидит и предложит как кандидата. Прокси тут ни при чём. То же с STUN: браузер стучится на `stun.l.google.com:19302` напрямую, если трафик не перехвачен на уровне ОС. Сервер отвечает с публичным IP — и вот он, ваш настоящий адрес, в логах антифрода.
```javascript
const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });
pc.onicecandidate = (e) => {
if (e.candidate) console.log(e.candidate.candidate);
};
pc.createDataChannel('probe');
pc.createOffer().then(o => pc.setLocalDescription(o));
```
Три строки — и вы уже слили свой домашний IP. Даже если весь остальной трафик идёт через цепочку из пяти SOCKS5.
Что именно утекает и в каком виде
ICE-кандидат выглядит так: `candidate:842163049 1 udp 1677729535 203.0.113.77 54321 typ srflx`. Здесь `203.0.113.77` — STUN-рефлекс, реальный публичный адрес. Тип `srflx` означает, что адрес получен от STUN-сервера. Есть ещё `host` (локальный интерфейс) и `relay` (TURN). Антифрод-скрипт собирает все три типа.
Из host-кандидатов вытаскивается приватный IP: `192.168.x.x`, `10.x.x.x`, `172.16-31.x.x`. Это не идентификатор пользователя, но хороший маркер окружения. Два аккаунта с одинаковым `192.168.1.45` — почти наверняка одна машина. Особенно если совпадает ещё и mDNS-хэш.
Современные Chrome и Firefox маскируют host-кандидаты через mDNS (`a1b2c3d4-....local`), но srflx-кандидаты всё равно отдают публичный IP. Отключить это без флагов командной строки нельзя.
| Тип кандидата | Что раскрывает | Маскируется | Актуален для антифрода |
|---|---|---|---|
| host | Локальный IP, mDNS-хэш | Частично (mDNS) | Да, для связки устройств |
| srflx | Публичный IP через STUN | Нет | Критично |
| relay | IP TURN-сервера | Нет | Косвенно |
| prflx | Публичный IP из peer-reflexive | Нет | Редко |
Как антифрод это использует
Маркетплейсы не парсят ICE-кандидаты в момент логина — это слишком заметно. Они встраивают невидимый WebRTC-зонд в страницу оформления заказа, в чат с продавцом, в редактор отзывов. Зонд делает offer, собирает кандидатов, шлёт их на сервер вместе с обычной телеметрией. Пользователь ничего не замечает: DataChannel не открывается, соединение не устанавливается, медиа не передаётся.
Дальше — граф. Каждый аккаунт получает набор узлов: srflx-IP, host-IP, mDNS-хэш, набор codec'ов, версия Chrome, часовой пояс. Совпадение srflx-IP между двумя аккаунтами — сильный сигнал. Совпадение srflx + host + mDNS — почти приговор, даже если IP-адрес прокси у аккаунтов разный.
Проблема в том, что srflx-IP у одного человека стабилен неделями. Домашний интернет от Ростелекома или Comcast выдаёт один и тот же адрес по DHCP на срок аренды в 24–72 часа, а часто и дольше. Прокси меняется хоть каждый запрос — а WebRTC всё равно светит один и тот же адрес.
Почему SOCKS5 не спасает
SOCKS5 проксирует TCP и UDP на уровне сокета. Если браузер явно настроен использовать SOCKS5 для всего, включая UDP (что редкость), WebRTC пойдёт через него. Но по умолчанию Chrome и Firefox не проксируют WebRTC через SOCKS. Есть флаг `--force-webrtc-ip-handling-policy=disable_non_proxied_udp`, но он работает только если прокси настроен системно или через PAC-скрипт.
Расширения вроде Proxy SwitchyOmega тоже не помогают: они перехватывают HTTP-запросы, но не UDP-сокеты WebRTC. Единственный надёжный способ — либо полностью отключить WebRTC, либо гнать весь трафик через VPN-туннель на уровне ОС, где UDP перехватывается маршрутизацией.
Второй вариант — использовать прокси, который умеет UDP ASSOCIATE. SOCKS5 поддерживает эту команду с 1996 года (RFC 1928), но большинство публичных прокси её не реализуют. Если реализует — WebRTC-трафик пойдёт через прокси, и STUN-рефлекс покажет адрес прокси-сервера.
Проверка собственных утечек
Откройте `chrome://webrtc-internals` и посмотрите на список ICE-кандидатов. Если там есть `srflx` с адресом, отличным от вашего прокси — утечка. То же через консоль:
```bash
curl -s https://raw.githubusercontent.com/diafygi/webrtc-ips/master/index.html -o /tmp/rtc.html
python3 -m http.server 8080 --directory /tmp
```
Откройте `http://localhost:8080/rtc.html` в браузере с настроенным прокси. Страница покажет все IP, которые видит WebRTC. Если увидите свой реальный — прокси не работает для этого канала.
Для автоматизации проверки на сервере используйте headless Chrome:
```python
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
opts = Options()
opts.add_argument('--headless=new')
opts.add_argument('--proxy-server=socks5://user:pass@proxy.example.com:1080')
opts.add_argument('--force-webrtc-ip-handling-policy=disable_non_proxied_udp')
driver = webdriver.Chrome(options=opts)
driver.get('https://browserleaks.com/webrtc')
print(driver.find_element('id', 'rtc-ip').text)
```
Флаг `disable_non_proxied_udp` заставляет браузер использовать только проксированные UDP-кандидаты. Без него headless Chrome сольёт локальный IP сервера.
Разбор реальных ситуаций
**Пример: сервер на nginx 1.24 с проксированием WebSocket.** Команда запустила пул браузеров на одном VPS, каждый через свой SOCKS5. Через неделю маркетплейс заблокировал 40 аккаунтов из 50. Причина: все браузеры имели один srflx-кандидат — публичный IP VPS, потому что WebRTC шёл напрямую, минуя SOCKS5. SOCKS5 проксировал только TCP-трафик HTTP, UDP-сокеты WebRTC уходили с интерфейса `eth0`. Решение: перевели браузеры на отдельные сетевые namespace с маршрутизацией UDP через `redsocks` и `iptables`, плюс флаг `disable_non_proxied_udp`. Утечка закрылась, но задержка выросла на 15–20 мс.
**Пример: Firefox 121 с `media.peerconnection.ice.default_address_only=true`.** Настройка отключает сбор srflx-кандидатов, оставляя только host. Казалось бы, утечка закрыта. Но host-кандидат всё равно отдавал `192.168.1.45` — одинаковый для трёх аккаунтов на одной машине. Антифрод связал их по этому признаку за два дня. Помогло `media.peerconnection.ice.no_host=true`, но это ломает видеозвонки в поддержку, если маркетплейс их использует. Пришлось поднимать отдельные VM.
**Пример: Python-скрипт на requests с прокси, но с встроенным WebRTC-зондом.** Разработчик использовал `requests` с SOCKS5 для API-запросов, но для прохождения капчи открывал Selenium. Selenium запускал Chrome без флага отключения WebRTC. Зонд на странице капчи собрал srflx-IP, и все 12 аккаунтов, прошедших капчу с одной машины, попали в один кластер. Решение: `--force-webrtc-ip-handling-policy=disable_non_proxied_udp` плюс ротация прокси на каждый запуск Selenium. Проблема ушла, но потребовалось 3 недели на разбор банов.
Что делать, если нужен WebRTC
Иногда WebRTC нужен по делу: видеозвонки с продавцом, голосовой чат в поддержке. Тогда полное отключение не вариант. Схема такая: поднимаете TURN-сервер (coturn, 4.6+) на том же хосте, что и прокси. Настраиваете браузер использовать только этот TURN. Все кандидаты становятся `relay` с IP TURN-сервера. Реальный адрес не светится.
Конфиг coturn:
```
listening-port=3478
tls-listening-port=5349
fingerprint
lt-cred-mech
user=webrtc:secret
realm=proxy.example.com
external-ip=203.0.113.77
no-multicast-peers
```
В браузере:
```javascript
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'turn:203.0.113.77:3478', username: 'webrtc', credential: 'secret' }],
iceTransportPolicy: 'relay'
});
```
`iceTransportPolicy: 'relay'` заставляет использовать только TURN. Никаких host и srflx. Задержка вырастет на 30–50 мс, но зато IP не утечёт.
Связка через mDNS и что это даёт антифроду
С Chrome 76 host-кандидаты маскируются mDNS-именами вида `a1b2c3d4-1234-5678-9abc-def012345678.local`. Антифрод не может резолвить их снаружи, но может сравнивать между аккаунтами. Если два аккаунта отдают одинаковый mDNS-хэш — это одна машина. Хэш генерируется случайно при старте браузера, но живёт весь сеанс. Перезапуск Chrome меняет хэш, но не спасает: srflx-IP остаётся тем же.
Некоторые антифроды идут дальше: собирают timing-атаки на mDNS-резолвинг. Если браузер отвечает на запрос `a1b2c3d4....local` за 2 мс — это локальная машина. Если за 50 мс — удалённая. Так строят граф устройств без явного IP.
Итог по защите
Полный чек-лист выглядит так. Проверьте `chrome://webrtc-internals` — не должно быть srflx с реальным IP. Убедитесь, что флаг `disable_non_proxied_udp` активен. Если нужен WebRTC — только через TURN с `iceTransportPolicy: 'relay'`. Для пула браузеров — отдельные сетевые namespace с перехватом UDP. И ротация прокси на каждый сеанс, иначе srflx-IP свяжет аккаунты даже при разных HTTP-прокси.
Прокси-сервисы вроде lexic.ml дают IPv6-адреса, которые WebRTC тоже умеет использовать. IPv6-кандидаты утекают точно так же, как IPv4, и антифроды их собирают. Не думайте, что переход на IPv6 что-то меняет: ICE работает с обоими стеками одинаково.