← Назад в базу знаний

Как антифрод-системы маркетплейсов вычисляют мультиаккаунты через WebRTC-утечки

Как антифрод-системы маркетплейсов вычисляют мультиаккаунты через 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-утечка — не единственный вектор, но один из самых стабильных и самых недооценённых.

✔️Купить прокси