Почему антифрод-системы маркетплейсов палят IPv6 прокси по TTL и MSS: технический разбор
Содержание
- Что вообще проверяет антифрод на транспортном уровне
- TTL как маркер расстояния и его подмена
- MSS и почему он выдаёт ядро прокси
- Комбинация TTL + MSS + окно TCP
- Как IPv6 усугубляет ситуацию по сравнению с IPv4
- Пример: как выглядит палево в логах балансировщика
- Кейс: маркетплейс с 50 000 сессий в час
- Кейс: residential-прокси на IPv6 и его провал
- Кейс: IPv6-префикс и геолокация
- Практика: как проверить свой прокси на палево
- Что делать: чеклист для прокси-сервиса
- Почему это гонка без финиша
Что вообще проверяет антифрод на транспортном уровне
Большинство статей про палево прокси крутится вокруг HTTP-заголовков: User-Agent, Accept-Language, порядок заголовков. Это уровень L7, и его антифрод-системы маркетплейсов давно научились нормализовать. Настоящая мясорубка начинается ниже — на L3 и L4, куда разработчики прокси-сервисов лезут неохотно, потому что там нельзя просто подменить строку.
Когда вы подключаетесь к маркетплейсу через IPv6-прокси, TCP-сессия проходит через цепочку: ваш клиент → прокси → целевой сервер. Каждый хоп этой цепочки оставляет отпечатки, которые нельзя убрать заголовком. TTL пакета, размер MSS в SYN, окно TCP, опции TCP — всё это формируется ядром операционной системы и маршрутизаторами по пути. Антифрод маркетплейса видит не ваш браузер, а совокупность этих параметров на входе своего балансировщика.
Проблема IPv6 в том, что адресное пространство огромное, но структура аллокации предсказуемая. Провайдеры выдают /64 или /56 префиксы, и эти префиксы зарегистрированы в RIR. Антифрод знает: этот /48 принадлежит хостингу, этот /64 — residential ISP. И когда с одного /64 приходит 500 сессий с одинаковым TTL и одинаковым MSS — это уже не статистика, это подпись прокси-пула.
TTL как маркер расстояния и его подмена
TTL (Time To Live) в IPv6 называется Hop Limit. Поле 8 бит, максимум 255. Каждый маршрутизатор уменьшает его на единицу. Когда счётчик доходит до нуля — пакет дропается, отправителю уходит ICMPv6 Time Exceeded.
Начальное значение Hop Limit задаёт ядро отправителя:
- Linux: 64
- Windows: 128
- Cisco IOS: 255
- Некоторые BSD: 64
Целевой сервер видит не начальный TTL, а остаточный. Если от клиента до сервера 12 хопов, Linux-пакет придёт с TTL=52. Windows-пакет — с 116. Антифрод строит карту: какие значения TTL типичны для residential-подключений в этом регионе, какие — для датацентров.
Прокси-сервер, который просто форвардит пакеты, не меняет Hop Limit. Клиент с Linux отправил TTL=64, прокси пробросил, сервер получил 64 минус хопы от прокси до сервера. Если прокси в датацентре во Франкфурте, а маркетплейс в Москве — это 15-20 хопов. Остаточный TTL будет 44-49. Для residential-клиента из Москвы с тем же Linux — 5-7 хопов, TTL 57-59.
Разница в 10 единиц — это уже сигнал. А если с одного IPv6-адреса приходят сессии с TTL 44, 45, 46, 47 — антифрод понимает, что за адресом сидит пул, и хопы до разных клиентов разные.
Подмена TTL делается на уровне ядра через iptables/nftables или на уровне прокси-демона. В nftables:
```bash
nft add table ip6 mangle
nft add chain ip6 mangle prerouting '{ type filter hook prerouting priority mangle; }'
nft add rule ip6 mangle prerouting tcp dport 443 counter ttl set 64
```
Но тут грабли: если вы ставите фиксированный TTL=64, а реальный клиент был Windows с TTL=128, вы создаёте новую аномалию. Антифрод видит: с этого /64 все сессии приходят с TTL=64, хотя User-Agent говорит Windows. Несоответствие. Нужна карта соответствия OS → TTL, и она должна применяться по сессии, а не глобально.
MSS и почему он выдаёт ядро прокси
MSS (Maximum Segment Size) — опция TCP, объявляется в SYN-пакете. Это максимальный размер payload сегмента, который отправитель готов принять. MSS = MTU - 40 для IPv6 (20 байт IPv6-заголовок + 20 байт TCP-заголовок).
Стандартные значения:
- Ethernet MTU 1500 → MSS 1460
- PPPoE MTU 1492 → MSS 1452
- IPv6 туннель (6in4) MTU 1480 → MSS 1440
- WireGuard MTU 1420 → MSS 1380
Прокси-серверы часто работают в контейнерах или через туннели. Docker по умолчанию ставит MTU 1500 на bridge, но если хост в WireGuard — MTU 1420, и MSS будет 1380. Антифрод маркетплейса видит: клиент из residential-сети с MSS 1380 — это нетипично. Residential обычно 1460 или 1452.
Ещё хуже: если прокси-сервер не делает MSS clamping, а клиент за туннелем — может случиться PMTU blackhole. SYN проходит, данные не идут. Прокси-сервис ставит MSS clamping на границе, но ставит его криво — например, фиксирует 1360 для всех. Тогда все сессии через этот прокси имеют MSS 1360, независимо от реального клиента. Это отпечаток.
Правильный подход — MSS clamping по интерфейсу с учётом реального MTU:
```bash
nft add rule ip6 mangle prerouting tcp flags syn tcp option maxseg size set rt mtu
```
Это устанавливает MSS в зависимости от MTU маршрута. Но `rt mtu` работает не везде — в некоторых ядрах требует CONFIG_IPV6_ROUTE_INFO. Проверять надо на конкретной версии.
Комбинация TTL + MSS + окно TCP
По отдельности TTL и MSS — слабые сигналы. В комбинации — сильные. Антифрод маркетплейса строит fingerprint по тройке: TTL, MSS, TCP window size.
Типичные профили:
- Windows 10/11: TTL 128, MSS 1460, window 64240
- Linux 5.x: TTL 64, MSS 1460, window 65495 (с window scaling)
- macOS: TTL 64, MSS 1460, window 65535
- Android: TTL 64, MSS 1460, window 65535
Если прокси-сервер на Linux форвардит трафик Windows-клиента, но не трогает TTL — сервер видит TTL 128 минус хопы. Если трогает TTL, но не трогает window — видит Linux-window с Windows-TTL. Антифрод ловит несоответствие.
Плюс есть TCP timestamps, SACK, window scaling factor. Всё это в SYN и SYN-ACK. Полный fingerprint — это 8-12 параметров. Подделать все на прокси-сервере можно, но это уже не прокси, а полноценный TCP-стек с эмуляцией.
Как IPv6 усугубляет ситуацию по сравнению с IPv4
В IPv4 адресное пространство 4.3 млрд, и пулы прокси-сервисов часто используют residential IP — реальные адреса домашних пользователей. Антифрод видит: адрес принадлежит Ростелекому, TTL 57, MSS 1452 — похоже на реального клиента.
В IPv6 адресное пространство 3.4×10^38, но аллокация структурирована. Провайдер выдаёт /56 или /64. Прокси-сервис арендует /48 у хостинга и раздаёт /64 клиентам. Антифрод видит: с /64 приходит 200 сессий. В IPv4 это было бы 200 разных /32 — выглядит как 200 клиентов. В IPv6 это один /64 — выглядит как один клиент с 200 сессиями. Это палево.
Плюс IPv6-префиксы хостингов зарегистрированы в RIR с флагом "hosting". Антифрод маркетплейса делает whois-запрос к RIR, видит "NetName: HOSTING-XX", и помечает весь /48 как датацентр. Residential-прокси на IPv6 почти не существует — домашние провайдеры выдают /56, но не продают их для прокси.
Пример: как выглядит палево в логах балансировщика
Возьмём nginx 1.24 с модулем stream и логированием на уровне TCP. Настраиваем логирование TTL и MSS через переменные:
```nginx
stream {
log_format tcp_basic '\$remote_addr:\$remote_port '
'\$time_iso8601 '
'\$bytes_sent \$bytes_received '
'\$session_time '
'\$upstream_addr';
server {
listen [::]:443;
proxy_pass backend;
access_log /var/log/nginx/tcp.log tcp_basic;
}
}
```
Nginx не отдаёт TTL и MSS напрямую. Их надо снимать через eBPF или tcpdump. Пример на bpftrace:
```bash
bpftrace -e '
kprobe:tcp_v6_rcv {
\$skb = (struct sk_buff *)arg0;
\$iph = (struct ipv6hdr *)(\$skb->data + \$skb->network_header);
\$tcph = (struct tcphdr *)(\$skb->data + \$skb->transport_header);
if (\$tcph->syn == 1 && \$tcph->ack == 0) {
printf("%s TTL=%d MSS=%d WIN=%d\n",
ntop(\$iph->saddr),
\$iph->hop_limit,
\$tcph->doff,
\$tcph->window);
}
}'
```
Это даёт поток SYN-пакетов с TTL, размером TCP-заголовка (косвенно MSS через опции) и окном. Прогоняем через этот скрипт трафик с прокси-пула — и видим кластеры. Если 500 сессий с одного /64 имеют TTL 44-49 и MSS 1380 — это прокси за туннелем.
Кейс: маркетплейс с 50 000 сессий в час
Маркетплейс электроники, балансировщик на базе Envoy 1.28, антифрод на собственном стеке. Пул прокси-сервиса — 2000 IPv6-адресов из /48, арендованного у Hetzner. Прокси на Debian 12, ядро 6.1, форвардинг через nftables без изменения TTL.
Первая неделя: антифрод пропускает 80% трафика. Вторая неделя: палево растёт, блокировки 60%. Причина — антифрод накопил статистику по /48. Все 2000 адресов имеют одинаковый TTL после прохождения хопов до балансировщика: 47 (Hetzner → маркетплейс 17 хопов, начальный TTL 64). MSS 1460, окно 65495. Это Linux-профиль, но User-Agent в 70% случаев Windows.
Решение прокси-сервиса: добавить подмену TTL и MSS на уровне nftables с картой по сессии. Но карта требует знания OS клиента, а прокси видит только TCP-сессию. Пришлось делать эвристику по первому HTTP-запросу: если User-Agent Windows — ставить TTL 128 и MSS 1460, если Linux — 64 и 1460. Задержка на первом пакете выросла на 3-5 мс, но палево упало до 15%.
Кейс: residential-прокси на IPv6 и его провал
Сервис residential-прокси арендовал /56 у немецкого ISP, раздал /64 клиентам. Клиенты — реальные пользователи с домашними роутерами. Проблема: роутеры часто имеют MTU 1492 (PPPoE), MSS 1452. Но прокси-сервер на входе делал MSS clamping до 1440 (запас под туннель). Все сессии приходили с MSS 1440, хотя реальные клиенты имели 1452.
Антифрод маркетплейса заметил: с /56 приходит 300 сессий с MSS 1440, TTL 61-63. Для residential это нетипично — обычно MSS 1452. Плюс TTL 61-63 означает 1-3 хопа от клиента до сервера, а residential-клиенты разбросаны по стране, хопов должно быть 5-15. Вывод: это не residential, это прокси-ферма, эмулирующая residential.
Решение: убрать MSS clamping, пробросить реальный MSS клиента. Но тогда PMTU blackhole на туннелях. Пришлось ставить clamping только для клиентов с MTU < 1400, а для остальных пробрасывать. Сложность выросла, но палево упало.
Кейс: IPv6-префикс и геолокация
Маркетплейс с антифродом на базе MaxMind GeoIP2. Прокси-сервис арендовал /48 в Нидерландах, но клиенты — из России. Антифрод видит: IPv6-адрес из NL, TTL 50 (10 хопов от NL до RU), MSS 1460. User-Agent русский, Accept-Language ru-RU, но IP нидерландский.
Первая линия антифрода — геолокация по IP. Прокси-сервис ставит IPv6-адреса, зарегистрированные в RU. Но RIR-регистрация не совпадает с физическим расположением. Антифрод делает traceroute: видит, что первый хоп после прокси — Amsterdam. Геолокация по traceroute точнее, чем по whois.
Решение: использовать IPv6-адреса, физически расположенные в RU. Но российские хостинги редко продают /48 для прокси. Приходится брать /64 у мелких ISP, которые не засвечены в базах антифрода. Это гонка вооружений.
Практика: как проверить свой прокси на палево
Собираем fingerprint на стороне сервера. Запускаем tcpdump на балансировщике, парсим SYN-пакеты:
```bash
tcpdump -i eth0 -nn -c 1000 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0' -w syn.pcap
tshark -r syn.pcap -T fields -e ipv6.src -e ipv6.hlim -e tcp.options.mss_val -e tcp.window_size_value
```
Получаем таблицу. Группируем по /64 префиксу. Если видим кластер с одинаковым TTL и MSS — это прокси. Дальше сравниваем с эталонным профилем реального клиента из целевого региона.
Python-скрипт для агрегации:
```python
import subprocess
from collections import defaultdict
def get_syn_fingerprints(iface, count=1000):
cmd = [
'tshark', '-i', iface, '-c', str(count),
'-f', 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0',
'-T', 'fields',
'-e', 'ipv6.src', '-e', 'ipv6.hlim',
'-e', 'tcp.options.mss_val', '-e', 'tcp.window_size_value'
]
out = subprocess.check_output(cmd, text=True)
clusters = defaultdict(list)
for line in out.strip().split('\n'):
parts = line.split('\t')
if len(parts) < 4:
continue
src, ttl, mss, win = parts
prefix = ':'.join(src.split(':')[:4])
clusters[prefix].append((ttl, mss, win))
return clusters
for prefix, fps in get_syn_fingerprints('eth0').items():
if len(fps) > 50:
ttls = set(f[0] for f in fps)
msss = set(f[1] for f in fps)
print(f"{prefix}/64: {len(fps)} sessions, TTL={ttls}, MSS={msss}")
```
Если вывод показывает `2000:abcd:1234:5678/64: 500 sessions, TTL={'47'}, MSS={'1460'}` — это прокси-пул без подмены TTL.
Что делать: чеклист для прокси-сервиса
Первое — подмена TTL по сессии. Не глобально, а с картой OS → TTL. Карту строить по User-Agent первого HTTP-запроса или по TCP-опциям (Windows ставит timestamp с определённым паттерном).
Второе — MSS clamping только там, где нужно. Не фиксировать MSS для всех. Пробрасывать реальный MSS клиента, если MTU позволяет. Если туннель — clamping по MTU маршрута, а не по константе.
Третье — окно TCP. Linux и Windows имеют разные начальные окна. Если прокси на Linux форвардит Windows-клиента, окно в SYN-ACK будет Linux. Нужно подменять.
Четвёртое — IPv6-префиксы. Не использовать один /64 для сотен клиентов. Разбивать на /64, каждый — один клиент. Но это дорого: /48 = 65536 /64. Аренда /48 у RIR — от \$2000 в год.
Пятое — геолокация. IPv6-адрес должен быть физически в регионе клиента. Проверять через traceroute и RIPE Atlas.
Шестое — мониторинг. Раз в сутки прогонять свой пул через tcpdump-анализ и смотреть, не появилось ли кластеров. Антифрод обновляет базы, ваш fingerprint должен меняться.
Почему это гонка без финиша
Антифрод маркетплейса и прокси-сервис играют в кошки-мышки. Антифрод добавляет новый параметр в fingerprint — прокси-сервис учится его подменять. Антифрод добавляет корреляцию между параметрами — прокси-сервис строит карту соответствия. Антифрод добавляет машинное обучение на временных рядах — прокси-сервис начинает эмулировать поведение.
На уровне TTL и MSS это уже пройденный этап для крупных сервисов. lexic.ml, работающий с 2015 года, видел эту эволюцию: в 2016 хватало подмены User-Agent, в 2019 — TTL, в 2022 — комбинации TTL+MSS+window, в 2024 — поведенческих паттернов. Каждый новый слой отсеивает часть пулов. Выживают те, кто эмулирует полный стек, а не отдельные поля.
Практический вывод: если вы строите прокси-сервис на IPv6, не экономьте на подмене L3/L4. Один /64 на клиента, карта OS → TTL/MSS/window, геолокация по traceroute, мониторинг fingerprint раз в сутки. Иначе антифрод маркетплейса отсеет пул за неделю-две, и вы даже не поймёте, почему.