todo/Zapret2/распознавание mtproto.md
loop-uh 0ad0bcc8dd
Some checks failed
Published content check / validate (push) Failing after 5s
seo+зеркало: [!mirror]-плашка со ссылкой на вики и description во frontmatter всех заметок
Каждая публикуемая заметка получила callout-шапку со ссылкой на свою страницу
wiki.zapret.moe (на самой вики она вырезается транформером RemoveMirrorCallout,
видна только на зеркале Obsidian Publish и в Forgejo) и SEO-поле description —
1–2 предложения для meta description обоих сайтов.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 22:05:57 +03:00

24 KiB
Raw Permalink Blame History

tags link aliases img description
Как Zapret 2 распознаёт MTProto: AES-детект первого пакета, фильтры --filter-l7=mtproto и --payload=mtproto_initial, примеры стратегий обхода.

[!mirror] Резервное зеркало Актуальная версия этой страницы — на основной вики: wiki.zapret.moe/Zapret2/распознавание-mtproto

Zapret действительно распознаёт MTProto:

static const char *l7proto_name[] = {"all","unknown","known","http","tls","quic","wireguard","dht","discord","stun","xmpp","dns","mtproto"};
 "mtproto_initial"};

Как работает определение MTProto

bool IsMTProto(const uint8_t *data, size_t len)
{
	if (len>=64)
	{
		// this way requires only one AES instead of 4
		uint8_t decrypt[16], iv[16];
		aes_context ctx;

		memcpy(iv, data+40, 16);
		ctr_add(iv,3);
		if (!aes_setkey(&ctx, AES_ENCRYPT, data+8, 32) && !aes_cipher(&ctx, iv, decrypt))
		{
			*((uint32_t*)(decrypt+8)) ^= *((uint32_t*)(data+56));
			return !memcmp(decrypt+8,"\xEF\xEF\xEF\xEF",4);  // сигнатура MTProto2
		}
	}
	return false;
}

Определяется через AES дешифровку — проверяется магическая сигнатура 0xEFEFEFEF.

Ограничения

		else if (ctrack && (ctrack->pos.seq_last - ctrack->pos.seq0)==1 && IsMTProto(dis->data_payload, dis->len_payload))
		{
			DLOG("packet contains telegram mtproto2 initial\n");
			// mtproto detection requires aes. react only on the first tcp data packet. do not detect if ctrack unavailable.
			l7payload = L7P_MTPROTO_INITIAL;
  1. Определяется только на первом TCP пакете с данными
  2. Требуется conntrack (ctrack)
  3. Последующие пакеты будут unknown

Важно для фильтрации!

MTProto НЕ содержит hostname! Это значит:

Фильтрация Работает?
--filter-l7=mtproto Да
--payload=mtproto_initial Да
--hostlist=... Нет (нет hostname)
--ipset=telegram_ips.txt Да

Пример использования

nfqws2 --lua-init=@zapret-lib.lua --lua-init=@zapret-antidpi.lua \
  --filter-l7=mtproto \
  --payload=mtproto_initial \
  --lua-desync=fake:blob=0x00000000:repeats=3

Или с ipset для большей точности:

nfqws2 \
  --ipset=telegram_ips.txt \
  --filter-l7=mtproto \
  --payload=mtproto_initial \
  --lua-desync=multisplit:pos=32

hostlist бесполезен — только ipset или фильтрация по l7 proto.

Ключевое условие в коде:

		else if (ctrack && (ctrack->pos.seq_last - ctrack->pos.seq0)==1 && IsMTProto(dis->data_payload, dis->len_payload))
		{
			DLOG("packet contains telegram mtproto2 initial\n");
			// mtproto detection requires aes. react only on the first tcp data packet. do not detect if ctrack unavailable.
			l7payload = L7P_MTPROTO_INITIAL;
			if (l7proto == L7_UNKNOWN)
			{
				l7proto = L7_MTPROTO;
				if (ctrack->l7proto == L7_UNKNOWN) ctrack->l7proto = l7proto;
			}
		}

Когда MTProto → unknown

Условие детекции:

(ctrack->pos.seq_last - ctrack->pos.seq0) == 1

Это значит: только первый TCP пакет с данными.

TCP соединение MTProto:
────────────────────────────────────────────────────────────►

[SYN]     [SYN+ACK]     [ACK]     [DATA #1]          [DATA #2]     [DATA #3] ...
  │           │           │           │                  │             │
  │           │           │           │                  │             │
  │           │           │           └── seq_last-seq0=1  └─────────────┴── seq_last-seq0 > 1
  │           │           │               payload=mtproto_initial         payload=unknown
  │           │           │               proto=mtproto                   proto=mtproto (сохранён)
  │           │           │
  └───────────┴───────────┴── payload=empty

Разница l7proto vs payload

Пакет --filter-l7 (proto) --payload
SYN, SYN+ACK, ACK unknown empty
1-й пакет с данными mtproto mtproto_initial
2-й пакет с данными mtproto unknown
3-й пакет с данными mtproto unknown
... mtproto unknown

l7proto (протокол соединения) сохраняется в conntrack и остаётся mtproto для всего соединения.

payload (тип содержимого пакета) — mtproto_initial только для первого пакета, потом unknown.

Практический вывод

# Сработает только на первом пакете с данными
--filter-l7=mtproto --payload=mtproto_initial --lua-desync=fake

# Сработает на всех пакетах MTProto соединения (включая unknown)
--filter-l7=mtproto --payload=all --lua-desync=multisplit

# Или без фильтра payload (по умолчанию known, mtproto_initial входит)
--filter-l7=mtproto --lua-desync=fake

Да, можно! Множественные payload поддерживаются через запятую:

static bool parse_l7p_list(char *opt, uint64_t *l7p)
{
	char *e, *p, c;
	t_l7payload payload;

	for (p = opt, *l7p = 0; p; )
	{
		if ((e = strchr(p, ',')))  // парсинг через запятую
		{
			c = *e;
			*e = 0;

Правильный синтаксис

--payload=mtproto_initial,unknown

Это сработает на:

  • Первом пакете MTProto (mtproto_initial)
  • Всех последующих пакетах (unknown)

Пример для MTProto

nfqws2 \
  --filter-l7=mtproto \
  --payload=mtproto_initial,unknown \
  --lua-desync=fake:blob=0x00000000:repeats=2

Или если нужны разные стратегии для разных payload:

nfqws2 \
  --filter-l7=mtproto \
  --payload=mtproto_initial \
    --lua-desync=fake:blob=0x00000000:repeats=3 \
  --payload=unknown \
    --lua-desync=multisplit:pos=16

Доступные payload типы

Из кода protocol.c:

empty, http_req, http_resp, tls_client_hello, tls_server_hello, 
tls_app_data, quic_initial, quic_0rtt, wireguard_initiation, 
wireguard_response, wireguard_cookie, dht, discord_ip_discovery, 
stun, xmpp_stream, xmpp_starttls, dns_req, dns_resp, mtproto_initial,
unknown, known, all

Лучший подход для MTProto для обхода блокировок

Только первый пакет имеет значение! Вот почему:

Как DPI детектит MTProto

  1. По первому пакету — есть характерная структура (64+ байт, AES сигнатура)
  2. По IP серверов — известные диапазоны Telegram
  3. После установления соединения — всё зашифровано, DPI не видит ничего

Рекомендация

nfqws2 \
  --filter-l7=mtproto \
  --payload=mtproto_initial \
  --lua-desync=fake:blob=0x00000000000000000000000000000000:repeats=3

Почему так:

Вариант Плюсы Минусы
payload=mtproto_initial Минимум нагрузки, точно по цели
payload=mtproto_initial,unknown Охватывает всё Лишняя обработка зашифрованных пакетов
payload=all Проще написать Обрабатывает даже пустые ACK

Почему unknown не нужен

После первого пакета MTProto2:

  • Трафик полностью зашифрован
  • Выглядит как случайные данные
  • DPI не может его отличить от обычного шума
  • Обработка = пустая трата CPU

Если блокируют жёстко

Если DPI анализирует паттерны (размеры, тайминги), можно добавить:

nfqws2 \
  --filter-l7=mtproto \
  --payload=mtproto_initial \
  --lua-desync=fake:blob=0x00000000000000000000000000000000:repeats=3 \
  --lua-desync=multisplit:pos=32

Если блокируют по IP

Тут только VPN/proxy поможет, или использовать MTProxy с obfuscation.


TL;DR: Используй --payload=mtproto_initial — этого достаточно в 99% случаев.

Не используйте TLS

Telegram API (api.telegram.org) использует обычный HTTPS/TLS, поэтому is_valid_tls_client_hello корректно определяет его как TLS. Но:

  1. seqovl с подменой SNI ломает соединение с Telegram - серверы Telegram могут проверять SNI или не поддерживать TCP overlap
  2. Пакет дропается (drop) после отправки модифицированных частей, но сервер не принимает их

payload_check(desync) пропускает трафик если в конфиге payload=known (все известные типы)

Для non-TLS fallback по умолчанию = "pattern" → применяется seqovl с raw TLS blob → ломает MTProto и другие протоколы

MTProto не TLS, функция это видит, но fallback=pattern всё равно добавляет TLS blob перед MTProto пакетом → сервер Telegram не понимает такой мусор → соединение разрывается.

Глобальный фильтр

Давайте посмотрим, какие фильтры (--wf) поддерживаются в zapret и как они работают с MTProto.

[10 tools called]

Отличные вопросы! Давайте разберёмся:

Захват MTProto пакетов

Для MTProto нельзя создать простой WinDivert фильтр по пейлоаду, потому что детекция MTProto требует AES расшифровки:

bool IsMTProto(const uint8_t *data, size_t len)
{
	if (len>=64)
	{
/*
		uint8_t decrypt[64];
		aes_ctr_crypt(data+8, 32, data+40, data, 64, decrypt);
		return !memcmp(decrypt+56,"\xEF\xEF\xEF\xEF",4);
*/
		// this way requires only one AES instead of 4
		uint8_t decrypt[16], iv[16];
		aes_context ctx;

		memcpy(iv, data+40, 16);
		ctr_add(iv,3);
		if (!aes_setkey(&ctx, AES_ENCRYPT, data+8, 32) && !aes_cipher(&ctx, iv, decrypt))
		{
			*((uint32_t*)(decrypt+8)) ^= *((uint32_t*)(data+56));
			return !memcmp(decrypt+8,"\xEF\xEF\xEF\xEF",4);
		}
	}
	return false;
}

Поэтому нужно:

  1. Перехватывать весь TCP на порту 443: --wf-tcp-out=443
  2. Фильтровать на уровне nfqws: --filter-l7=mtproto
  3. Использовать --payload=mtproto_initial для реакции только на первый пакет

Можно ли применять dupsid и TLS методы к MTProto?

Нет, нельзя!

dupsid — это TLS-специфичная модификация, которая копирует Session ID из реального TLS ClientHello в фейк. MTProto — это собственный зашифрованный протокол Telegram (AES-CTR), а не TLS. Поэтому:

  • tls_mod=dupsid — не работает
  • tls_mod=rnd,rndsni — не работает
  • blob=fake_default_tls — бесполезен

Что применимо для MTProto:

  • split/multisplit — разбиение пакетов
  • disorder/multidisorder — переупорядочивание
  • fake с generic данными (blob=0x00000000)
  • TTL трюки (ip_ttl, ip_autottl)
  • tcp_md5, badseq и другие TCP fooling методы

Пример для MTProto:

winws --wf-tcp-out=443 \
  --filter-l7=mtproto \
  --payload=mtproto_initial \
  --lua-desync=fake:blob=0x00000000:ip_ttl=5:tcp_md5 \
  --lua-desync=split:pos=8

MTProto детектится только на первом TCP пакете с данными после хендшейка (требуется conntrack).

Отличный вопрос! Это ключевой момент понимания работы DPI и обхода.

Логика работы

DPI определяет протокол по первым пакетам. Если DPI распознал MTProto в начале соединения — он блокирует/ресетит всё соединение. Если не распознал — пропускает дальше.

Поэтому "дурилка" нужна только на первых пакетах:

Клиент ──[mtproto_initial]──> DPI ──> Сервер Telegram
              ↑
         ТУТ нужен desync

После успешного "пробития" первого пакета, DPI уже потерял контекст и не понимает, что это MTProto. Дальнейший трафик идёт без обработки.

Как это реализовано

Параметр --out-range ограничивает диапазон обработки:

winws --wf-tcp-out=443 \
  --filter-l7=mtproto \
  --out-range=-d10 \
  --payload=mtproto_initial \
  --lua-desync=fake:blob=0x00000000:ip_ttl=5:tcp_md5 \
  --lua-desync=split:pos=8
  • --out-range=-d10 — обрабатывать только первые 10 пакетов с данными
  • --payload=mtproto_initial — реагировать только на первый пакет MTProto

Итог

Пакет Обработка
1-й (mtproto_initial) Применяется fake/split/disorder
2-й и далее Пропускается без изменений

Это нормально и правильно — вся суть в том, чтобы сбить DPI на этапе детекции протокола. После этого соединение уже установлено и DPI не знает, что блокировать.

Аналогично работает и для TLS (tls_client_hello), и для QUIC (quic_initial) — обрабатывается только инициализация.

Хороший вопрос! Давайте посмотрю точно в коде:

[1 tool called]

После детекции:

Протокол (l7proto) → known (mtproto)

				l7proto = L7_MTPROTO;
				if (ctrack->l7proto == L7_UNKNOWN) ctrack->l7proto = l7proto;

Протокол сохраняется в conntrack. Все последующие пакеты этого соединения будут иметь l7proto = L7_MTPROTO.

Payload (l7payload) → unknown

Только первый пакет имеет l7payload = L7P_MTPROTO_INITIAL. Все остальные пакеты соединения будут l7payload = L7P_UNKNOWN.

Итоговая картина

Пакет l7proto l7payload
1-й mtproto mtproto_initial
2-й mtproto unknown
3-й mtproto unknown
... mtproto unknown

Что это значит для фильтров

--filter-l7=mtproto           # ✅ Матчит ВСЕ пакеты соединения
--filter-l7=known             # ✅ Матчит ВСЕ пакеты соединения  
--payload=mtproto_initial     # ✅ Матчит только 1-й пакет
--payload=unknown             # ✅ Матчит 2-й и далее

Поэтому типичная конфигурация:

--filter-l7=mtproto --payload=mtproto_initial --lua-desync=...

Обработает только первый пакет, а остальные пройдут мимо (payload не совпадёт).

Механизм запоминания

Conntrack = отслеживание соединений по ключу (src_ip, src_port, dst_ip, dst_port, protocol).

Шаг 1: Первый пакет — детекция и сохранение

			l7proto = L7_MTPROTO;
			if (ctrack->l7proto == L7_UNKNOWN) ctrack->l7proto = l7proto;
		}

При детекции MTProto протокол сохраняется в структуру ctrack этого соединения.

Шаг 2: Последующие пакеты — извлечение из памяти

		if (ctrack) l7proto = ctrack->l7proto;

На каждом новом пакете сначала смотрим в conntrack — если там уже есть определённый протокол, берём его оттуда.

Как это работает

Пакет 1 (mtproto_initial):
  ┌─────────────────────────────────────┐
  │ Conntrack lookup по (ip:port)       │
  │ ctrack->l7proto = UNKNOWN           │
  │ → Детекция: IsMTProto() = true      │
  │ → Сохранение: ctrack->l7proto = MTPROTO │
  └─────────────────────────────────────┘

Пакет 2 (зашифрованный мусор):
  ┌─────────────────────────────────────┐
  │ Conntrack lookup по (ip:port)       │
  │ ctrack->l7proto = MTPROTO  ← уже!   │
  │ → Детекция не нужна                 │
  │ → l7proto = MTPROTO                 │
  └─────────────────────────────────────┘

Ключ соединения

Conntrack идентифицирует соединение по кортежу:

  • Source IP + Source Port
  • Destination IP + Destination Port
  • Protocol (TCP/UDP)

Все пакеты с одинаковым кортежом принадлежат одному соединению и используют одну и ту же структуру ctrack с сохранённым l7proto.

Поэтому даже когда после первого пакета идёт "мусор" — zapret не анализирует содержимое, а просто смотрит "этот пакет от того же соединения? → значит это MTProto".

Покажу примеры для MTProto с разной логикой:

1. Только первый пакет (стандартный случай)

Дурим DPI только на инициализации — этого обычно достаточно:

winws --wf-tcp-out=443 \
  --filter-l7=mtproto \
  --payload=mtproto_initial \
  --lua-desync=fake:blob=0x00000000:ip_ttl=5:tcp_md5 \
  --lua-desync=split:pos=8
Пакет Обработка
1-й (initial) fake + split
2-й и далее пропуск

2. Все пакеты MTProto соединения

Если DPI анализирует весь поток (редко, но бывает):

winws --wf-tcp-out=443 \
  --filter-l7=mtproto \
  --out-range=-d20 \
  --lua-desync=split:pos=8
Пакет Обработка
1-20 split
21+ пропуск

3. Разная логика для initial и остальных

winws --wf-tcp-out=443 \
  --filter-l7=mtproto --payload=mtproto_initial \
    --lua-desync=fake:blob=0x00000000:ip_ttl=3:tcp_md5 \
    --lua-desync=disorder:pos=8 \
  --new \
  --filter-l7=mtproto --payload=unknown --out-range=-d10 \
    --lua-desync=split:pos=16
Пакет Обработка
1-й (initial) fake + disorder
2-10 split
11+ пропуск

4. MTProto + TLS + HTTP в одном инстансе

winws --wf-tcp-out=80,443 \
  --lua-init=@zapret-lib.lua \
  --lua-init=@zapret-antidpi.lua \
  \
  --filter-l7=mtproto --payload=mtproto_initial \
    --lua-desync=fake:blob=0x00000000:ip_ttl=5:tcp_md5 \
    --lua-desync=split:pos=8 \
  --new \
  --filter-l7=tls --payload=tls_client_hello \
    --lua-desync=fake:blob=fake_default_tls:tcp_md5:tls_mod=rnd,dupsid \
    --lua-desync=multisplit:pos=1,midsld \
  --new \
  --filter-l7=http --payload=http_req \
    --lua-desync=fake:blob=fake_default_http:ip_ttl=5 \
    --lua-desync=split:pos=host+1
Протокол Payload Обработка
MTProto initial fake + split
TLS client_hello fake с dupsid + multisplit
HTTP request fake + split по host

5. Только MTProto, игнорировать всё остальное (тоже что и первый)

winws --wf-tcp-out=443 \
  --filter-l7=mtproto \
  --payload=mtproto_initial \
  --lua-desync=fake:blob=0x00000000:ip_ttl=4:repeats=3 \
  --lua-desync=disorder:pos=4,16

TLS на 443 пройдёт мимо — --filter-l7=mtproto его отфильтрует.


6. Всё кроме MTProto

winws --wf-tcp-out=80,443 \
  --filter-l7=tls,http \
  --payload=tls_client_hello,http_req \
  --lua-desync=fake:blob=fake_default_tls:tcp_md5 \
  --lua-desync=multisplit:pos=midsld

MTProto на 443 пройдёт без обработки — он не в --filter-l7.


7. Агрессивный режим — все known протоколы

winws --wf-tcp-out=443 \
  --filter-l7=known \
  --out-range=-d5 \
  --lua-desync=disorder:pos=8
l7proto Обработка
mtproto
tls
http
unknown

Сводная таблица фильтров

Фильтр Что матчит
--filter-l7=mtproto Только MTProto соединения
--filter-l7=known Все распознанные протоколы
--filter-l7=unknown Нераспознанные соединения
--payload=mtproto_initial Только 1-й пакет MTProto
--payload=known Любой распознанный payload
--payload=unknown Нераспознанные пакеты
--out-range=-d10 Первые 10 пакетов с данными

Первый пакет: l7proto="mtproto", l7payload="mtproto_initial"

Последующие пакеты: l7proto="mtproto" (из ctrack), l7payload="unknown"