Каждая публикуемая заметка получила callout-шапку со ссылкой на свою страницу wiki.zapret.moe (на самой вики она вырезается транформером RemoveMirrorCallout, видна только на зеркале Obsidian Publish и в Forgejo) и SEO-поле description — 1–2 предложения для meta description обоих сайтов. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
24 KiB
| description |
|---|
| Чтение логов telemt 3.4.x: ошибка expected_64_got_0, JA4-фингерпринты клиентов Telegram, детект блокировок ТСПУ и обход лимитом SYN-ACK. |
[!mirror] Резервное зеркало Актуальная версия этой страницы — на основной вики: wiki.zapret.moe/Zapret/mtproto/10-telemt-logs-dpi
telemt 3.4.x: чтение логов, TLS-фингерпринты и детект блокировок ТСПУ
[!info] О чём заметка С версии 3.4.x telemt пишет в логи TLS-фингерпринт (JA4/JA3) каждого подключающегося клиента. Это даёт редкую возможность увидеть снаружи прокси, по какому именно признаку ТСПУ режет соединения. Здесь — как читать эти логи, что значит ошибка
expected_64_got_0, и разбор реального наблюдения: DPI начал блокировать конкретную новую версию клиента Telegram по её JA4.
[!warning] Статус данных Ниже — разбор логов одного сервера (telemt 3.4.15) + сопоставление JA3 с публичными базами. Часть выводов (особенно «какой клиент это сломал») — гипотезы по наблюдениям, а не подтверждённая спецификация ТСПУ. Конкретные хеши и подсети — снимок на момент анализа; у вас будут другие. Читайте как метод, а не как готовые константы.
Что значит expected_64_got_0
Это ключевая ошибка в логах. Чтобы понять её, вспомним порядок FakeTLS-рукопожатия:
- Клиент → сервер: TLS
ClientHello(здесь telemt и снимает фингерпринт). - Сервер → клиент:
ServerHello+ChangeCipherSpec+ApplicationData. - Клиент → сервер:
ApplicationData, внутри которого 64-байтный обфусцированный MTProto-хендшейк.
telemt ждёт на шаге 3 ровно 64 байта. Запись:
expected 64 got 0
означает: ClientHello пришёл (фингерпринт снят), но потом соединение закрылось, не прислав ни байта обфусцированного заголовка. Клиент так себя не ведёт — он всегда досылает 64 байта. А вот DPI ведёт себя именно так: видит ClientHello, опознаёт его как Telegram по фингерпринту и рвёт/душит соединение до того, как пойдут полезные данные.
[!important] Вывод Массовый
expected_64_got_0с одной подсети/оператора = активная блокировка по TLS-фингерпринту, а не сетевой сбой.got 0(а неgot N) — признак именно обрыва после ClientHello.
Как читать JA4-фингерпринт в логах
telemt пишет JA4 вида t13d2014h2_<cipher_hash>_<extension_hash>. Расшифровка первой части (a-сегмент) — по VLESS/dpi-tls-june-2026:
| Поле | t13d2014h2 |
Значение |
|---|---|---|
t |
TLS-over-TCP (q = QUIC) |
транспорт |
13 |
TLS 1.3 | версия (из supported_versions) |
d |
domain — SNI есть (i = по IP) |
наличие SNI |
20 |
20 cipher suites | число шифров |
14 |
14 extensions | число расширений |
h2 |
ALPN = h2 (HTTP/2) |
первый/последний символ первого ALPN |
Дальше идут два хеша: _<cipher_hash>_<extension_hash> — усечённые SHA256 от отсортированных наборов шифров и расширений. Именно по ним отличают версии клиента при одинаковом a-сегменте.
Корень проблемы: фингерпринт клиента «протух» (tdesktop #30733)
Самое важное — из официального issue telegramdesktop/tdesktop#30733 (тесты блокировки начались 22 мая в Сибири):
Telegram Desktop в FakeTLS мимикрирует под браузер, и сейчас шлёт фингерпринт
t13d1516h2_8daaf6152771_d8a2da3f94cd— это Chrome 134 на macOS. Но реальный Chrome уже 148 (на Win —t13d1514h2_8daaf6152771_827b515c4f52). Пресет заморожен на старой версии, и эта несвежесть сама стала маркером.
Что тут видно при сравнении двух JA4:
| Telegram Desktop (мимикрия) | Реальный Chrome 148 (Win) | |
|---|---|---|
| JA4 | t13d1516h2_8daaf6152771_d8a2da3f94cd |
t13d1514h2_8daaf6152771_827b515c4f52 |
| Шифры (cipher hash) | 8daaf6152771 |
8daaf6152771 ← совпадает |
| Расширений | 16 | 14 |
| Extension hash | d8a2da3f94cd |
827b515c4f52 |
Cipher-хеш тот же (Telegram честно копирует набор шифров Chrome), но набор расширений разъехался — Telegram застрял на раскладке Chrome 134, а живой Chrome ушёл вперёд. DPI ловит именно это расхождение: «почерк Chrome, но такого Chrome уже нет в природе».
[!important] Ключевой вывод Это ровно «несвежесть пресета» из VLESS/dpi-tls-june-2026 (там это про uTLS в REALITY, здесь — про встроенный FakeTLS-пресет Telegram). Лечится это только в самом клиенте Telegram (issue просит ротацию/обновление фингерпринтов). Оператор прокси фингерпринт не меняет — отсюда и весь набор костылей ниже (дробление, desync, SYN-ACK), которые прячут/ломают опознание ClientHello, а не правят его.
Разбор наблюдения (telemt 3.4.15, июнь 2026)
[!warning] Точность хешей Ниже — обработка логов одного сервера (частично нейросетью). Конкретные cipher/extension-хеши тут могут быть неточными — авторитетные значения берите из issue #30733 выше. Доверять стоит структуре наблюдения (два семейства, новый фингерпринт чаще падает), а не отдельным хешам.
Два семейства по cipher-хешу
| Семейство | Cipher hash | Шифров | Кто это |
|---|---|---|---|
| Мобильные (Android/iOS) | a09f3c6… |
20 | BoringSSL — стек мобильного Telegram |
| Desktop | f57a46b… |
13 | Telegram Desktop |
Extension-хеш 7f0f34a4126d совпадает у мобильных и десктопа → набор расширений у них одинаковый, различаются только шифры (мобильный BoringSSL тащит больше cipher suites).
Сопоставление JA3 с публичными базами
JA3 этих клиентов уже лежат в открытых базах (ja3er.com, sslbl.abuse.ch) — то есть они давно известны и ТСПУ их видел:
| JA3 | Клиент |
|---|---|
ecdf4f49dd59effc439639da29186671 |
Telegram Android |
8527da8b8a640065e72ec6b6f99764f3 |
Telegram Desktop |
Новый фингерпринт — и именно он ломается
t13d2014h2 с extension-хешем e42f34c56612 — мобильный клиент, у которого изменился набор расширений (отсюда другой ext-хеш и счётчик …14 расширений). Похоже на свежее обновление приложения, добавившее один extension.
И вот суть:
Именно
t13d2014h2(e42f34c56612) чаще всего падает вexpected_64_got_0. На МТС подсеть95.24.149.xпочти целиком состоит из этого фингерпринта.
Интерпретация: ТСПУ научился резать конкретно новую версию клиента по её свежему JA4. Старые фингерпринты (которые в публичных базах) проходят, а только что появившийся — рубится. Это укладывается в логику «волны проблем после обновления Telegram»: сломалось не у всех, а у тех, кто обновил приложение до версии с новым extension.
[!note] Осторожно с причинностью Альтернативное объяснение: новый клиент мог сам поменять тайминг/поведение хендшейка, и
got 0— побочка, а не таргетированный блок по JA4. Но концентрация одного фингерпринта в одной подсети одного оператора склоняет к версии «таргетированный детект». Проверяется сравнением: режется ли тот же JA4 у других операторов и на других подсетях.
Лимит SYN-ACK — помогает или нет
Гуляет приём — дропать исходящие SYN-ACK сверх 1/сек правилом nft. Его часто называют то «вредным», то «волшебным» — на деле всё посередине: он иногда реально помогает (в т.ч. на telemt), но с понятной ценой. Разберём честно.
[!example] На пальцах: что такое SYN-ACK и почему его дроп сбивает DPI Установка TCP-соединения — это как звонок:
- Клиент: «SYN» — «Алло, можем говорить?»
- Сервер: «SYN-ACK» — «Да, говори». ← вот его и дропаем
- Клиент: «ACK» — «Ок». И только теперь шлёт ClientHello — свою «визитку» (тот самый почерк, который ловит DPI).
Если сервер иногда роняет своё «Да, говори», происходит две полезные вещи:
- DPI теряет нить. Цензор-«подслушка» строит запись о разговоре по этому рукопожатию. Когда «Да, говори» приходит с задержкой и повтором, запись у DPI получается кривая — и он не привязывает к ней визитку клиента, то есть не опознаёт её.
- Нет «залпа». Пока «Да, говори» не дошло, клиент молчит и визитку не отправляет. Значит визитки выходят не пачкой, а по одной в секунду — и поведенческий триггер «много коннектов разом» не срабатывает.
Минус — звонок дольше устанавливается (надо переспросить через секунду). Для Telegram это разовая задержка на старте, потом соединение живёт долго — поэтому терпимо.
Что вообще делает правило
add table inet filter
add chain inet filter output { type filter hook output priority 0; }
add rule inet filter output tcp flags & (syn|ack) == syn|ack \
tcp sport 45443 limit rate over 1/second burst 1 packets drop
45443 — порт, на котором слушает telemt. Правило дропает второй пакет TCP-рукопожатия (SYN-ACK, сервер→клиент), когда их больше 1/сек.
Почему это может ломать блокировку
Два механизма, оба правдоподобны:
- Десинхронизация stateful DPI. Дроп своего SYN-ACK заставляет ядро переслать его с задержкой. Рукопожатие завершается «криво» и позже — а stateful-трекер ТСПУ, который строит запись о потоке по хендшейку, на аномальном/переотправленном SYN-ACK может не привязать последующий ClientHello к потоку и не проинспектировать его. По духу это родственно nfqws-desync, только на уровне SYN-ACK.
- Слом «залпа соединений». Пока SYN-ACK не дошёл, клиент не шлёт ClientHello (TCP ещё не установлен). Значит частота ClientHello троттлится до 1/сек → рушится поведенческий Сигнал 3 (VLESS/dpi-tls-june-2026: «>3 коннектов с интервалом <100мс»).
Цена и оговорки
- Медленная установка соединений. Каждый коннект сверх лимита ждёт ретрансмит SYN-ACK (~1с+). Для Telegram с долгоживущими соединениями это разовая задержка на старте — терпимо. Для high-churn — больно.
- Глобальный вариант калечит всех юзеров разом — один бюджет 1/сек на весь порт. На многопользовательском прокси это плохо → нужен per-port вариант (ниже).
- Не лечит корень (протухший фингерпринт из #30733) — это обходной костыль, может перестать работать при адаптации ТСПУ.
- Сервер чуть аномален статистически, но «реально проходит» важнее «выглядит идеально».
[!tip] Вердикт Тестируйте на своём маршруте, и сразу per-port вариант. Это не замена дроблению/desync/
mask, а дополнение к ним. Заработало — оставляйте, но мониторьте логи (expected_64_got_0), чтобы поймать момент, когда ТСПУ адаптируется и приём перестанет действовать.
Per-port вариант — правильный (бюджет 1/сек на каждый порт)
Проблему «один лимит на всех» решает раздача юзерам разных портов из диапазона + лимит, считаемый по порту:
table ip telemt_limit {
set synack_ports {
type inet_service
size 65535
flags dynamic,timeout
timeout 5s
}
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
tcp dport 4000-5000 redirect to :443 # все порты 4000-5000 → telemt:443
}
chain postrouting {
type filter hook postrouting priority srcnat + 1; policy accept;
tcp flags & (syn|ack) == syn|ack tcp sport 4000-5000 \
update @synack_ports { tcp sport limit rate over 1/second burst 1 packets } drop
}
}
Командами:
nft add table ip telemt_limit
nft add set ip telemt_limit synack_ports { type inet_service \; flags dynamic, timeout \; timeout 5s \; }
nft add chain ip telemt_limit prerouting { type nat hook prerouting priority dstnat\; }
nft add rule ip telemt_limit prerouting tcp dport 4000-5000 redirect to :443
nft add chain ip telemt_limit postrouting { type filter hook postrouting priority srcnat + 1\; }
nft add rule ip telemt_limit postrouting tcp flags \& \(syn \| ack\) == syn \| ack tcp sport 4000-5000 \
update @synack_ports { tcp sport limit rate over 1/second burst 1 packets } drop
Простыми словами: вместо одного порта 443 ты даёшь каждому пользователю свой порт из диапазона 4000-5000 (в ссылке). Все они внутри ведут на тот же telemt, но лимит «1 соединение в секунду» считается отдельно для каждого порта. Поэтому активность одного пользователя больше не мешает остальным — у каждого свой бюджет.
Как это работает технически:
prerouting(DNAT): клиент может коннектиться на любой порт4000-5000, всё редиректится («заворачивается») на telemt:443. Раздаёшь каждому юзеру/ссылке свой порт из диапазона.postrouting(prioritysrcnat+1): правило срабатывает после un-NAT, когда sport SYN-ACK уже переписан обратно в клиентский порт (4000-5000).update @synack_ports { tcp sport limit rate ... }заводит отдельный счётчик 1/сек на каждый порт (элементы set живут 5с).- Итог: каждый раздаваемый порт лимитируется независимо → один юзер не давит других. Это снимает главное возражение против глобального правила.
[!warning] Нюансы per-port варианта
- Только IPv4 (
table ip). Для IPv6 нужен отдельныйtable ip6.- «Per-port = per-user» работает, только если реально раздавать каждому свой порт. Делят порт → делят бюджет.
- Лимит считается по порту назначения, не по IP клиента — для приватного прокси это ок.
Что с этим делать
Диагностика — грепаем логи
# Сколько обрывов после ClientHello и с каких IP
docker compose logs telemt | grep "expected 64 got 0" | grep -oE '([0-9]+\.){3}[0-9]+' \
| sort | uniq -c | sort -rn | head
# Привязка обрывов к фингерпринту (если JA4 в той же строке/рядом)
docker compose logs telemt | grep -E "expected 64 got 0|t13d" | tail -50
Если expected_64_got_0 концентрируется на одной подсети/операторе и одном JA4 — это подпись активного DPI, а не случайные сбои.
Лечение — то же, что и для любого FakeTLS
Фингерпринт генерирует приложение Telegram, а не сервер — на стороне telemt его не поменять (нет uTLS, как в VLESS/dpi-tls-june-2026). Единственное серверное лекарство — не дать DPI собрать и опознать ClientHello:
- TCPMSS-дробление — анонсировать малый MSS, чтобы клиент порезал ClientHello на куски, и stateful DPI не пересобрал фингерпринт.
- nfqws TCP desync (zapret) — fake-пакеты + TTL-limited split, чтобы сбить машину состояний DPI.
⚠️ В отличие от mtproxy/mtproto-zig (ставит это сам через mtbuddy install), telemt этим не занимается — обход на уровне ОС придётся накатывать руками поверх. Базовый рецепт TCPMSS+nfqws — в разборе mtproxy/mtproto-zig и в Zapret/about.
- Лимит SYN-ACK (1/сек) — десинхронизирует stateful DPI и троттлит «залп». Спорный, но рабочий приём — см. #Лимит SYN-ACK — помогает или нет; бери сразу per-port вариант.
- Сменить узел/подсеть, если конкретный IP/диапазон попал под раздачу (Сигнал 1 VLESS/dpi-tls-june-2026).
- Не дёргаться — рефлекторная смена настроек под блоком сама по себе сигнал.
[!summary] Главное
- Корень (issue #30733): FakeTLS-фингерпринт Telegram протух (мимикрия под Chrome 134, а живой — 148). Чинится только в самом клиенте Telegram — оператор фингерпринт не меняет.
- Детект: telemt 3.4.x логирует JA4 →
expected_64_got_0+ один фингерпринт + одна подсеть = таргетированный блок по версии клиента. Это твой бесплатный детектор DPI.- Обход на стороне сервера (фингерпринт не трогаем, прячем/ломаем опознание): дробление ClientHello (TCPMSS), nfqws-desync, лимит SYN-ACK (per-port), смена узла. Telemt сам это не ставит — накатывай поверх.
📚 См. также
- mtproxy/ja4-sni-client-side — почему смена почерка и ротация SNI возможны только на стороне клиента, а сервер бьёт лишь по «залпу»
- 🔗 telegramdesktop/tdesktop#30733 — официальный issue: протухший FakeTLS-фингерпринт, блокировки с 22 мая
- Zapret/mtproto/03-telemt — установка и настройка telemt
- Zapret/mtproto/05-censorship — каскад детекции ТСПУ
- mtproxy/mtproto-zig — как дробление ClientHello и nfqws ставятся «под ключ»
- VLESS/dpi-tls-june-2026
- DPI/dpi-analysis-pipeline
- DPI/browser-ja4-fingerprint-block — тот же
…d8a2da3f94cdломает и обычные сайты в Chrome (кейс wireflow.space)