Почему антифрод TikTok ловит мультиаккаунты по TLS-отпечатку, даже если IP чистый
Содержание
- Что такое JA3 и почему он важнее IP
- Почему одного JA3 мало — TLS fingerprinting в 2024
- Как выглядит ClientHello вживую
- Почему прокси не спасает
- Таблица: что видит сервер на каждом уровне
- Кейс: ферма на requests через резидентные прокси
- Кейс: мобильный отпечаток на десктопном стеке
- Кейс: антидетект-браузер с кривым TLS
- Что реально проверять перед запуском
- Про IPv6 и пулы адресов
- Итог
IP — это уровень 3. Антифрод TikTok работает на уровнях 4-7. Прокси меняет адрес в пакете. Отпечаток TLS остаётся твой. Именно его и смотрят.
Разберём, что реально видит сервер, когда ты стучишься в `api16-normal-c-useast1a.tiktokv.com`, и почему свежий резидентный IP не спасает от бана.
Что такое JA3 и почему он важнее IP
JA3 — это хеш от параметров ClientHello. Версия TLS, наборы шифров, расширения, эллиптические кривые, форматы точек. Всё это клиент отправляет открытым текстом до установки шифрованного канала. Сервер видит это ещё до того, как поймёт, кто ты.
Схема простая. Клиент шлёт ClientHello. Сервер парсит поля в фиксированном порядке, склеивает через запятую, считает MD5. Получает 32-символьную строку. Она одинакова у всех, кто пользуется одной и той же библиотекой с одинаковыми настройками.
Chrome 120 на Windows даёт один JA3. Chrome 120 на macOS — другой, потому что другой набор расширений и порядок кривых. Python `requests` с `urllib3` — третий. Curl с OpenSSL 3.0 — четвёртый. И вот тут начинается веселье: если ты сидишь на чистом резидентном IP из Милана, а JA3 у тебя от питоновского скрипта — ты уже палишься. Потому что живой пользователь TikTok с iPhone не ходит через `requests`.
Почему одного JA3 мало — TLS fingerprinting в 2024
JA3 придумали в Salesforce в 2017-м. С тех пор клиенты научились его подделывать, а антифрод — смотреть глубже. Появился JA3N (нормализованный, без рандомизированных расширений), JA4, а дальше — анализ порядка расширений, GREASE-значений, ALPN, SNI, поддерживаемых групп и сигнатурных алгоритмов.
TikTok (ByteDance) не публикует свою логику. Но по поведению банхаммера видно: они сравнивают отпечаток с эталонным для заявленного User-Agent. Если UA говорит `TikTok 33.5.3 (iPhone; iOS 17.2)`, а ClientHello содержит набор шифров, которого в этом билде приложения нет — флаг. Даже если IP девственно чист.
Плюс HTTP/2 fingerprint. Порядок псевдозаголовков, значения `SETTINGS`, размер window, приоритеты потоков. Это уже уровень 7, но собирается из того же соединения. Akamai называет это HTTP/2 fingerprint, и он тоже хешируется.
Как выглядит ClientHello вживую
Возьмём реальный запрос через `curl` с системным OpenSSL и посмотрим, что уходит.
```bash
curl -v --tlsv1.3 https://www.tiktok.com/ 2>&1 | grep -A2 "TLS"
```
В выводе увидишь согласованный шифр, например `TLS_AES_128_GCM_SHA256`. Но это только итог. Чтобы увидеть весь ClientHello, нужен перехват:
```bash
openssl s_client -connect www.tiktok.com:443 -tlsextdebug -msg < /dev/null 2>&1 | head -50
```
Там будет список расширений: `server_name`, `supported_groups`, `ec_point_formats`, `signature_algorithms`, `ALPN`, `session_ticket`, `key_share`. Порядок и содержимое — это и есть отпечаток. У curl с OpenSSL 3.0 порядок один, у Chrome — другой, у мобильного TikTok — третий.
Вот как посчитать JA3 в питоне, если хочется проверить свой трафик:
```python
import hashlib
def ja3(version, ciphers, extensions, curves, point_formats):
parts = [
str(version),
"-".join(map(str, ciphers)),
"-".join(map(str, extensions)),
"-".join(map(str, curves)),
"-".join(map(str, point_formats)),
]
raw = ",".join(parts)
return hashlib.md5(raw.encode()).hexdigest()
print(ja3(771, [4865, 4866, 4867], [0, 23, 65281, 10, 11], [29, 23, 24], [0]))
```
Числа здесь — реальные ID из IANA. `771` — это TLS 1.2, `772` — TLS 1.3. `4865/4866/4867` — AES-128/256-GCM и ChaCha20. Кривые `29/23/24` — X25519, secp256r1, secp384r1. Хеш получится детерминированный и совпадёт с тем, что видит сервер.
Почему прокси не спасает
Прокси на уровне SOCKS5 или HTTP CONNECT просто перекладывает TCP-поток. TLS-рукопожатие идёт end-to-end между твоим клиентом и сервером TikTok. Прокси видит только зашифрованные байты. Сервер TikTok видит ClientHello твоего клиента как есть.
Значит, если ты запускаешь десять аккаунтов из одного питон-скрипта через десять разных резидентных прокси — у всех десяти будет одинаковый JA3. Антифрод это видит моментально. Десять разных людей из десяти разных городов не могут иметь идентичный TLS-отпечаток до последнего байта. Это физически невозможно, если они на разных устройствах.
Отсюда правило: чистить IP нужно, но без подмены TLS это половина работы. И часто — меньшая половина.
Таблица: что видит сервер на каждом уровне
| Уровень | Что видно | Меняется прокси? | Меняется антидетект-браузером? |
|---|---|---|---|
| L3 (IP) | Адрес источника | Да | Нет |
| L4 (TCP) | Опции TCP, окно, TTL | Частично | Нет |
| TLS ClientHello | JA3/JA4, порядок расширений | Нет | Да, если браузер правильный |
| HTTP/2 | SETTINGS, приоритеты, порядок заголовков | Нет | Да |
| HTTP/1.1 | Порядок заголовков, casing | Нет | Да |
| JS-окружение | canvas, WebGL, timezone | Нет | Да |
Из таблицы видно: прокси закрывает только первую строку. Всё остальное — на клиенте.
Кейс: ферма на requests через резидентные прокси
Пример: скрипт на Python 3.11, `requests==2.31`, `urllib3==2.0.7`, 40 аккаунтов, каждый через свой резидентный прокси (Bright Data, пул US). IP-репутация чистая, гео совпадает с заявленным. Через 6-8 часов после регистрации — массовый бан, 34 из 40.
Причина: у всех 40 запросов одинаковый JA3, потому что `requests` использует один и тот же `ssl`-контекст из системного OpenSSL 3.0. Плюс User-Agent подставлен мобильный (`TikTok 33.x`), а отпечаток — десктопный питоновский. Плюс HTTP/2 не используется вообще, `requests` ходит по HTTP/1.1 с дефолтным порядком заголовков `urllib3`.
Решение: уйти на `curl_cffi`, который умеет имперсонировать браузеры на уровне TLS.
```python
from curl_cffi import requests
r = requests.get(
"https://www.tiktok.com/api/user/detail/",
impersonate="chrome120",
proxies={"https": "http://user:pass@residential:port"},
headers={"Accept-Language": "en-US,en;q=0.9"},
)
print(r.status_code, r.json())
```
`impersonate="chrome120"` подставляет реальный ClientHello от Chrome 120: набор шифров, порядок расширений, ALPN `h2,http/1.1`, HTTP/2-настройки. После перехода на этот стек выживаемость выросла до 31 из 40 за первую неделю. Остальные 9 отвалились по другим причинам — поведенческим.
Кейс: мобильный отпечаток на десктопном стеке
Пример: попытка выдать себя за iPhone. UA выставляется `TikTok/33.5.3 (iPhone15,2; iOS 17.2; ru)`, прокси — мобильный 4G из Москвы. Всё логично. Но ClientHello идёт от Node.js `undici` с дефолтным набором шифров OpenSSL. А у настоящего iOS-приложения TikTok TLS-стек на BoringSSL с другим порядком расширений и с поддержкой `application_settings` (ALPS).
Антифрод ловит несовпадение: UA заявляет iOS, а TLS-отпечаток — Node.js на Linux. Такого в природе не бывает. Бан приходит на этапе регистрации, до первого лайка. Причём IP тут вообще ни при чём — он чистый, мобильный, живой.
Решение: либо эмулировать реальный iOS-стек (сложно, BoringSSL-специфика), либо не притворяться мобильным. Второе дешевле. Десктопный Chrome через `curl_cffi` с `impersonate="chrome120"` и UA от реального Chrome — консистентная картина. Пользователь на ноутбуке смотрит TikTok через веб. Никаких противоречий.
Кейс: антидетект-браузер с кривым TLS
Пример: связка Dolphin Anty + 20 профилей + резидентные прокси. Canvas и WebGL подменены, WebRTC закрыт, timezone совпадает с гео. Но профили создавались из шаблона с одинаковыми настройками, и TLS-отпечаток у всех двадцати — идентичный. Антидетект подменяет JS-окружение, но не трогает TLS-стек Chromium. Он у всех профилей один и тот же, потому что это один и тот же бинарник.
TikTok собирает JA3 по всем запросам с одного IP-пула и группирует. Если 20 «разных» пользователей с 20 разных IP имеют один JA3 — это кластер. Дальше смотрят поведение внутри кластера: похожие тайминги, похожие паттерны скролла, похожие интервалы между действиями. Бан.
Решение: либо разные версии Chromium в профилях (тогда JA3 разойдётся естественным образом), либо ручная подмена TLS через патченный бинарник. Второе — та ещё возня, но работает. Третье — вообще не использовать антидетект для TikTok, а работать через официальный API, если задача позволяет.
Что реально проверять перед запуском
Чек-лист, который сэкономит недели:
- JA3/JA4 совпадает с заявленным браузером/приложением. Проверяется через `tls.peet.ws` или локальный перехват.
- HTTP/2 fingerprint консистентен. Тот же `tls.peet.ws` показывает `akamai`-хеш.
- Порядок HTTP-заголовков соответствует браузеру. У Chrome `sec-ch-ua` идёт до `user-agent`, у Firefox — иначе.
- Прокси не в публичных чёрных списках. Проверять на `ipqualityscore`, не на «бесплатных чекерах».
- IP-гео совпадает с timezone и языком в запросах. Немецкий IP и `Accept-Language: en-US` — уже подозрительно.
Если хоть один пункт расходится с легендой — не начинай. TikTok собирает сигналы пачками и банит не сразу, а накопленно. Через сутки-двое. Когда ты уже расслабился.
Про IPv6 и пулы адресов
Отдельная история — IPv6. У многих провайдеров /64 на клиента, а то и /56. Это значит, что из одной подсети можно выставить тысячи адресов. Прокси-сервисы это используют. Но антифрод TikTok знает про /64 и группирует адреса по префиксу. Смена последних 64 бит внутри одной /64 для них — тот же пользователь.
Поэтому пул IPv6-прокси с одного /48 — это по сути один «житель» в их модели. И если через этот /48 идёт 500 регистраций с одинаковым JA3 — картина складывается мгновенно. Провайдер вроде lexic.ml выдаёт адреса из разных пулов, но правило про /64 всё равно надо держать в голове: смотри, из какого префикса приходит адрес, а не только на сам адрес.
Итог
IP — это билет на вход. TLS-отпечаток — это паспорт. Можно купить сколько угодно билетов, но если паспорт у всех один, на входе развернут. Работать надо на обоих уровнях одновременно: чистый адрес из правильного гео плюс консистентный отпечаток, который не противоречит легенде.
`curl_cffi` с `impersonate` закрывает 80% задачи за пять строк кода. Остальные 20% — поведение, тайминги и аккуратность. Без них даже идеальный JA3 не спасёт.