TLS fingerprinting в прокси-серверах: почему JA3/JA4 выдает туннель даже при смене IP
Содержание
- Что вообще такое JA3 и почему он появился
- JA4: что изменилось и почему JA3 мало
- Почему смена IP не спасает
- Как выглядит утечка на практике
- Где именно ломается цепочка
- Реальный пример: nginx как reverse proxy
- Как правильно мимикрировать
- Почему прокси с 2015 года всё ещё актуальны
- Таблица: что видит сервер
- Что делать, если вы уже спалились
Что вообще такое JA3 и почему он появился
Проблема стара как мир: клиент меняет IP, меняет User-Agent, чистит cookies — а его всё равно палят. Причина в том, что TLS-рукопожатие содержит кучу параметров, которые клиент отправляет ещё до того, как передаст хоть байт полезной нагрузки. Набор этих параметров называется ClientHello.
В 2017 году инженеры Salesforce (John Althouse, Jeff Atkinson, Josh Atkins) придумали сворачивать ClientHello в хеш. Получилось JA3. Идея простая: берёшь версию TLS, список cipher suites, список расширений, список эллиптических кривых и форматы точек — склеиваешь через запятую, считаешь MD5. На выходе 32-символьная строка.
```
769,47-53-5-10-49171-49172-50-56-19-4,0-10-11,23-24-25,0
```
Это реальный JA3 от Chrome 120 на Windows. Каждый браузер, каждая библиотека, каждый язык программирования генерируют свой уникальный отпечаток. Python requests с OpenSSL 3.0 даёт один JA3, Go net/http — другой, curl — третий. И вот тут начинается веселье.
Прокси-сервер, который просто перекидывает TCP-поток, не трогает TLS-рукопожатие. Клиент сам договаривается с сервером. Значит, JA3 клиента уходит на сервер как есть. И если вы сидите через VPN или SOCKS5-прокси с домашнего Chrome, а сервер ожидает увидеть миллион разных отпечатков от реальных пользователей — вы будете одним из немногих с "чистым" Chrome JA3 с одного IP. Это уже аномалия.
JA4: что изменилось и почему JA3 мало
К 2023 году FoxIO выпустили JA4. Старый JA3 имел три проблемы: MD5 (коллизии теоретически возможны), отсутствие учёта порядка расширений (некоторые серверы его проверяют) и полная слепота к ALPN и SNI. JA4 решает это иначе.
Формат JA4 выглядит так:
```
t13d1516h2_8daaf6152771_02713d6af862
```
Разберём: `t` — TCP, `13` — TLS 1.3, `d` — domain (есть SNI), `15` — 15 cipher suites, `16` — 16 extensions, `h2` — ALPN h2. Дальше два хеша (SHA256, усечённые) — по шифрам и по расширениям отдельно. Плюс JA4 игнорирует порядок cipher suites, потому что Chrome его рандомизирует с версии 110.
JA4 стал стандартом де-факто в 2024. Cloudflare, Fastly, AWS WAF — все перешли на него. Для прокси это означает: даже если вы подделали JA3, JA4 вас выдаст по ALPN, SNI-паттернам и порядку расширений.
Почему смена IP не спасает
Классическая ошибка — думать, что fingerprinting привязан к IP. Нет. JA3/JA4 — это свойство клиента, не сети. Сервер собирает отпечаток с каждого входящего соединения и складывает в базу. Дальше работает статистика.
Представьте: с 10 000 IP приходят запросы. У 9 500 из них JA4 начинается с `t13d1516h2_` — это реальные Chrome. У 400 — `t13d1715h2_` — Firefox. У оставшихся 100 — `t13d1517h2_` — что-то странное. Если эти 100 идут с 100 разных IP, но с одинаковым JA4 и одинаковым набором заголовков — это палево. Даже Cloudflare Turnstile такие паттерны ловит на раз.
Более того, есть корреляция JA4 + HTTP/2 fingerprint (SETTINGS-фрейм, порядок заголовков, приоритеты потоков) + TCP/IP fingerprint (TTL, window size, MSS). Смена IP ломает только последний. Первые два остаются.
Как выглядит утечка на практике
Возьмём типичную схему: Python-скрипт через SOCKS5-прокси. Клиент использует `requests` с urllib3. Рукопожатие делает OpenSSL 3.0.x. JA4 получается примерно такой:
```
t13d1715h2_5b57614c22b0_3d5424432f57
```
А теперь сравним с реальным Chrome 124:
```
t13d1516h2_8daaf6152771_02713d6af862
```
Разница в количестве extensions (17 vs 15) и в хешах. Сервер видит: "клиент утверждает, что он Chrome 124, но TLS-отпечаток не совпадает". Всё, бан.
Проверить свой отпечаток можно через curl к специальным сервисам:
```bash
curl -s https://tls.peet.ws/api/all | jq '.tls.ja3_hash, .tls.ja4'
```
Или через Python:
```python
import requests
r = requests.get("https://tls.peet.ws/api/all", timeout=10)
data = r.json()
print("JA3:", data["tls"]["ja3_hash"])
print("JA4:", data["tls"]["ja4"])
print("HTTP/2 fingerprint:", data["http2"]["akamai_fingerprint"])
```
Запустите это локально и через свой прокси. Если отпечатки совпадают — прокси прозрачный, вы палитесь. Если различаются — прокси подменяет TLS, что тоже палево (например, все клиенты через прокси получают одинаковый JA4).
Где именно ломается цепочка
Прокси бывают разные. HTTP-прокси (CONNECT) не трогает TLS — клиент сам делает рукопожатие, отпечаток уходит как есть. SOCKS5 — то же самое. А вот MITM-прокси (например, корпоративные) подменяют сертификат и делают своё рукопожатие с сервером. Тогда JA4 будет от прокси, а не от клиента.
Первый случай опасен тем, что выдаёт клиента. Второй — тем, что выдаёт прокси. Оба плохи.
Есть третий вариант — TLS-терминирующий прокси, который умеет мимикрировать. Например, uTLS (Go-библиотека) позволяет собрать ClientHello байт-в-байт как у Chrome. Тогда JA4 совпадёт. Но тут всплывает HTTP/2 fingerprint: порядок SETTINGS-параметров, значения INITIAL_WINDOW_SIZE, HEADER_TABLE_SIZE. Go net/http их выставляет по-своему, и Akamai fingerprint не совпадёт с Chrome.
Реальный пример: nginx как reverse proxy
Допустим, у вас nginx 1.24 терминирует TLS перед бэкендом. Конфиг:
```nginx
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
location / {
proxy_pass http://backend;
}
}
```
Клиент видит JA4 от nginx, а не от себя. Если nginx настроен стандартно, его JA4 стабилен для всех клиентов. Сервер за nginx теряет информацию о клиентском отпечатке. Это нормально для легитимного reverse proxy. Но если вы используете такой nginx как "анонимайзер" — все ваши запросы будут с одинаковым JA4, что само по себе подозрительно при большом объёме.
Проверить, что отдаёт ваш nginx:
```bash
openssl s_client -connect example.com:443 -tls1_3 -brief 2>&1 | head -20
```
Или через `nmap`:
```bash
nmap --script ssl-enum-ciphers -p 443 example.com
```
Как правильно мимикрировать
Если задача — не палиться, нужно контролировать три слоя: TLS ClientHello, HTTP/2 фреймы, HTTP-заголовки. Смена IP — четвёртый слой, наименее важный.
Для TLS есть uTLS (Go), curl-impersonate (форк curl с поддержкой Chrome/Firefox отпечатков), tls-client (Python-обёртка над Go). Пример с curl-impersonate:
```bash
curl-impersonate-chrome https://tls.peet.ws/api/all
```
Этот бинарник отправляет ClientHello, идентичный Chrome 124. JA4 совпадёт. Но HTTP/2 fingerprint всё равно может отличаться, если curl-impersonate не эмулирует SETTINGS-фрейм.
Для полной мимикрии нужен headless-браузер (Playwright, Puppeteer) с реальным Chrome. Тогда все три слоя совпадут. Минус — ресурсы: один инстанс Chrome жрёт 200-400 МБ RAM. На 1000 параллельных сессий это 400 ГБ. Не масштабируется.
Компромисс — uTLS + кастомный HTTP/2-клиент с подменой SETTINGS. В Go это делается через `golang.org/x/net/http2` с ручной настройкой `http2.Transport`. Работа муторная, но результат близок к идеалу.
Почему прокси с 2015 года всё ещё актуальны
Прокси-сервисы, которые работают давно, обычно имеют большую базу IP и разнообразный трафик. Это размывает статистику. Если через прокси идут тысячи разных клиентов с разными JA4 — ваш запрос тонет в шуме. Проблема возникает, когда прокси используется одним клиентом с одним отпечатком на тысячи запросов.
Например, lexic.ml с 2015 года крутит IPv6-прокси и за это время накопил пул адресов, которые не светились в спаме. Но даже большой пул не спасёт, если вы шлёте 10 000 запросов с одинаковым JA4 Chrome с разных IP. Сервер увидит: "одинаковый браузер, разные IP, одинаковый порядок заголовков" — и забаннит по паттерну, а не по IP.
Правильная стратегия: ротация не только IP, но и отпечатков. Минимум 5-10 разных JA4 в пуле. Тогда статистика размывается.
Таблица: что видит сервер
| Слой | Что проверяется | Меняется ли при смене IP |
|------|-----------------|--------------------------|
| TCP/IP | TTL, window size, MSS | Да, частично |
| TLS | JA3, JA4, порядок расширений | Нет |
| HTTP/2 | SETTINGS, приоритеты, порядок заголовков | Нет |
| HTTP | User-Agent, Accept-Language, порядок | Нет |
| Cookies | Session ID, fingerprinting cookies | Нет |
Смена IP влияет только на первый слой. Остальные четыре остаются неизменными. Именно поэтому "сменил IP — и всё заработало" перестало работать ещё году в 2020.
Что делать, если вы уже спалились
Первое — не паниковать. Второе — проверить, по какому именно слою вас поймали. Запустите тест на `tls.peet.ws` и `browserleaks.com/tls` через свой прокси. Сравните с эталонным Chrome.
Если JA4 не совпадает — меняйте TLS-стек. Если совпадает, но HTTP/2 fingerprint отличается — правьте SETTINGS-фрейм. Если всё совпадает, но банят — дело в поведении: частота запросов, паттерны навигации, отсутствие mouse-движений.
Третье — распределяйте нагрузку. Не шлите 1000 запросов с одного отпечатка. Разбейте на 10 групп по 100, каждой дайте свой JA4. Это не панацея, но снижает риск.
И последнее: не пытайтесь обмануть всё сразу. Начните с TLS, потом HTTP/2, потом поведение. Пошагово, с тестами после каждого шага. Иначе получится зоопарк из полуработающих костылей, который палится ещё сильнее, чем голый Python requests.