Как антифрод-системы маркетплейсов вычисляют IPv6-прокси по паттернам портов и TTL
Содержание
- Почему IPv6-прокси палится легче
- TTL как отпечаток операционной системы
- Паттерны портов источника
- Согласованность TCP-опций
- Кейс: пул /64 с одинаковым Hop Limit
- Кейс: последовательные порты из SOCKS5
- Кейс: несоответствие MTU и MSS
- Что делать провайдеру прокси
- Как антифрод собирает сигналы в скоринг
- Ограничения метода
- Итог для тех, кто строит защиту
Маркетплейсы научились ловить прокси не по чёрным спискам IP, а по поведению стека. Адрес меняется, подсеть меняется, а TCP-отпечаток и TTL остаются. Разбираю, как антифрод строит сигнатуры и почему IPv6 тут даёт больше зацепок, чем IPv4.
Почему IPv6-прокси палится легче
В IPv4 пул адресов у провайдера прокси — это одна-две /24. Антифрод их быстро заносит в базу. В IPv6 провайдер выдаёт /64 на клиента, и формально у тебя 2^64 адресов. Казалось бы, рай для скрытности.
На практике всё наоборот. Антифрод смотрит не на отдельный адрес, а на префикс /64 и /48. Если с одной /64 за сутки пришло 800 регистраций с разных «устройств» — это не 800 человек. Это один прокси-пул. И тут вскрываются детали, которые в IPv4 никто не замечал: как именно устроены соединения внутри этой подсети.
Плюс IPv6 почти всегда означает конкретный хостинг. Жилой провайдер даёт /56 или /48, но если префикс принадлежит AS хостера (Hetzner, OVH, DigitalOcean) — вопрос закрыт. Антифрод сверяет BGP-анонс префикса с типом ASN за 3 мс.
TTL как отпечаток операционной системы
TTL (Time To Live) в IPv6-заголовке — это Hop Limit. Дефолтные значения по ОС: Linux — 64, Windows — 128, macOS — 64, Cisco IOS — 255. Пакет проходит через N маршрутизаторов, каждый уменьшает счётчик на 1.
Если прокси стоит на Linux-сервере в Германии, а клиент подключается из Москвы, исходный Hop Limit 64 пройдёт через 10-15 хопов и придёт как 49-54. Антифрод видит: браузер заявляет Windows 11 (User-Agent, Client Hints), а Hop Limit соответствует Linux-серверу. Несоответствие.
Есть нюанс. Многие прокси-решения нормализуют TTL, чтобы он выглядел «как у клиента». Но нормализация сама по себе — паттерн. Если у 500 сессий из одной /64 ровно Hop Limit 64 — это не 500 разных маршрутов до клиента, это один сервер с настроенным mangle.
Вот как выглядит проверка на стороне сервиса:
```python
import ipaddress
EXPECTED_HOP_LIMIT = {
"windows": 128,
"linux": 64,
"macos": 64,
"android": 64,
"ios": 64,
}
def check_hop_limit(observed_ttl, claimed_os, hop_estimate=0):
base = EXPECTED_HOP_LIMIT.get(claimed_os)
if base is None:
return None
expected = base - hop_estimate
delta = abs(observed_ttl - expected)
if delta > 8:
return f"ttl_mismatch: observed={observed_ttl} expected~{expected}"
if observed_ttl == base:
return "ttl_not_decremented: possible normalization"
return None
```
Порог 8 хопов — эмпирика. Меньше — много ложных срабатываний на мобильных операторах с длинными маршрутами. Больше — пропускаешь явные прокси.
Паттерны портов источника
Тут начинается интересное. Прокси-серверы (особенно SOCKS5 и HTTP CONNECT) при исходящих соединениях используют эфемерные порты из диапазона ядра. Linux по умолчанию берёт из `ip_local_port_range` — обычно 32768-60999. Windows — 49152-65535. macOS — 49152-65535.
Если маркетплейс видит, что все запросы из /64 приходят с source port в диапазоне 32768-60999 и при этом User-Agent заявляет Windows — сигнал. Windows-машина физически не может выдать такой порт. Это либо Linux-сервер напрямую, либо прокси на Linux.
Второй паттерн — последовательность портов. Ядро Linux выдаёт эфемерные порты по алгоритму, близкому к последовательному с шагом. Если собрать 20 соединений из одной /64 за минуту и увидеть source ports 41234, 41236, 41238, 41240 — это один NAT-шлюз с предсказуемой аллокацией. Живые клиенты так себя не ведут: у них порты разбросаны по всему диапазону, потому что между соединениями проходят часы и тысячи других сокетов.
Собрать статистику можно так:
```python
from collections import Counter
def analyze_port_distribution(sessions):
ports = [s["src_port"] for s in sessions]
if len(ports) < 10:
return None
in_linux_range = sum(1 for p in ports if 32768 <= p <= 60999)
in_win_range = sum(1 for p in ports if 49152 <= p <= 65535)
ratio_linux = in_linux_range / len(ports)
deltas = [b - a for a, b in zip(sorted(ports), sorted(ports)[1:])]
common_delta, freq = Counter(deltas).most_common(1)[0]
sequential = freq / len(deltas) > 0.4 and common_delta < 10
flags = []
if ratio_linux > 0.85:
flags.append(f"linux_port_range:{ratio_linux:.2f}")
if sequential:
flags.append(f"sequential_ports:delta={common_delta}")
return flags
```
Порог 85% выбран не случайно: живые пользователи на Linux тоже дают порты из этого диапазона, но у них разброс шире из-за долгих сессий и фоновых приложений.
Согласованность TCP-опций
TTL и порты — только часть картины. Прокси-серверы часто не пересобирают TCP-стек, а просто пересылают поток. Значит, TCP options (MSS, window scaling, SACK permitted, timestamps) приходят от серверной ОС, а не от клиентской.
Типичная сигнатура Linux-сервера: MSS 1460, window scaling factor 7, timestamps включены, SACK permitted. Windows 11 даёт MSS 1460, но window scaling 8 и другие timestamps. macOS — MSS 1440 (из-за другого MTU на некоторых интерфейсах).
Антифрод строит отпечаток из комбинации: TTL + TCP options + порядок опций. Это то, что делает p0f. Если отпечаток говорит Linux 5.x, а браузер заявляет Windows — прокси.
Кейс: пул /64 с одинаковым Hop Limit
Маркетплейс электроники, 40 тысяч регистраций в сутки. Антифрод заметил аномалию: за 6 часов с префикса 2a01:4f8:1c1c::/64 пришло 340 регистраций. Все с разными IPv6-адресами внутри /64, разные User-Agent, разные устройства по Client Hints.
Причина вскрылась при анализе Hop Limit. У всех 340 сессий observed_ttl был ровно 57. Ни одного отклонения. Реальные клиенты с таким же заявленным гео (Германия) давали разброс 54-58, потому что маршруты разные. Ровное значение 57 у всех — это нормализация на прокси-сервере: сервер ставил фиксированный TTL 64 на выходе, до маркетплейса доходило 57 хопов по магистрали.
Решение: добавили сигнал `ttl_variance < 1` для групп из более чем 50 сессий с одного /64. Ложных срабатываний на легитимных корпоративных NAT — 0.3%, потому что там TTL всё равно варьируется из-за разных путей до шлюза.
Кейс: последовательные порты из SOCKS5
Сервис бронирования, всплеск фейковых отзывов. 120 отзывов за час, все с разных IPv6 из /48, принадлежащего одному хостеру. Антифрод сначала смотрел на ASN — это сработало, но не объясняло, почему часть отзывов прошла с residential-прокси.
Копнули глубже. У прошедших отзывов source ports шли с шагом 2: 38210, 38212, 38214… Это классика Linux `__inet_hash_connect` при быстром открытии сокетов из одного процесса. Residential-прокси на деле оказался цепочкой: клиент → SOCKS5 на VPS → residential-выход. Порты на первом хопе выдавали VPS.
Решение: сигнал `sequential_ports` с весом 0.4 в скоринге. Порог срабатывания — 15+ соединений с шагом меньше 10 за 60 секунд с одной /64. Точность выросла: отсекли 92% фейков, потеряли 1.1% легитимных (в основном корпоративные API-клиенты, которые тоже открывают сокеты пачками).
Кейс: несоответствие MTU и MSS
Площадка с объявлениями. Прокси-пул на OVH, IPv6, разные адреса. Антифрод поймал по MSS. У всех сессий из /64 MSS был 1440, а не 1460. Это значит, что где-то в пути MTU 1480 вместо стандартных 1500 — типично для туннелей (GRE, IPIP) или PPPoE.
Проблема: заявленный клиент — мобильный Android в сети оператора, где MTU обычно 1500 или 1400 (для LTE с GTP-туннелями). MSS 1440 не бьётся ни с одним типичным профилем. Это выдало промежуточный туннель на стороне прокси.
Решение: добавили проверку MSS против заявленного типа сети. Для Wi-Fi — ожидание 1460, для LTE — 1400 или 1460, для 5G — 1420-1460. Отклонение больше 40 байт — сигнал. В этом кейсе отсекли пул за 2 дня, до того как он успел нагадить.
Что делать провайдеру прокси
Если ты держишь пул и хочешь жить — нужно нормализовать не только TTL, но и весь стек. Проблема в том, что нормализация сама становится сигнатурой, если делать её грубо.
Рабочий подход — рандомизация в пределах правдоподобного. TTL не фиксировать, а выставлять `base - random(8, 14)`, чтобы имитировать реальный путь. Порты — не полагаться на ядро, а биндить сокеты на случайные из диапазона, соответствующего заявленной ОС. MSS — подстраивать под профиль клиента.
Сервис lexic.ml с 2015 года крутит IPv6-прокси и на практике знает: клиенты, которые не заморачиваются со стеком, выгорают за дни. Те, кто нормализует — живут месяцами, но требуют индивидуальной настройки под каждую площадку.
Как антифрод собирает сигналы в скоринг
По отдельности каждый сигнал слабый. TTL-несоответствие бывает у 3-5% легитимных пользователей за корпоративным NAT. Последовательные порты — у API-клиентов. MSS-аномалия — у мобильных операторов с туннелями.
Сила в комбинации. Антифроды используют взвешенную сумму: TTL mismatch 0.3, port range mismatch 0.25, sequential ports 0.4, MSS mismatch 0.2, ASN hosting 0.5. Порог блокировки — 0.7-0.8. Один сигнал не блокирует, два-три — да.
Плюс временное окно. Сигналы считаются не по одной сессии, а по группе за окно 5-60 минут. Это отсекает шум и ловит именно паттерны пула.
Ограничения метода
TTL врёт на мобильных сетях. Операторы часто используют CGNAT с непредсказуемым числом хопов. Hop Limit может прийти 40 или 55 — и это нормально.
Порты врёт на IPv6 с privacy extensions. Ядро всё равно выдаёт эфемерные из своего диапазона, но при долгих сессиях и NAT64 картина размывается.
MSS врёт везде, где есть туннели. А их сейчас много: WireGuard, Tailscale, корпоративные VPN.
Так что антифрод никогда не блокирует по одному сигналу. Только по совокупности, и всегда с ручной проверкой для пограничных случаев.
Итог для тех, кто строит защиту
Смотри на стек, а не на адреса. IPv6 даёт миллиарды адресов, но стек у всех одинаковый по ОС. Собирай сигнатуры: Hop Limit, TCP options, MSS, source ports. Считай не по одной сессии, а по /64 за окно. Комбинируй сигналы в скоринг, не блокируй по одному.
И помни: нормализация стека на стороне прокси — это тоже сигнал. Ровные значения подозрительнее разбросанных.