Как антифрод выявляет IPv6-прокси по поведенческим паттернам: тайминги TLS-хендшейка и MTU
Содержание
- Почему IP-репутация больше не работает
- TLS-хендшейк как источник тайминговых сигналов
- JA3/JA4 и что они не ловят
- MTU как маркер туннеля
- Профиль ретрансмиссий и RTT-аномалии
- Асимметрия задержек между хопами
- Как выглядит детект в логах балансировщика
- Реальные кейсы из практики
- Почему комбинация фич сильнее каждой по отдельности
- Что делать, если ты на стороне клиента
- Куда движется детект
Антифрод-системы давно перестали смотреть только на IP. Адрес — расходник. Пул /64 выдаётся тысячами, ротация идёт каждые пару минут, и чёрный список провайдера обновляется быстрее, чем ты успеваешь дописать парсер. Поэтому современные детекторы цепляются за поведение: как клиент устанавливает соединение, с какими таймингами, каким MTU, каким порядком TLS-сообщений. Прокси-слой почти всегда оставляет следы в этих метриках, даже если сам IP чистый.
Почему IP-репутация больше не работает
Провайдер прокси покупает подсеть /48 у хостера, режет на /64, каждый клиент получает свой блок. Для антифрода это означает: репутация адреса живёт часами. Один клиент нагадил — блок ушёл в бан, второй клиент через час получил тот же /64 и попал под раздачу. Провайдеры это знают и крутят пулы. В итоге доля «свежих» адресов в трафике растёт, а сигнал от IP-репутации слабеет.
Что остаётся? Сетевой отпечаток. TCP-стек, TLS-стек, размеры пакетов, задержки на хендшейке. Это дороже подделать, чем купить новый /64. Антифрод-вендоры (Sift, Forter, внутренние системы банков) уже годами строят фичи вокруг JA3/JA4 и RTT-профилей. IPv6 добавил к этому пару специфических маркеров.
TLS-хендшейк как источник тайминговых сигналов
Клиент и сервер обмениваются несколькими сообщениями до первого байта полезной нагрузки. ClientHello → ServerHello + Certificate + ServerHelloDone → ClientKeyExchange + ChangeCipherSpec + Finished → Finished. Каждый переход даёт RTT-замер. У обычного браузера на домашнем канале эти замеры коррелируют с географией и типом канала. У прокси-цепочки корреляция ломается.
Прокси добавляет хоп. Иногда два. Иногда хоп идёт через другой континент, потому что выходной узел в Нидерландах, а входной — в Сингапуре. И тогда первый RTT (ClientHello → ServerHello) большой, а последующие — маленькие, потому что после установки туннеля трафик идёт по уже прогретому пути. Такой «сломанный» профиль задержек — классический маркер.
Вот как выглядит сбор таймингов через Python с `scapy` и `ssl`:
```python
import socket, ssl, time
def handshake_timings(host, port=443):
ctx = ssl.create_default_context()
timings = {}
sock = socket.create_connection((host, port), timeout=5)
t0 = time.perf_counter()
ssock = ctx.wrap_socket(sock, server_hostname=host)
t1 = time.perf_counter()
timings["tcp_connect_ms"] = round((t1 - t0) * 1000, 2)
timings["tls_version"] = ssock.version()
timings["cipher"] = ssock.cipher()[0]
timings["mtu_guess"] = ssock.getsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG)
ssock.close()
return timings
print(handshake_timings("example.com"))
```
Один замер ничего не даёт. Нужна серия и статистика: медиана, разброс, корреляция между RTT и географией IP. Антифрод делает это на своей стороне, из логов балансировщика.
JA3/JA4 и что они не ловят
JA3 — хеш от полей ClientHello: версия TLS, список шифров, расширения, эллиптические кривые, форматы точек. JA4 — его переработанная версия с более устойчивым форматом. Идея простая: разные клиенты шлют разные наборы, и хеш становится отпечатком стека.
Проблема в том, что JA3 подделывается тривиально. Достаточно поменять порядок шифров в `curl_cffi` или в кастомном клиенте на Go — и хеш другой. Прокси-провайдеры это делают. Так что JA3 сам по себе — слабый сигнал. Сила появляется, когда его скрещивают с таймингами и MTU: подделать хеш легко, подделать задержки на реальном канале — нет.
Вот как снять JA3 с реального клиента для сверки:
```bash
curl -s -o /dev/null -w "%{time_connect} %{time_appconnect} %{time_starttransfer}\n" \
--tlsv1.3 --http2 https://example.com/
```
Разница `time_appconnect - time_connect` — это стоимость TLS-хендшейка. У прямого клиента из Москвы до сервера во Франкфурте это 40-70 мс. У прокси-цепочки Москва → Сингапур → Франкфурт — 180-250 мс, причём с характерным «двойным горбом» на распределении.
MTU как маркер туннеля
IPv6 требует MTU минимум 1280 байт на канале. Реальный Ethernet даёт 1500. Провайдеры прокси часто гонят трафик через туннели (WireGuard, GRE, IPIP), и каждый туннель съедает заголовки. WireGuard — 60 байт оверхеда, GRE — 24, IPIP — 20. Итоговый MTU падает до 1420, 1400 или даже 1280.
Клиент под прокси редко об этом знает. Он шлёт пакеты 1500 байт, получает ICMPv6 «Packet Too Big», ретрансмитит мельче. Если ICMP зарезан файрволом (а его часто режут), начинается Path MTU Blackhole: соединение висит, ретрансмиты идут, таймауты растут. Антифрод видит аномально долгие ретрансмиты на первых пакетах — и это красный флаг.
Проверить MTU до цели можно так:
```bash
ping6 -c 3 -M do -s 1400 example.com
ping6 -c 3 -M do -s 1452 example.com
```
Если 1400 проходит, а 1452 — нет, реальный MTU где-то между 1428 и 1480. У прямого клиента на Ethernet обычно 1500. У прокси — 1420 или ниже. Разница в 80 байт на каждом пакете складывается в характерный профиль ретрансмиссий.
Профиль ретрансмиссий и RTT-аномалии
TCP-стек шлёт ретрансмит, если не получил ACK за RTO. На прямом канале RTO стабильный, потери редкие. На прокси-цепочке потери выше, RTO скачет, а паттерн ретрансмитов становится «дырявым» — то густо, то пусто. Антифрод смотрит на распределение интервалов между ретрансмитами и на их долю от общего числа пакетов.
Конкретика: у прямого клиента доля ретрансмитов на TLS-хендшейке — 0.1-0.5%. У прокси с плохим аплинком — 2-5%. При этом медианный RTT может быть нормальным, маскируя проблему. Именно поэтому антифрод смотрит на хвост распределения (p95, p99), а не на среднее. Хвост выдаёт туннель.
Ещё маркер — корреляция RTT с размером пакета. На нормальном канале RTT почти не зависит от размера (до MTU). На канале с фрагментацией крупные пакеты идут дольше, потому что собираются из фрагментов. Если антифрод видит, что RTT на 1400-байтных пакетах в 2-3 раза выше, чем на 500-байтных, — это фрагментация, а фрагментация — почти всегда туннель.
Асимметрия задержек между хопами
Прокси-цепочка асимметрична: запрос идёт через один набор узлов, ответ — через другой. Иногда потому что провайдер так настроил маршрутизацию, иногда потому что выходной узел делает NAT и ответ идёт напрямую. В логах это видно как разница между временем отправки ClientHello и временем получения ServerHello, не соответствующая географии.
Пример: клиент заявляет IP в Германии, но RTT до сервера в Амстердаме — 180 мс. Прямой маршрут Германия → Амстердам — 8-15 мс. Разница в 10-20 раз означает, что трафик реально идёт из другого места. Антифрод сверяет RTT с геолокацией IP и ловит расхождение.
Как выглядит детект в логах балансировщика
Реальный пример: nginx 1.24 с `\$request_time` и `\$upstream_connect_time` в логах. Антифрод-скрипт парсит лог и считает статистику по IP-блокам /64.
```nginx
log_format antifraud '\$remote_addr - \$remote_user [\$time_local] '
'"\$request" \$status \$body_bytes_sent '
'rt=\$request_time uct=\$upstream_connect_time '
'urt=\$upstream_response_time';
access_log /var/log/nginx/access.log antifraud;
```
Дальше питон читает лог и группирует:
```python
import re
from collections import defaultdict
pat = re.compile(r'^([0-9a-f:]+) .* rt=([\d.]+) uct=([\d.]+)')
stats = defaultdict(list)
for line in open('/var/log/nginx/access.log'):
m = pat.match(line)
if not m:
continue
ip, rt, uct = m.group(1), float(m.group(2)), float(m.group(3))
prefix = ':'.join(ip.split(':')[:4])
stats[prefix].append((rt, uct))
for prefix, vals in stats.items():
if len(vals) < 50:
continue
rts = sorted(v[0] for v in vals)
p95 = rts[int(len(rts) * 0.95)]
if p95 > 1.2:
print(f"{prefix} p95={p95:.2f}s n={len(vals)}")
```
Блок с p95 выше 1.2 секунды при медиане 0.2 — подозрительный. Дальше смотрят, что за клиенты в этом /64, и сверяют с JA3-профилями.
Реальные кейсы из практики
**Кейс с банковским антифродом.** Банк в Восточной Европе ловил мошеннические логины. Клиенты шли через IPv6-прокси с выходом в Нидерландах. IP чистые, JA3 совпадал с Chrome. Что выдало: MTU 1420 вместо 1500 у всех «клиентов» из одного /64. Плюс RTT до банковского сервера 120-140 мс при заявленной геолокации в той же стране, где сервер стоял. Прямой путь был бы 5-10 мс. Решение: банк добавил фичу «MTU + RTT vs гео» в скоринг, доля фрода по этому вектору упала на 60% за месяц.
**Кейс с e-commerce.** Магазин на nginx 1.24 видел всплеск регистраций с IPv6. Анализ логов показал: у 70% подозрительных сессий `upstream_connect_time` был выше 0.4 с при медиане 0.08. Причина — прокси-провайдер гнал трафик через двойной хоп (Сингапур → Франкфурт). Антифрод добавил порог по `upstream_connect_time` p95 на /64-блок, и всплеск ушёл. Ложноположительных оказалось мало: реальные клиенты из того же региона укладывались в 0.15 с.
**Кейс с игровой платформой.** Платформа ловила мультиаккаунты. Прокси-провайдер ротировал IPv6 каждые 5 минут, но MTU у всех сессий был 1280 — минимум для IPv6. Прямые клиенты на мобильных сетях тоже иногда имеют 1280, но у них RTT до сервера стабильный и коррелирует с типом сети (LTE/5G). У прокси RTT скакал от 40 до 300 мс без корреляции с чем-либо. Платформа построила модель на этих двух фичах — MTU и дисперсия RTT — и отсекла 80% мультиаккаунтов. Остальные 20% доразбирали вручную.
Почему комбинация фич сильнее каждой по отдельности
По отдельности каждая фича шумит. JA3 подделывается. MTU иногда 1280 у мобильных. RTT скачет у всех. Но вместе они образуют узкий кластер: поддельный JA3 + MTU 1420 + RTT-дисперсия 200 мс + расхождение RTT с гео — это уже не совпадение. Антифрод-модели (градиентный бустинг, нейросети) как раз и натренированы на такие комбинации.
Провайдеры прокси отвечают тем, что пытаются выровнять профиль: настраивают MTU 1500 на выходных узлах, добавляют джиттер в тайминги, маскируют JA3 под популярные браузеры. Гонка продолжается. Но выровнять всё сразу дорого: каждый фикс увеличивает задержку или снижает пропускную способность. Вот здесь и появляется ниша для сервисов, которые держат инфраструктуру в одном дата-центре с целевыми сайтами — чтобы RTT был честно низким, а MTU — честно 1500. Например, lexic.ml держит выходные узлы в европейских ДЦ с прямым аплинком, и MTU там не режется туннелями, а RTT до популярных сервисов остаётся в пределах 10-30 мс.
Что делать, если ты на стороне клиента
Если ты используешь прокси и не хочешь палиться — проверяй свой профиль сам. Снимай RTT-серию, смотри MTU, сверяй JA3. Вот минимальный чек-лист в виде скрипта:
```python
import subprocess, statistics
def check_profile(host):
rtts = []
for _ in range(10):
out = subprocess.run(
["ping6", "-c", "1", "-W", "2", host],
capture_output=True, text=True
).stdout
for line in out.splitlines():
if "time=" in line:
rtts.append(float(line.split("time=")[1].split()[0]))
if rtts:
print(f"median={statistics.median(rtts):.1f}ms "
f"p95={sorted(rtts)[int(len(rtts)*0.95)]:.1f}ms "
f"stdev={statistics.stdev(rtts):.1f}ms")
check_profile("example.com")
```
Если stdev больше 30% от медианы — канал нестабильный, антифрод это заметит. Если медиана сильно выше ожидаемой по гео — тоже маркер. MTU проверяется через `ping6 -M do -s`, как выше.
Куда движется детект
Антифрод уходит от статических правил к моделям на потоковых фичах. Смотрят не на один хендшейк, а на последовательность: как меняется RTT между запросами, как ведёт себя MTU при разных размерах пакетов, как коррелируют тайминги с геолокацией. Прокси-провайдеры отвечают выравниванием профиля, но это дорого. В итоге выигрывает тот, у кого инфраструктура физически ближе к цели и без лишних туннелей — потому что честный профиль подделать не нужно, он и так честный.