telemt 3.4.x: чтение логов, TLS-фингерпринты и детект блокировок ТСПУ
О чём заметка
С версии 3.4.x telemt пишет в логи TLS-фингерпринт (JA4/JA3) каждого подключающегося клиента. Это даёт редкую возможность увидеть снаружи прокси, по какому именно признаку ТСПУ режет соединения. Здесь — как читать эти логи, что значит ошибка
expected_64_got_0, и разбор реального наблюдения: DPI начал блокировать конкретную новую версию клиента Telegram по её JA4.
Статус данных
Ниже — разбор логов одного сервера (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 по фингерпринту и рвёт/душит соединение до того, как пойдут полезные данные.
Вывод
Массовый
expected_64_got_0с одной подсети/оператора = активная блокировка по TLS-фингерпринту, а не сетевой сбой.got 0(а неgot N) — признак именно обрыва после ClientHello.
Как читать JA4-фингерпринт в логах
telemt пишет JA4 вида t13d2014h2_<cipher_hash>_<extension_hash>. Расшифровка первой части (a-сегмент) — по той же схеме JA4 (разбор uTLS):
| Поле | 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 уже нет в природе».
Ключевой вывод
Это ровно «несвежесть пресета» из сибирской заметки (там это про uTLS в REALITY, здесь — про встроенный FakeTLS-пресет Telegram). Лечится это только в самом клиенте Telegram (issue просит ротацию/обновление фингерпринтов). Оператор прокси фингерпринт не меняет — отсюда и весь набор костылей ниже (дробление, desync, SYN-ACK), которые прячут/ломают опознание ClientHello, а не правят его.
Разбор наблюдения (telemt 3.4.15, июнь 2026)
Точность хешей
Ниже — обработка логов одного сервера (частично нейросетью). Конкретные 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.
Осторожно с причинностью
Альтернативное объяснение: новый клиент мог сам поменять тайминг/поведение хендшейка, и
got 0— побочка, а не таргетированный блок по JA4. Но концентрация одного фингерпринта в одной подсети одного оператора склоняет к версии «таргетированный детект». Проверяется сравнением: режется ли тот же JA4 у других операторов и на других подсетях.
Лимит SYN-ACK — помогает или нет
Гуляет приём — дропать исходящие SYN-ACK сверх 1/сек правилом nft. Его часто называют то «вредным», то «волшебным» — на деле всё посередине: он иногда реально помогает (в т.ч. на telemt), но с понятной ценой. Разберём честно.
На пальцах: что такое 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 drop45443 — порт, на котором слушает telemt. Правило дропает второй пакет TCP-рукопожатия (SYN-ACK, сервер→клиент), когда их больше 1/сек.
Почему это может ломать блокировку
Два механизма, оба правдоподобны:
- Десинхронизация stateful DPI. Дроп своего SYN-ACK заставляет ядро переслать его с задержкой. Рукопожатие завершается «криво» и позже — а stateful-трекер ТСПУ, который строит запись о потоке по хендшейку, на аномальном/переотправленном SYN-ACK может не привязать последующий ClientHello к потоку и не проинспектировать его. По духу это родственно nfqws-desync, только на уровне SYN-ACK.
- Слом «залпа соединений». Пока SYN-ACK не дошёл, клиент не шлёт ClientHello (TCP ещё не установлен). Значит частота ClientHello троттлится до 1/сек → рушится поведенческий Сигнал 3 (сибирская схема: «>3 коннектов с интервалом <100мс»).
Цена и оговорки
- Медленная установка соединений. Каждый коннект сверх лимита ждёт ретрансмит SYN-ACK (~1с+). Для Telegram с долгоживущими соединениями это разовая задержка на старте — терпимо. Для high-churn — больно.
- Глобальный вариант калечит всех юзеров разом — один бюджет 1/сек на весь порт. На многопользовательском прокси это плохо → нужен per-port вариант (ниже).
- Не лечит корень (протухший фингерпринт из #30733) — это обходной костыль, может перестать работать при адаптации ТСПУ.
- Сервер чуть аномален статистически, но «реально проходит» важнее «выглядит идеально».
Вердикт
Тестируйте на своём маршруте, и сразу 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с).- Итог: каждый раздаваемый порт лимитируется независимо → один юзер не давит других. Это снимает главное возражение против глобального правила.
Нюансы 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, как в REALITY). Единственное серверное лекарство — не дать DPI собрать и опознать ClientHello:
- TCPMSS-дробление — анонсировать малый MSS, чтобы клиент порезал ClientHello на куски, и stateful DPI не пересобрал фингерпринт.
- nfqws TCP desync (zapret) — fake-пакеты + TTL-limited split, чтобы сбить машину состояний DPI.
⚠️ В отличие от mtproto.zig (ставит это сам через mtbuddy install), telemt этим не занимается — обход на уровне ОС придётся накатывать руками поверх. Базовый рецепт TCPMSS+nfqws — в разборе mtproto.zig и в zapret.
- Лимит SYN-ACK (1/сек) — десинхронизирует stateful DPI и троттлит «залп». Спорный, но рабочий приём — см. раздел выше; бери сразу per-port вариант.
- Сменить узел/подсеть, если конкретный IP/диапазон попал под раздачу (Сигнал 1 сибирской схемы).
- Не дёргаться — рефлекторная смена настроек под блоком сама по себе сигнал.
Главное
- Корень (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 сам это не ставит — накатывай поверх.
📚 См. также
- SNI — почему смена почерка и ротация SNI возможны только на стороне клиента, а сервер бьёт лишь по «залпу»
- 🔗 telegramdesktop/tdesktop#30733 — официальный issue: протухший FakeTLS-фингерпринт, блокировки с 22 мая
- 03-telemt — установка и настройка telemt
- 05-censorship — каскад детекции ТСПУ
- mtproto.zig — как дробление ClientHello и nfqws ставятся «под ключ»
- JA4, подсеть, частота
- Воронка проверок DPI
- Блокировка сайта по JA4-отпечатку браузера — тот же
…d8a2da3f94cdломает и обычные сайты в Chrome (кейс wireflow.space)