Как антифрод-системы маркетплейсов вычисляют мультиаккаунты через WebRTC-утечки
Содержание
Что такое WebRTC и почему он течёт
WebRTC — набор протоколов для передачи аудио, видео и данных в реальном времени. Браузеры реализуют его через встроенный стек: ICE, STUN, TURN, DTLS, SRTP. Для установления прямого соединения между двумя узлами клиенту нужно узнать свой публичный IP и локальные адреса интерфейсов. Эту информацию он получает через ICE-кандидатов — и вот тут начинается самое интересное.
ICE-кандидат — это пара «IP:порт» плюс тип: host, srflx, prflx, relay. Host-кандидаты — локальные адреса сетевых карт. Srflx — публичный адрес, полученный от STUN-сервера. Браузер собирает их все и отдаёт в JavaScript через `RTCPeerConnection.onicecandidate`. Никаких разрешений на это не нужно. Достаточно вызвать конструктор и подождать пару сотен миллисекунд.
Антифрод-скрипт на странице маркетплейса делает ровно это. Создаёт `RTCPeerConnection`, вешает обработчик, ждёт кандидатов, парсит IP. Если пользователь сидит через VPN, но браузер утекает локальный адрес `192.168.1.45` — это уже сигнал. Если два аккаунта имеют одинаковый host-кандидат — сигнал сильнее. Если host-кандидат совпадает с подсетью другого аккаунта — почти приговор.
Что именно утекает
Список утечек шире, чем принято думать. Помимо IP-адресов, ICE-кандидаты содержат порты, тип кандидата, приоритет, foundation. Foundation — хеш от типа, базового IP и протокола. По нему можно группировать кандидаты, но для фингерпринтинга он слабый. А вот комбинация `local IP + mDNS hostname + набор портов` даёт устойчивый отпечаток сетевого стека.
Современные браузеры (Chrome с 2019, Firefox с 2019) маскируют host-кандидаты через mDNS: вместо `192.168.1.45` в кандидате появляется `a1b2c3d4-....local`. Но это не спасает. mDNS-имя генерируется на основе случайного UUID, который живёт в рамках сессии браузера, но само наличие mDNS-кандидата выдаёт, что браузер прячет локальный IP. А если политика `iceTransportPolicy` стоит `all` и STUN-сервер доступен — srflx-кандидат с публичным IP всё равно утечёт.
Вот минимальный сниппет, который собирает всё за 1.5 секунды:
```javascript
async function harvestICE() {
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
const candidates = [];
pc.onicecandidate = (e) => {
if (e.candidate) candidates.push(e.candidate.candidate);
};
pc.createDataChannel('x');
await pc.setLocalDescription(await pc.createOffer());
await new Promise(r => setTimeout(r, 1500));
pc.close();
return candidates;
}
```
Каждая строка здесь — стандартный API. Ни один браузер не показывает пользователю предупреждение. Ни один антивирус не блокирует. Это не баг, это фича.
Как из этого делают вывод о мультиаккаунте
Сам по себе один IP ничего не доказывает. Антифрод работает на графах. Узлы — аккаунты, рёбра — совпадения атрибутов. WebRTC-кандидаты дают рёбра двух типов: точное совпадение (одинаковый host IP) и подсетевое (одинаковый /24 или /16). Второе слабее, но в домашних сетях провайдеры часто выдают адреса из одной подсети — это создаёт ложные срабатывания, поэтому чистый /16-матч весит мало.
Сильнее работает комбинация. Пример: два аккаунта заходят с разных VPN-выходов (разные /24), но оба утекают host-кандидат `192.168.0.101`. Это значит — одна физическая машина, разные сетевые туннели. Или: host-кандидат разный, но mDNS-хостнейм совпадает — один и тот же браузерный профиль. Или: host-кандидат разный, mDNS разный, но набор портов, выданных ICE, пересекается на 80%+ — одна ОС с одинаковым диапазоном эфемерных портов.
Маркетплейсы (Ozon, Wildberries, Amazon) держат такие графы в графовой БД — Neo4j, TigerGraph, JanusGraph. Запрос на поиск кластеров размером >3 с рёбрами веса >0.7 выполняется за десятки миллисекунд. Аккаунты с общим WebRTC-отпечатком автоматически попадают в один кластер, дальше — ручная модерация или автоматические санкции.
Почему IPv6 всё меняет
В IPv4-мире host-кандидат — это `192.168.x.x`, за NAT. В IPv6-мире у каждого устройства глобально маршрутизируемый адрес. NAT нет. Host-кандидат = глобальный адрес = реальный идентификатор устройства. Это не «локальный IP», это адрес, по которому устройство доступно из интернета напрямую.
Провайдеры выдают /64 или /56 на абонента. Внутри /64 адреса генерируются через SLAAC: либо EUI-64 (по MAC-адресу), либо privacy extensions (RFC 4941). EUI-64 даёт стабильный адрес на всю жизнь сетевой карты. Privacy extensions меняют адрес раз в сутки-неделю, но в рамках сессии он стабилен. Для антифрода это подарок: если два аккаунта утекли IPv6 из одного /64 — это одна квартира, один роутер, одна сеть. Даже если пользователь перезагрузил роутер и получил новый /64 от провайдера — совпадение по префиксу /56 (если провайдер выдаёт /56) сохраняется.
Вот как выглядит сбор IPv6-кандидатов в Python через Selenium:
```python
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
opts = Options()
opts.add_argument("--headless=new")
driver = webdriver.Chrome(options=opts)
driver.get("about:blank")
script = """
const pc = new RTCPeerConnection({iceServers:[]});
const out = [];
pc.onicecandidate = e => { if (e.candidate) out.push(e.candidate.candidate); };
pc.createDataChannel('x');
pc.setLocalDescription(await pc.createOffer());
await new Promise(r => setTimeout(r, 1000));
pc.close();
return out;
"""
candidates = driver.execute_async_script(script)
ipv6 = [c for c in candidates if ':' in c.split()[4]]
print(ipv6)
```
Без STUN-сервера в `iceServers` браузер всё равно соберёт host-кандидаты. В IPv6-сети это будут глобальные адреса. Скрипт вернёт их за секунду.
Методы обхода и их слабые места
Отключить WebRTC в браузере — базовый шаг. В Chrome это `chrome://flags/#disable-webrtc` (устарело в новых версиях) или расширение. В Firefox — `media.peerconnection.enabled = false`. Но это ломает видеозвонки, веб-конференции, часть игр. Пользователь, который отключил WebRTC, сам становится подозрительным: антифрод видит, что API недоступен, и помечает профиль как «скрывающийся».
Прокси-серверы с поддержкой UDP и IPv6 решают часть проблемы. Если весь трафик, включая STUN, идёт через прокси — host-кандидаты будут адресами прокси, а не локальными. Но тут есть нюанс: STUN работает по UDP, а многие HTTP-прокси UDP не поддерживают. SOCKS5 поддерживает. Если прокси SOCKS5 с IPv6-выходом, host-кандидат = IPv6-адрес прокси = стабильный идентификатор, который виден всем.
Именно здесь появляется сервис вроде lexic.ml: IPv6-прокси с 2015 года, где каждый аккаунт получает отдельный /64 или /128, и WebRTC-кандидаты утекают адресами, не связанными с реальной сетью пользователя. Это не панацея, но убирает самый жирный сигнал — совпадение host-кандидатов между аккаунтами.
Слабое место любого прокси — mDNS. Если браузер маскирует host-кандидаты через mDNS, прокси не влияет на mDNS-имена. Они генерируются локально и утекают через тот же ICE. Антифрод может сравнивать mDNS-хостнеймы между сессиями: если один и тот же UUID-хостнейм появляется на двух аккаунтах — это один браузерный профиль, независимо от IP.
Антифрод-логика в деталях
Разберём, как выглядит скоринг. Каждый аккаунт получает вектор признаков: `host_ipv4`, `host_ipv6_prefix`, `srflx_ip`, `mdns_hostname`, `port_range`, `ice_ufrag`, `ice_pwd`, `candidate_types`. `ice_ufrag` и `ice_pwd` — случайные строки, генерируемые на сессию, они бесполезны для долгосрочного трекинга. Остальное — полезно.
Сравнение идёт попарно. Веса примерно такие (на основе публичных утечек кода антифрод-систем):
| Признак | Вес | Ложные срабатывания |
|---|---|---|
| Совпадение host IPv4 | 0.9 | Низкие (NAT) |
| Совпадение host IPv6 /64 | 0.95 | Очень низкие |
| Совпадение host IPv6 /56 | 0.6 | Средние (один провайдер) |
| Совпадение mDNS hostname | 0.85 | Низкие |
| Пересечение портов >80% | 0.3 | Высокие |
| Совпадение srflx IP | 0.4 | Высокие (общий VPN) |
Порог срабатывания обычно 0.75. Если сумма весов по нескольким признакам превышает порог — аккаунты связываются. Дальше — либо автоматический бан, либо теневой бан (аккаунт виден только владельцу, но не покупателям), либо запрос верификации.
Теневой бан — любимый инструмент маркетплейсов. Пользователь не знает, что забанен, продолжает лить трафик, но продажи падают в ноль. Это дешевле, чем разбираться с апелляциями.
Реальный пример: три аккаунта, один роутер
Магазин электроники, три продавца. Все три заходят с разных VPN: Нидерланды, Германия, Швеция. Публичные IP разные, /24 разные. Антифрод сначала ничего не видит. Но скрипт на странице собирает ICE-кандидаты.
У всех трёх утекает host-кандидат `192.168.1.77` — потому что все три сидят за одним роутером в офисе, а VPN-клиенты запущены на одной машине или на трёх машинах в одной сети. Плюс mDNS-хостнейм у двух из трёх совпадает — один и тот же Chrome-профиль, скопированный через `--user-data-dir`.
Система строит граф: три узла, рёбра с весом 0.9 (host IPv4) и 0.85 (mDNS). Кластер размером 3. Автоматическая блокировка всех трёх. Продавцы пишут в поддержку, получают шаблонный ответ «нарушение правил мультиаккаунтинга». Доказательства им не показывают.
Что можно было сделать: разнести машины по разным физическим сетям, использовать разные браузерные профили с разными mDNS-идентификаторами, отключить host-кандидаты через `iceTransportPolicy: 'relay'` (тогда останутся только TURN-кандидаты). Но `relay` требует TURN-сервера, а TURN-сервер видит реальный IP клиента — если он не за прокси.
Почему это не решается одним расширением
Расширения, блокирующие WebRTC, работают на уровне API: они либо удаляют `RTCPeerConnection` из `window`, либо подменяют его заглушкой. Антифрод это детектит. Проверка простая: `typeof RTCPeerConnection === 'undefined'` или `RTCPeerConnection.toString()` не содержит нативного кода. Если API отсутствует — профиль помечается.
Более хитрые расширения подменяют кандидаты на фейковые. Но тогда антифрод видит кандидаты, которые не соответствуют реальному сетевому окружению: например, host-кандидат `10.0.0.1`, а srflx-кандидат от STUN показывает совсем другой IP. Несоответствие — сигнал. Или кандидаты вообще не приходят, хотя `iceGatheringState` переходит в `complete`. Это тоже сигнал.
Единственный надёжный путь — изоляция на уровне сети. Отдельная виртуальная машина или контейнер на каждый аккаунт, свой сетевой namespace, свой IPv6-префикс, свой браузерный профиль. Тогда WebRTC-кандидаты у каждого аккаунта свои, и они не пересекаются. Дорого, но работает.
Что делать, если вы уже в кластере
Если аккаунты уже связаны — разорвать граф задним числом нельзя. Можно только не давать новых рёбер. Меняете сетевую изоляцию, меняете браузерные профили, меняете mDNS-идентификаторы. Но старые рёбра остаются в графе навсегда — антифрод-системы хранят историю.
Некоторые маркетплейсы дают «амнистию»: если аккаунт в течение 90 дней не имеет новых рёбер с забаненными, его могут разбанить. Но это редкость. Чаще — перманентный теневой бан.
Практический совет: не заводите второй аккаунт на той же машине, даже под VPN. Не копируйте браузерные профили. Не используйте один роутер для нескольких аккаунтов, если они не должны быть связаны. WebRTC-утечка — не единственный вектор, но один из самых стабильных и самых недооценённых.