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

Как антифрод ловит IPv6-прокси по TLS-отпечаткам

Как антифрод ловит IPv6-прокси по TLS-отпечаткам

Антифрод маркетплейсов давно перестал смотреть только на IP. IPv6-адресов в пулах миллиарды, блокировать подсети бессмысленно — завтра прокси переедет на соседний /64. Поэтому в ход пошёл TLS-слой. Именно там прокси оставляют следы, которые не спрятать сменой адреса.

Почему IPv6 всё усложнил

С IPv4 было просто: серые списки, ASN-фильтры, проверка на датацентровые диапазоны. С IPv6 картина другая. Один провайдер может выдать клиенту /64 — это 18 квинтиллионов адресов. Ротация внутри подсети выглядит как «новый пользователь» для наивного антифрода, но не для того, кто читает TLS.

Датацентровые IPv6-диапазоны всё ещё палятся по whois и rDNS. Но резидентные прокси на IPv6 — уже головная боль: адрес жилой, ASN домашнего провайдера, а поведение — как у бота. Тут и включается анализ хендшейка.

Что вообще видит сервер в TLS

ClientHello — первое сообщение. В нём клиент отдаёт: версию TLS, список cipher suites, extensions, группы эллиптических кривых, форматы точек, ALPN, SNI. Порядок и состав этих полей формирует отпечаток. JA3 — хеш от версии, шифров, extensions, кривых и форматов точек. JA4 — новее, стабильнее, учитывает больше.

Сервер маркетплейса считает этот хеш на каждом коннекте. Дальше — сверка с базой известных клиентов. Chrome 124 даёт один JA3, curl 8.x — другой, python-requests — третий. Прокси, который терминирует TLS и переоткрывает его к цели, часто ломает отпечаток: клиент думает, что говорит с Chrome, а на выходе — Go-клиент прокси.

Где прокси выдаёт себя

Первый маркер — несоответствие User-Agent и TLS-отпечатка. Браузер заявляет Chrome/124, а JA3 соответствует Go net/http. Антифрод это ловит мгновенно: реальный Chrome никогда не отдаст такой набор шифров и порядок extensions.

Второй — отсутствие или искажение GREASE. Chrome и Firefox с 2019 года вставляют случайные GREASE-значения в списки шифров и extensions. Прокси, собранный на старом openssl или на кастомном стеке, их не шлёт. Пустой GREASE — красный флаг.

Третий — ALPN. Браузер предлагает h2 и http/1.1. Прокси на голом TCP часто отдаёт только http/1.1 или вообще ничего.

Четвёртый — порядок extensions. У Chrome жёсткий порядок с 2021 года (permute_extensions появился позже, но паттерн узнаваем). У прокси extensions идут в порядке вставки в код.

Таблица: типичные отпечатки

| Клиент | JA3 (сокращённо) | GREASE | ALPN | Версия TLS |

|---|---|---|---|---|

| Chrome 124 | 1d19d9c4... | да | h2, http/1.1 | 1.3 |

| Firefox 125 | 5f4b8e1c... | да | h2, http/1.1 | 1.3 |

| curl 8.5 | 3b5074b1... | нет | http/1.1 | 1.3 |

| Go net/http 1.22 | 6d3f1c2a... | нет | h2 | 1.3 |

| python-requests 2.31 | 8c1e0f3b... | нет | нет | 1.2/1.3 |

Цифры хешей условные, но паттерн реален: Go и python стабильно палятся по отсутствию GREASE и бедному набору extensions.

Как антифрод строит правило

Маркетплейс держит таблицу «UA → допустимые JA3». Пришёл запрос с UA Chrome/124, а JA3 не из списка — скоринг растёт. Плюс проверка на согласованность: если TLS 1.3, но нет supported_versions в extensions — это подделка.

Дальше — корреляция по времени. Один IPv6-адрес делает 200 запросов за минуту с идеальным Chrome-отпечатком. Живой человек так не может. Отпечаток чистый, а поведение — нет. Антифрод ловит на втором слое.

Ещё один слой — TCP-фингерпринт (p0f). TTL, размер окна, MSS. Прокси-сервер на Linux с дефолтным стеком даёт окно 29200, а Windows-клиент заявляет 64240. Несовпадение ОС в UA и в TCP — ещё баллы к скору.

Кейс: маркетплейс на nginx 1.24 и Go-прокси

Проблема: пул IPv6-прокси на Go-клиенте, UA подставлялся браузерный, но 40% запросов уходили в капчу.

Причина: Go net/http по умолчанию не шлёт GREASE и отдаёт урезанный ClientHello. JA3 не совпадал с Chrome, хотя UA был Chrome.

Детали: nginx 1.24 с `ssl_protocols TLSv1.2 TLSv1.3`, логирование через `\$ssl_cipher` и кастомный модуль JA3. В логах — 38% коннектов с JA3 Go-клиента при UA Chrome/124.

Решение: перевели прокси на `utls` с ClientHello-профилем Chrome, включили GREASE и permute extensions. Доля капчи упала до 6%.

```go

package main

import (

"net/http"

"github.com/refraction-networking/utls"

)

func main() {

cfg := &utls.Config{ServerName: "marketplace.example"}

conn, err := utls.Dial("tcp", "marketplace.example:443", cfg)

if err != nil {

panic(err)

}

defer conn.Close()

client := &http.Client{

Transport: &http.Transport{

DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {

return utls.UClient(conn, cfg, utls.HelloChrome_Auto), nil

},

},

}

client.Get("https://marketplace.example/api/v1/cart")

}

```

Ключевое — `HelloChrome_Auto`: utls сам собирает актуальный Chrome-профиль с GREASE и правильным порядком extensions.

Кейс: python-requests за прокси

Проблема: скрипт на python-requests 2.31 через IPv6-прокси, 70% запросов — 403 от антифрода.

Причина: requests использует urllib3 поверх стандартного ssl. Отпечаток — бедный, без GREASE, TLS 1.2 по умолчанию в старых сборках. Плюс заголовки: `Accept-Encoding: identity` вместо браузерного.

Детали: сервер отдавал 403 с телом `{"error":"bot_detected"}` при `User-Agent: Mozilla/5.0 ... Chrome/124`. Сверка JA3 показала python-ssl.

Решение: перешли на `curl_cffi`, который эмулирует браузерный TLS через libcurl-impersonate.

```python

from curl_cffi import requests

session = requests.Session(impersonate="chrome124")

resp = session.get(

"https://marketplace.example/api/v1/orders",

proxies={"https": "http://[2001:db8::1]:8080"},

headers={"Accept-Language": "ru-RU,ru;q=0.9"},

)

print(resp.status_code)

```

`impersonate="chrome124"` подставляет реальный Chrome ClientHello: GREASE, порядок extensions, ALPN h2. 403 ушли.

Кейс: nginx как терминирующий прокси

Проблема: свой прокси на nginx, TLS терминировался на нём, к цели шёл второй TLS. Антифрод видел отпечаток nginx, а не клиента.

Причина: nginx при проксировании открывает новое TLS-соединение со своим ClientHello. Клиентский отпечаток терялся полностью.

Детали: nginx 1.24, `proxy_pass https://backend`, без `proxy_ssl_*` настроек. JA3 на бэкенде — стабильно nginx-овский, все клиенты выглядели одинаково.

Решение: либо pass-through на L4 (stream-модуль), либо `proxy_ssl_*` с профилем. Чище — первое.

```nginx

stream {

upstream backend {

server marketplace.example:443;

}

server {

listen [::]:8443;

proxy_pass backend;

proxy_ssl on;

proxy_ssl_protocols TLSv1.2 TLSv1.3;

}

}

```

В stream-режиме nginx не трогает TLS, ClientHello идёт как есть. Отпечаток клиента сохраняется.

Что реально менять в конфигурации клиента

Если работаешь через Go — utls с профилем Chrome или Firefox. Если Python — curl_cffi с `impersonate`. Если Node — `node-tls-client` или undici с кастомным TLS. Голый requests и голый Go net/http — гарантированный палево.

Проверять отпечаток до продакшена. Есть публичные эндпоинты вроде `tls.peet.ws` или `browserleaks.com/tls`, которые отдают твой JA3/JA4. Гоняй через них конфиг до того, как пускать трафик на маркетплейс.

Сервис lexic.ml даёт IPv6-пулы с 2015 года, и на практике связка «чистый TLS-отпечаток + ротация внутри /64» работает лучше, чем попытки подделать всё сразу. Адрес меняется, отпечаток остаётся браузерным — антифрод не видит паттерна.

Согласованность важнее подделки

Самый частый косяк — подделали JA3, но забыли про остальное. Chrome-отпечаток, а заголовки `Accept-Encoding: gzip` без `br` и `zstd`. Chrome всегда шлёт br. Или HTTP/2 не согласован: ALPN обещает h2, а клиент шлёт HTTP/1.1. Антифрод сверяет всё.

Проверяй связку: UA ↔ JA3 ↔ ALPN ↔ заголовки ↔ TCP-фингерпринт. Расхождение в любом звене — баллы к скору. Идеальная подделка одного слоя при кривом втором только привлекает внимание.

Ещё нюанс — версия TLS. Chrome 124 предпочитает TLS 1.3. Если клиент вдруг отдаёт 1.2 с браузерным UA — вопрос. Проверяй `supported_versions` в ClientHello, а не только negotiated версию.

Итог

IPv6-адрес давно не главный маркер. Антифрод читает TLS-хендшейк, сверяет с базой клиентов, ловит несоответствия UA и отпечатка, смотрит на GREASE, ALPN, порядок extensions. Прокси на стандартных библиотеках палится на первом же коннекте.

Менять надо не IP, а стек. utls для Go, curl_cffi для Python, pass-through для nginx. И проверять отпечаток на публичных эндпоинтах до продакшена. Ротация IPv6 внутри /64 плюс честный браузерный TLS — рабочая связка. Подделка одного слоя при кривом другом — только хуже.

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