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

TLS fingerprinting в прокси-серверах: почему JA3/JA4 выдает туннель даже при смене IP

TLS fingerprinting в прокси-серверах: почему JA3/JA4 выдает туннель даже при смене IP

Что вообще такое 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.

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