Как антифрод Twitch вычисляет мультиаккаунты по TLS/JA3 и что реально меняет ротация прокси
Содержание
- Что такое JA3 и почему он живёт дольше cookie
- data — сырые байты TLS record, начиная с 0x16 0x03
- Где Twitch берёт отпечаток и что с ним делает
- Почему ротация прокси не спасает от TLS-корреляции
- Что реально меняет картину: подмена ClientHello
- Кейс: как один JA3 сжёг пул из 200 прокси
- Кейс: MITM-прокси и сертификат, который всё выдал
- Кейс: JA4 и почему старый JA3-обход перестал работать
- Таблица: что меняет каждый уровень подмены
- Почему прокси всё-таки нужны, но не как основное средство
- Что смотреть в своих логах, чтобы не гадать
- Короткий вывод без выводов
Twitch ловит мультиаккаунты не по IP. Точнее — не только по IP. IP у них давно вторичен, потому что любой школьник с VPN меняет адрес за две минуты. Основной сигнал — TLS-рукопожатие. Клиентское приветствие ClientHello содержит набор шифров, расширений и их порядок. Этот отпечаток называется JA3. Он стабилен для конкретной сборки браузера и операционки. Меняешь прокси — отпечаток не меняется. Вот тут начинаются грабли.
Что такое JA3 и почему он живёт дольше cookie
JA3 — это MD5-хеш от строки, собранной из полей ClientHello: версия TLS, список cipher suites, список расширений, список эллиптических кривых, форматы точек. Собрал строку — получил 32-символьный хеш. Chrome 120 на Windows даёт один хеш, Chrome 120 на macOS — другой, Firefox — третий. Причём хеш не зависит от IP, от User-Agent и от cookie. Он формируется до того, как ты отправишь первый HTTP-запрос.
Для антифрода это золото. Представь: с одного IP заходят десять аккаунтов. IP ты поменял прокси-ротацией. Но все десять пришли с одним JA3 — значит, это один и тот же браузер, одна и та же машина. Или, что хуже, — один и тот же автоматизированный клиент. Антифрод Twitch (как и большинства крупных платформ) строит граф связей: аккаунт → JA3 → IP → устройство. Совпадение по любому ребру — повод присмотреться.
Хеш ClientHello легко посчитать локально. Вот рабочий сниппет на Python, который вытаскивает JA3 из сырого ClientHello:
```python
import hashlib
def ja3_from_client_hello(data: bytes) -> str:
data — сырые байты TLS record, начиная с 0x16 0x03
assert data[0] == 0x16
ptr = 5 # skip record header
ptr += 4 # handshake type + length
ptr += 2 # client version
ptr += 32 # random
sid_len = data[ptr]
ptr += 1 + sid_len
cs_len = int.from_bytes(data[ptr:ptr+2], "big")
ptr += 2
ciphers = [int.from_bytes(data[ptr+i:ptr+i+2], "big")
for i in range(0, cs_len, 2)]
ptr += cs_len
comp_len = data[ptr]
ptr += 1 + comp_len
ext_len = int.from_bytes(data[ptr:ptr+2], "big")
ptr += 2
end = ptr + ext_len
exts, curves, points = [], [], []
while ptr < end:
etype = int.from_bytes(data[ptr:ptr+2], "big")
elen = int.from_bytes(data[ptr+2:ptr+4], "big")
exts.append(etype)
body = data[ptr+4:ptr+4+elen]
if etype == 10 and len(body) >= 2:
n = int.from_bytes(body[0:2], "big")
curves = [int.from_bytes(body[2+i:4+i], "big")
for i in range(0, n, 2)]
if etype == 11 and len(body) >= 1:
n = body[0]
points = list(body[1:1+n])
ptr += 4 + elen
raw = ",".join(map(str, [
ciphers, exts, curves, points, [0]
])).replace(" ", "")
return hashlib.md5(raw.encode()).hexdigest()
```
Хеш считается до TLS-сессии. Прокси тут ни при чём — он видит уже готовый ClientHello и просто пересылает байты. Никакой ротацией IP ты JA3 не изменишь. Это первое, что ломает мозг у людей, которые думают, что «прокси решает проблему мультиаккаунтов».
Где Twitch берёт отпечаток и что с ним делает
Twitch сидит за CloudFront и собственным балансировщиком. TLS терминируется на их стороне, значит ClientHello доходит целиком. Дальше — либо самописный парсер, либо готовый движок вроде JA3/JA4 от Salesforce. Скорее всего, у них уже JA4, потому что JA3 ломается на TLS 1.3: в 1.3 список шифров в ClientHello формально есть, но реальный обмен идёт через TLS 1.3 cipher suites, и JA3 перестаёт различать некоторые сборки. JA4 учитывает больше полей и сортирует их иначе.
Помимо JA3/JA4 они смотрят на HTTP/2 fingerprint (набор SETTINGS, приоритеты, window size), на порядок заголовков, на наличие `sec-ch-ua`, на TLS-расширения вроде `application_layer_protocol_negotiation`. Всё это вместе называется fingerprinting. Один только JA3 — слабый сигнал, но в связке с остальным даёт очень точную картину.
Дальше строится поведенческая модель. Если с одного JA3 за сутки приходит двадцать регистраций с разных IP — это флаг. Если с одного JA3 логинятся в аккаунты, которые раньше не пересекались по IP, — второй флаг. Если JA3 совпадает с известным отпечатком headless Chrome (а у него свой стабильный хеш), — третий флаг. Каждый флаг добавляет вес в скоринг. Превысил порог — бан, теневой бан или капча.
Почему ротация прокси не спасает от TLS-корреляции
Ротация прокси меняет IP. Всё. Больше ничего. TCP-соединение идёт от твоего клиента до прокси, потом прокси открывает новое соединение до Twitch. Но TLS-рукопожатие формирует твой клиент, а не прокси. Прокси в режиме CONNECT просто туннелирует байты. Значит, ClientHello уходит ровно тот, который собрал твой браузер или твой скрипт.
Есть нюанс с MITM-прокси. Если прокси терминирует TLS и переустанавливает его со своей стороны (корпоративные прокси так делают), то Twitch видит JA3 прокси, а не твоего клиента. Но такие прокси требуют установки корневого сертификата и палятся по сертификату цепочки. Twitch такое видит сразу — самоподписанный или неизвестный CA в цепочке = красный флаг. Так что этот путь — тупик.
Ещё один нюанс: некоторые HTTP-прокси переписывают заголовки, добавляют `Via`, `X-Forwarded-For`. Это тоже сигнал. Twitch смотрит на наличие `Via` и на то, совпадает ли `X-Forwarded-For` с реальным IP. Если прокси неаккуратный — спалит себя раньше, чем JA3.
Вот как выглядит типичная ошибка: человек берёт ротационный пул на 1000 IP, запускает 50 аккаунтов, все с одного Chrome. IP разные, JA3 один. Антифрод склеивает все 50 в один кластер за пару часов. Бан прилетает не сразу — сначала теневой, чтобы собрать больше данных. Потом — пачкой.
Что реально меняет картину: подмена ClientHello
Единственный способ разорвать связь по JA3 — сделать так, чтобы каждый аккаунт приходил с разным отпечатком. Вариантов несколько, и все они разной степени костыльности.
Первый — использовать разные браузеры и версии. Chrome 118, Chrome 120, Firefox 121, Safari 17 — четыре разных JA3. Но это не масштабируется на сотни аккаунтов и требует ручной работы.
Второй — подменять ClientHello на уровне TLS-библиотеки. В Python это делается через `curl_cffi`, который умеет имитировать отпечатки разных браузеров. Вот пример:
```python
from curl_cffi import requests
for imp in ["chrome120", "chrome118", "firefox121", "safari17_0"]:
r = requests.get(
"https://www.twitch.tv/",
impersonate=imp,
proxies={"https": "http://user:pass@proxy-pool:8080"},
timeout=15,
)
print(imp, r.status_code, len(r.content))
```
`curl_cffi` подменяет не только JA3, но и HTTP/2 fingerprint, и порядок заголовков. Это уже серьёзнее. Но и тут есть проблема: если у тебя сто аккаунтов и четыре отпечатка, антифрод всё равно увидит кластеры по 25 аккаунтов на отпечаток. Нужно либо много разных отпечатков, либо рандомизация внутри одного.
Третий — собственный TLS-стек с рандомизацией полей ClientHello. Это уже уровень uTLS в Go или tls-client. Там можно двигать порядок расширений, добавлять/убирать опциональные, менять GREASE-значения. Каждый запуск — новый JA3. Дорого по разработке, но работает.
Кейс: как один JA3 сжёг пул из 200 прокси
Был у меня опыт с одним проектом — не Twitch, но механика та же. Клиент крутил 200 резидентских прокси, ротация каждые 10 минут, 80 аккаунтов на платформе с похожим антифродом. Все аккаунты — через один headless Chrome 119, запущенный через Puppeteer с дефолтными настройками. JA3 у headless Chrome стабильный и заметно отличается от обычного Chrome: другой набор расширений, нет некоторых `sec-ch-ua`-связанных полей.
Через 36 часов после старта прилетела волна банов — 62 аккаунта из 80. Разбор показал: платформа сгруппировала все 80 по JA3, потом посмотрела на поведение внутри группы. Аккаунты логинились с интервалом 40-90 секунд, смотрели стримы по 12-15 минут, уходили. Паттерн слишком ровный. IP-ротация не помогла, потому что кластеризация шла по отпечатку, а не по адресу.
Решение: перешли на `curl_cffi` с рандомизацией между пятью отпечатками (chrome118, chrome119, chrome120, firefox120, firefox121), добавили рандомные паузы 2-7 минут между действиями, разбавили поведение — часть аккаунтов смотрела длинные стримы, часть короткие, часть вообще не смотрела, а только логинилась. Выживаемость выросла с 22% до 71% за месяц. Прокси-пул при этом остался тот же.
Кейс: MITM-прокси и сертификат, который всё выдал
Другой случай. Человек настроил Squid с SSL bump — терминировал TLS на прокси, чтобы переписывать заголовки. Логика была простая: раз прокси переустанавливает TLS, значит Twitch видит JA3 прокси, а не клиента, и все аккаунты выглядят одинаково «чисто». Ага, конечно.
Проблема в цепочке сертификатов. Squid подписывает сертификаты своим CA. Этот CA не входит в доверенные у Twitch (и ни у кого не входит, кроме самого клиента). Браузер-то его примет, потому что сертификат установлен локально. А вот Twitch видит цепочку: leaf → промежуточный → неизвестный корень. Это мгновенный флаг. Плюс JA3 у Squid свой, стабильный, и он не совпадает ни с одним реальным браузером.
Итог: 100% аккаунтов ушли в теневой бан за неделю. Причём теневой — то есть они могли логиниться, но чат не работал, стримы не грузились в полном качестве, рекомендации не обновлялись. Человек неделю не понимал, что происходит, пока не посмотрел на логи прокси и не увидел, что все запросы идут через один и тот же TLS-канал. Ротация IP тут вообще не участвовала — она была, но бесполезна.
Кейс: JA4 и почему старый JA3-обход перестал работать
Третий случай — уже про переход на JA4. Проект использовал uTLS с рандомизацией JA3, всё работало месяцев шесть. Потом платформа (не Twitch, но из той же лиги) обновила антифрод и начала считать JA4. А JA4 учитывает, помимо прочего, ALPN, версию TLS, количество расширений и их сортировку по-другому. Рандомизация, которая ломала JA3, не ломала JA4 — потому что порядок расширений в JA4 нормализуется, и хеш оставался стабильным.
Симптом: баны вернулись, хотя JA3 у каждого запуска был разный. Проверили через `ja4`-калькулятор — да, JA4 одинаковый у всех сессий. Пришлось переписывать рандомизатор: двигать не порядок, а состав расширений (добавлять/убирать `session_ticket`, `signature_algorithms`, менять `supported_groups`), и рандомизировать ALPN. После этого выживаемость вернулась к норме. Мораль: JA3 — не константа, антифрод эволюционирует, и обход, который работал год назад, сегодня может быть мёртвым.
Таблица: что меняет каждый уровень подмены
| Уровень | Что меняется | Что видит антифрод | Эффективность против Twitch |
|---|---|---|---|
| Ротация IP | IP-адрес | Разные IP, один JA3 | Низкая, кластеризация по отпечатку |
| Разные браузеры | JA3, HTTP/2 | 4-6 отпечатков на пул | Средняя, кластеры по 15-25 аккаунтов |
| curl_cffi / impersonate | JA3, HTTP/2, заголовки | 5-10 отпечатков | Средняя-высокая при рандомизации |
| uTLS / tls-client | Полный ClientHello | Много разных JA3 | Высокая, но нужен JA4-учёт |
| MITM-прокси | JA3 прокси + сертификат | Неизвестный CA, стабильный JA3 | Нулевая, палится по цепочке |
Цифры в последней колонке — из личного опыта на похожих платформах, не из документации Twitch. Twitch своих порогов не публикует.
Почему прокси всё-таки нужны, но не как основное средство
Прокси решают другую задачу — географию и репутацию IP. Если с одного IP заходит десять аккаунтов, это флаг независимо от JA3. Если IP из датацентра (ASN хостинга), это отдельный флаг — Twitch знает диапазоны AWS, DigitalOcean, Hetzner и понижает доверие к ним. Резидентские прокси с чистых ASN эту проблему снимают.
Второй момент — скорость ротации. Если IP меняется каждые 10 минут, а аккаунт живёт месяцами, это выглядит подозрительно: реальный пользователь не прыгает по странам. Стабильный IP на аккаунт с редкой сменой при переезде — естественнее. Некоторые пулы на lexic.ml как раз дают /64-подсети, где можно держать стабильный IPv6 на аккаунт и не светить смену адреса каждые полчаса. Это технически оправданно, когда нужно совместить чистоту ASN и стабильность адреса.
Третий момент — DNS. Если прокси не проксирует DNS, запросы уходят через локальный резолвер, и антифрод видит расхождение: IP из одной страны, DNS-резолвер из другой. Это ещё один сигнал. Проверить просто:
```bash
curl -s --proxy http://user:pass@proxy:8080 https://ifconfig.me/ip
curl -s --proxy http://user:pass@proxy:8080 https://1.1.1.1/cdn-cgi/trace | grep -E "ip=|loc="
dig +short TXT o-o.myaddr.l.google.com @ns1.google.com
```
Если первые две строки дают IP из Нидерландов, а `dig` показывает твой домашний резолвер из Москвы — расхождение зафиксировано.
Что смотреть в своих логах, чтобы не гадать
Прежде чем что-то менять, собери телеметрию. Без неё ты стреляешь в темноте. Минимум: JA3/JA4 каждого соединения, HTTP/2 fingerprint, IP, ASN, DNS-резолвер, время между запросами, порядок заголовков. Дальше — кластеризация: сколько аккаунтов делят один отпечаток, сколько делят один ASN, есть ли пересечения по времени.
Простой скрипт на Python, который считает распределение JA3 по логу:
```python
import json
from collections import Counter
def report(path: str) -> None:
ja3_counter = Counter()
asn_counter = Counter()
with open(path) as f:
for line in f:
rec = json.loads(line)
ja3_counter[rec["ja3"]] += 1
asn_counter[rec["asn"]] += 1
total = sum(ja3_counter.values())
print(f"connections: {total}")
for ja3, n in ja3_counter.most_common(10):
print(f"{ja3} {n:6d} {n/total*100:5.1f}%")
print("--- ASN ---")
for asn, n in asn_counter.most_common(10):
print(f"{asn} {n:6d} {n/total*100:5.1f}%")
```
Если топ-1 JA3 покрывает больше 30% трафика — у тебя проблема. Если топ-1 ASN покрывает больше 40% — вторая проблема. Обычно обе идут рука об руку.
Короткий вывод без выводов
Антифрод Twitch смотрит на TLS-отпечаток раньше, чем на IP. Ротация прокси меняет адрес, но не ClientHello. Если все аккаунты ходят с одним JA3 — они склеятся в кластер независимо от того, сколько у тебя прокси. Работает только комплекс: разные отпечатки на уровне TLS-стека, чистые ASN, стабильные IP на аккаунт, естественное поведение и учёт того, что антифрод уже перешёл на JA4. По отдельности каждый элемент — костыль. Вместе — рабочая схема, которую всё равно придётся пересобирать раз в полгода, потому что правила игры меняются.