Skip to content

Commit 1330c3d

Browse files
seriypsCopilot
andcommitted
Add mtp_ping escript and modernise FakeTLS ClientHello
- Add src/mtp_ping.erl: standalone escript that pings a Telegram MTProto proxy across configurable DC IDs and protocols (normal/secure/fake-tls). Accepts all proxy URL formats (tg://proxy and https://t.me/proxy, hex and base64 secrets). Prints per-attempt timings (TCP/Handshake/Ping/Total) and a two-section summary (protocol status + per-DC averages). Supports --dc, --proto, --timeout, --repeat, --verbose flags. - Modernise mtp_fake_tls:make_client_hello/4 to match tdesktop commit b72deb1 + tdlib commit d0de8a7: - ML-KEM-768 key share (X25519MLKEM768 group 0x11ec, 1184-byte key) - Updated supported_groups: GREASE + X25519MLKEM768 + x25519 + secp256r1 + secp384r1 - Full extension set (17 extensions including ALPS, SCT, status_request, …) - ECH outer extension type 0xfe0d (was 0xfe02), random field 32 bytes (was 20) - ALPS type 0x44cd (was 0x4469) - Variable-length output (~1776 bytes), no fixed padding - Remove legacy make_client_hello/3 and /5 (fixed-padding format) and add_padding_ext/2; update prop_mtp_fake_tls and mtp_test_client/ single_dc_SUITE to use the modern /2 and /4 arities. - Remove ifdef(TEST) guards from mtp_obfuscated and mtp_fake_tls exports needed by mtp_ping; widen DC ID guard in mtp_obfuscated:client_create/4 to full 16-bit signed range. - Document mtp_ping in README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent cb23a30 commit 1330c3d

8 files changed

Lines changed: 757 additions & 89 deletions

File tree

README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,3 +992,78 @@ mtp_config:status().
992992
If you used a fixed dist port in `vm.args` (`inet_dist_listen_min/max 9199`),
993993
only port 9199 needs to be open; otherwise allow the full 91999254 range plus
994994
EPMD (4369). **Never expose the distribution port to the public internet.**
995+
996+
## mtp_ping — proxy connectivity checker
997+
998+
`mtp_ping` is a standalone command line tool that measures connectivity and latency through
999+
a Telegram MTProto proxy to each Telegram DC. It uses the same `req_pq`/`res_pq` handshake
1000+
that real Telegram clients use, so it measures the full client→proxy→DC round-trip, not
1001+
just reachability of the proxy itself.
1002+
1003+
### Build
1004+
1005+
```bash
1006+
./rebar3 escriptize
1007+
# escript is placed at _build/default/bin/mtp_ping
1008+
```
1009+
1010+
> **Note:** `mtp_ping` is an Erlang escript, not a standalone binary.
1011+
> Erlang/OTP 25+ must be installed on the machine where it is run.
1012+
1013+
### Usage
1014+
1015+
```
1016+
./_build/default/bin/mtp_ping [OPTIONS] <proxy-url>
1017+
```
1018+
1019+
Accepts any standard Telegram proxy link (`tg://proxy?…` or `https://t.me/proxy?…`)
1020+
in all secret formats:
1021+
1022+
| Format | Secret example |
1023+
|--------|----------------|
1024+
| Normal | `6c0fc115f28307e3510041fffcaef3bc` |
1025+
| Secure (`dd`) | `dd6c0fc115f28307e3510041fffcaef3bc` |
1026+
| Fake-TLS hex (`ee`) | `ee6c0fc115f28307e3510041fffcaef3bc67707472752e70726f` |
1027+
| Fake-TLS base64 | `7mwPwRXygwfjUQBB__yu87xncHRydS5wcm8=` |
1028+
1029+
Options:
1030+
1031+
| Option | Default | Description |
1032+
|--------|---------|-------------|
1033+
| `--dc ID,...` | `-5…-1,1…5` | DC IDs to test. Current valid IDs: https://core.telegram.org/getProxyConfig |
1034+
| `--proto P,...` | from URL | Protocols: `normal`, `secure`, `fake-tls` |
1035+
| `--timeout MS` | `5000` | Per-attempt TCP timeout (ms) |
1036+
| `--repeat N` | `1` | Repeat each ping N times and show averages |
1037+
| `--verbose` / `-v` | off | Print full error stacktraces |
1038+
1039+
### Example
1040+
1041+
```
1042+
$ ./_build/default/bin/mtp_ping --proto fake-tls --dc 1,2,3 --repeat 3 \
1043+
"https://t.me/proxy?server=tg.example.com&port=443&secret=ee..."
1044+
1045+
Proxy : tg.example.com:443
1046+
Testing : 1 protocol(s) x 3 DC(s), timeout=5000ms, showing avg over 3 repeats
1047+
1048+
fake-tls DC +1 : tcp=45ms handshake=52ms ping=140ms [total=237ms] OK
1049+
fake-tls DC +2 : tcp=48ms handshake=54ms ping=155ms [total=257ms] OK
1050+
fake-tls DC +3 : tcp=46ms handshake=51ms ping=148ms [total=245ms] OK
1051+
1052+
=== Summary ===
1053+
1054+
Protocols:
1055+
fake-tls OK (3/3 DCs)
1056+
1057+
Avg timings per DC (across 1 working protocol(s)):
1058+
DC TCP(ms) Handshake(ms) Ping(ms) Total(ms)
1059+
--------------------------------------------------
1060+
+1 45 52 140 237
1061+
+2 48 54 155 257
1062+
+3 46 51 148 245
1063+
```
1064+
1065+
The summary shows:
1066+
- **Protocols** — whether each protocol got at least one successful ping (OK) or all
1067+
attempts failed (DISABLED), with the DC success ratio.
1068+
- **Avg timings per DC** — average TCP connect / TLS handshake / MTProto `req_pq` round-trip
1069+
and total time, averaged over all protocols and repeats.

rebar.config

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
% -*- mode: erlang -*-
22
{erl_opts, [debug_info]}.
33

4+
{escript_name, mtp_ping}.
5+
{escript_main_app, mtproto_proxy}.
6+
{escript_emu_args, "%%! +sbtu +A1\n"}.
7+
48
{deps, [{ranch, "2.2.0"},
59
{erlang_psq, {git, "https://github.com/seriyps/psq", {branch, "master"}}}
610
]}.

src/mtp_fake_tls.erl

Lines changed: 139 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,9 @@
2121
try_decode_packet/2,
2222
decode_all/2,
2323
encode_packet/2]).
24-
-ifdef(TEST).
2524
-export([make_client_hello/2,
26-
make_client_hello/3,
2725
make_client_hello/4,
28-
make_client_hello/5,
2926
parse_server_hello/1]).
30-
-endif.
3127

3228
-export_type([codec/0, meta/0]).
3329

@@ -294,63 +290,159 @@ make_srv_hello(Digest, SessionId, {KeyShareGroup, KeyShareKey}) ->
294290
| Extensions],
295291
[<<?TLS_TAG_SRV_HELLO, (iolist_size(Payload)):?u24>> | Payload].
296292

297-
-ifdef(TEST).
298-
%% Generate Fake-TLS "ClientHello". Used for tests only.
293+
%% Generate Fake-TLS "ClientHello".
299294
make_client_hello(Secret, SniDomain) ->
300295
make_client_hello(erlang:system_time(second),
301296
crypto:strong_rand_bytes(32),
302297
Secret, SniDomain).
303298

304-
%% Generate Fake-TLS "ClientHello" with custom TLS packet length. Used for tests only.
305-
make_client_hello(Secret, SniDomain, TlsPacketLen) ->
306-
make_client_hello(erlang:system_time(second),
307-
crypto:strong_rand_bytes(32),
308-
Secret, SniDomain, TlsPacketLen).
309-
310299
make_client_hello(Timestamp, SessionId, HexSecret, SniDomain) when byte_size(HexSecret) == 32 ->
311300
make_client_hello(Timestamp, SessionId, mtp_handler:unhex(HexSecret), SniDomain);
312301
make_client_hello(Timestamp, SessionId, Secret, SniDomain) when byte_size(SessionId) == 32,
313302
byte_size(Secret) == 16 ->
314-
make_client_hello(Timestamp, SessionId, Secret, SniDomain, 512).
315-
316-
%% @doc Generate ClientHello with custom TLS packet length (for testing variable-length support)
317-
make_client_hello(Timestamp, SessionId, HexSecret, SniDomain, TlsPacketLen) when byte_size(HexSecret) == 32 ->
318-
make_client_hello(Timestamp, SessionId, mtp_handler:unhex(HexSecret), SniDomain, TlsPacketLen);
319-
make_client_hello(Timestamp, SessionId, Secret, SniDomain, TlsPacketLen) when byte_size(SessionId) == 32,
320-
byte_size(Secret) == 16,
321-
TlsPacketLen >= 512 ->
322-
%% Wireshark capture from Telegram Desktop
303+
%% Modern ClientHello following tdesktop b72deb1 + tdlib d0de8a7.
304+
%% Variable length (no fixed padding); proxy only validates the HMAC in Random.
305+
GREASE = <<16#ea, 16#ea>>,
306+
307+
%% Cipher suites: GREASE + 15 standard suites (TLS_RSA_WITH_3DES removed)
323308
CipherSuites =
324-
mtp_handler:unhex(<<"eaea130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f0035000a">>),
325-
CSLen = byte_size(CipherSuites),
309+
<<GREASE/binary,
310+
16#13, 16#01, % TLS_AES_128_GCM_SHA256
311+
16#13, 16#02, % TLS_AES_256_GCM_SHA384
312+
16#13, 16#03, % TLS_CHACHA20_POLY1305_SHA256
313+
16#c0, 16#2b, % ECDHE_ECDSA_AES128_GCM_SHA256
314+
16#c0, 16#2f, % ECDHE_RSA_AES128_GCM_SHA256
315+
16#c0, 16#2c, % ECDHE_ECDSA_AES256_GCM_SHA384
316+
16#c0, 16#30, % ECDHE_RSA_AES256_GCM_SHA384
317+
16#cc, 16#a9, % ECDHE_ECDSA_CHACHA20_POLY1305
318+
16#cc, 16#a8, % ECDHE_RSA_CHACHA20_POLY1305
319+
16#c0, 16#13, % ECDHE_RSA_AES128_CBC_SHA
320+
16#c0, 16#14, % ECDHE_RSA_AES256_CBC_SHA
321+
16#00, 16#9c, % RSA_AES128_GCM_SHA256
322+
16#00, 16#9d, % RSA_AES256_GCM_SHA384
323+
16#00, 16#2f, % RSA_AES128_CBC_SHA
324+
16#00, 16#35>>, % RSA_AES256_CBC_SHA
326325

327326
SNI = make_sni([SniDomain]),
328-
%% Wireshark capture from Telegram Desktop
329-
KeyShare =
330-
mtp_handler:unhex(
331-
<<"0033002b00295a5a000100001d0020a4146c3e8573565bb5f5c877a88a98dcbbd46a9b3ca1ab3df7217cc33b4b6d2c">>),
327+
328+
SigAlgos =
329+
<<16#00, 16#0d, % signature_algorithms
330+
16#00, 16#20, % ext length 32 (2 list-len + 30 entries)
331+
16#00, 16#1e, % sig-algo list length 30 (15 algos × 2)
332+
16#04, 16#03, % ecdsa_secp256r1_sha256
333+
16#05, 16#03, % ecdsa_secp384r1_sha384
334+
16#06, 16#03, % ecdsa_secp521r1_sha512
335+
16#02, 16#03, % ecdsa_sha1
336+
16#08, 16#04, % rsa_pss_rsae_sha256
337+
16#08, 16#05, % rsa_pss_rsae_sha384
338+
16#08, 16#06, % rsa_pss_rsae_sha512
339+
16#04, 16#01, % rsa_pkcs1_sha256
340+
16#05, 16#01, % rsa_pkcs1_sha384
341+
16#06, 16#01, % rsa_pkcs1_sha512
342+
16#02, 16#01, % rsa_pkcs1_sha1
343+
16#04, 16#02,
344+
16#03, 16#02,
345+
16#02, 16#02,
346+
16#03, 16#01>>,
347+
348+
%% supported_groups: added X25519MLKEM768 (0x11ec), per tdesktop b72deb1
349+
SupportedGroups =
350+
<<16#00, 16#0a, % supported_groups
351+
16#00, 16#0c, % ext length 12 (2 list-len + 10 entries)
352+
16#00, 16#0a, % named-group list length 10 (5 groups × 2)
353+
GREASE/binary, % GREASE named group
354+
16#11, 16#ec, % X25519MLKEM768 (new)
355+
16#00, 16#1d, % x25519
356+
16#00, 16#17, % secp256r1
357+
16#00, 16#18>>, % secp384r1
358+
332359
SupportedVersions =
333-
mtp_handler:unhex(<<"002b000b0a1a1a0304030303020301">>),
334-
%% Calculate extensions length based on desired TLS packet length
335-
%% TLS Frame = Type(1) + Version(2) + Length(2) + Payload
336-
%% Payload = Hello(1) + HelloLen(3) + Version(2) + Random(32) + SessIdLen(1) + SessId
337-
%% + CSLen(2) + CS + CompMethodsLen(1) + CompMethods(1) + ExtLen(2) + Extensions
338-
%% TlsPacketLen = Payload size (everything after the Length field)
339-
HelloLen = TlsPacketLen - 4, % Subtract Hello(1) + HelloLen(3) = 4
340-
ExtLen = TlsPacketLen - (1 + 3 + 2 + 32 + 1 + byte_size(SessionId) + 2 + CSLen + 1 + 1 + 2),
341-
RealExtensions = <<KeyShare/binary, SupportedVersions/binary, SNI/binary>>,
342-
Extensions = add_padding_ext(RealExtensions, ExtLen),
343-
(ExtLen == byte_size(Extensions)) orelse error({bad_ext_len, byte_size(Extensions), ExtLen}),
344-
345-
SessIdLen = byte_size(SessionId),
360+
<<16#00, 16#2b, % supported_versions
361+
16#00, 16#0b, % ext length 11 (1 list-len + 10 entries)
362+
16#0a, % version list length 10 (5 versions × 2)
363+
GREASE/binary, % GREASE version
364+
16#03, 16#04, % TLS 1.3
365+
16#03, 16#03, % TLS 1.2
366+
16#03, 16#02, % TLS 1.1
367+
16#03, 16#01>>, % TLS 1.0
368+
369+
%% key_share: GREASE + X25519MLKEM768 (1184+32 bytes) + standalone X25519 (32 bytes)
370+
%% The proxy and server-side make_key_share/1 will pick the standalone X25519 entry.
371+
%% ML-KEM-768 public key: 1184 random bytes (proxy does not validate key material)
372+
MlKem768Key = crypto:strong_rand_bytes(1184),
373+
KSKey1 = crypto:strong_rand_bytes(32), % X25519 component of the hybrid entry
374+
KSKey2 = crypto:strong_rand_bytes(32), % standalone X25519 key (picked by proxy)
375+
KeyShareEntries =
376+
<<GREASE/binary, 16#00, 16#01, 16#00, % GREASE: group + key_len=1 + 0x00
377+
16#11, 16#ec, 16#04, 16#c0, % X25519MLKEM768: group + key_len=1216
378+
MlKem768Key/binary, % ML-KEM-768 public key (1184 bytes)
379+
KSKey1/binary, % X25519 component (32 bytes)
380+
16#00, 16#1d, 16#00, 16#20, % x25519: group + key_len=32
381+
KSKey2/binary>>, % standalone X25519 public key
382+
KSListLen = byte_size(KeyShareEntries), % 5 + 1220 + 36 = 1261
383+
KeyShare =
384+
<<16#00, 16#33, % key_share
385+
(KSListLen + 2):?u16, % ext length = list-len field (2) + entries
386+
KSListLen:?u16,
387+
KeyShareEntries/binary>>,
388+
389+
%% Encrypted Client Hello outer (0xfe0d), per tdlib d0de8a7
390+
%% (previously 0xfe02; the \x00\x20 + 32-byte field was also extended from 20 to 32 bytes)
391+
EchRand1 = crypto:strong_rand_bytes(1),
392+
EchRand32 = crypto:strong_rand_bytes(32),
393+
EchPayload = crypto:strong_rand_bytes(176), % fixed choice from {144,176,208,240}
394+
EchContent =
395+
<<16#00, 16#00, 16#01, 16#00, 16#01, % fixed ECH outer header
396+
EchRand1/binary, % 1 random byte
397+
16#00, 16#20,
398+
EchRand32/binary, % 32 random bytes
399+
(byte_size(EchPayload)):?u16,
400+
EchPayload/binary>>,
401+
ECH =
402+
<<16#fe, 16#0d, % ech_outer_extensions (0xfe0d)
403+
(byte_size(EchContent)):?u16,
404+
EchContent/binary>>,
405+
406+
Extensions =
407+
[<<GREASE/binary, 0:16>>, % leading GREASE extension (empty)
408+
SNI, % server_name (0x0000)
409+
<<16#00, 16#17, 0:16>>, % extended_master_secret (empty)
410+
<<16#00, 16#23, 0:16>>, % session_ticket (empty)
411+
SigAlgos, % signature_algorithms (0x000d)
412+
<<16#00, 16#05, 16#00, 16#05, % status_request (OCSP)
413+
16#01, 0:32>>, % type=ocsp(1) + empty responder list(2) + empty exts(2)
414+
SupportedGroups, % supported_groups (0x000a)
415+
<<16#00, 16#0b, 16#00, 16#02, 16#01, 16#00>>, % ec_point_formats: uncompressed
416+
<<16#00, 16#12, 0:16>>, % signed_certificate_timestamp (empty)
417+
<<16#00, 16#10, 16#00, 16#0e, % application_layer_protocol_negotiation
418+
16#00, 16#0c, % protocol list length 12
419+
16#02, $h, $2, % "h2"
420+
16#08, $h, $t, $t, $p, $/, $1, $., $1>>, % "http/1.1"
421+
<<16#44, 16#cd, 16#00, 16#05, % application_layer_protocol_settings
422+
16#00, 16#03, 16#02, $h, $2>>, % (type updated 0x4469→0x44cd in b72deb1)
423+
SupportedVersions, % supported_versions (0x002b)
424+
<<16#00, 16#2d, 16#00, 16#02, 16#01, 16#01>>, % psk_key_exchange_modes: psk_dhe_ke
425+
KeyShare, % key_share (0x0033)
426+
ECH, % encrypted_client_hello (0xfe0d)
427+
<<16#ff, 16#01, 16#00, 16#01, 16#00>>, % renegotiation_info (empty)
428+
<<GREASE/binary, 16#00, 16#01, 16#00>>], % trailing GREASE extension
429+
430+
ExtBin = iolist_to_binary(Extensions),
431+
CSLen = byte_size(CipherSuites),
432+
SessIdLen = byte_size(SessionId), % always 32
433+
ExtLen = byte_size(ExtBin),
434+
%% HelloBodyLen: TLS version(2) + Random(32) + SessIdLen(1) + SessId + CSLen(2) + CS
435+
%% + CompMethodsLen(1) + CompMethod(1) + ExtLen(2) + Extensions
436+
HelloBodyLen = 2 + 32 + 1 + SessIdLen + 2 + CSLen + 1 + 1 + 2 + ExtLen,
437+
TlsLen = HelloBodyLen + 4, % +4 for handshake type(1) + handshake length(3)
346438
Pack = fun(FakeRandom) ->
347-
<<?TLS_REC_HANDSHAKE, ?TLS_10_VERSION, TlsPacketLen:?u16,
348-
?TLS_TAG_CLI_HELLO, HelloLen:?u24, ?TLS_12_VERSION,
439+
<<?TLS_REC_HANDSHAKE, ?TLS_10_VERSION, TlsLen:?u16,
440+
?TLS_TAG_CLI_HELLO, HelloBodyLen:?u24, ?TLS_12_VERSION,
349441
FakeRandom:?DIGEST_LEN/binary,
350-
SessIdLen, SessionId:SessIdLen/binary,
351-
CSLen:?u16, CipherSuites:CSLen/binary,
352-
1, 0, %Compression methods
353-
ExtLen:?u16, Extensions:ExtLen/binary>>
442+
SessIdLen, SessionId/binary,
443+
CSLen:?u16, CipherSuites/binary,
444+
1, 0, % 1 compression method: null(0)
445+
ExtLen:?u16, ExtBin/binary>>
354446
end,
355447
FakeRandom0 = binary:copy(<<0>>, ?DIGEST_LEN),
356448
Hello0 = Pack(FakeRandom0),
@@ -365,15 +457,7 @@ make_sni(Domains) ->
365457
ItemsLen = byte_size(SniListItems),
366458
<<?EXT_SNI:?u16, (ItemsLen + 2):?u16, ItemsLen:?u16, SniListItems/binary>>.
367459

368-
add_padding_ext(RealExtensions, ExtLen) ->
369-
RealExtLen = byte_size(RealExtensions),
370-
PadSize = ExtLen - RealExtLen - 4,
371-
PaddingExt = <<21:?u16, %EXT_PADDING
372-
PadSize:?u16,
373-
(binary:copy(<<0>>, PadSize))/binary>>,
374-
<<RealExtensions/binary, PaddingExt/binary>>.
375-
376-
%% Parses "ServerHello" (the one produced by from_client_hello/2). Used for tests only.
460+
%% Parses "ServerHello" (the one produced by from_client_hello/2).
377461
parse_server_hello(<<?TLS_REC_HANDSHAKE, ?TLS_12_VERSION, HSLen:?u16, Handshake:HSLen/binary,
378462
?TLS_REC_CHANGE_CIPHER, ?TLS_12_VERSION, CCLen:?u16, ChangeCipher:CCLen/binary,
379463
?TLS_REC_DATA, ?TLS_12_VERSION, DLen:?u16, Data:DLen/binary,
@@ -382,8 +466,6 @@ parse_server_hello(<<?TLS_REC_HANDSHAKE, ?TLS_12_VERSION, HSLen:?u16, Handshake:
382466
parse_server_hello(B) when byte_size(B) < (512 + 5) ->
383467
incomplete.
384468

385-
-endif.
386-
387469
%% Data stream codec
388470

389471
-spec new() -> codec().

src/mtp_obfuscated.erl

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@
1515
encode_packet/2
1616
]).
1717
-export([bin_rev/1]).
18-
-ifdef(TEST).
1918
-export([client_create/3,
2019
client_create/4]).
21-
-endif.
2220

2321
-export_type([codec/0]).
2422

@@ -34,7 +32,6 @@
3432

3533
-opaque codec() :: #st{}.
3634

37-
-ifdef(TEST).
3835
client_create(Secret, Protocol, DcId) ->
3936
client_create(crypto:strong_rand_bytes(58),
4037
Secret, Protocol, DcId).
@@ -54,8 +51,8 @@ client_create(Seed, HexSecret, Protocol, DcId) when byte_size(HexSecret) == 32 -
5451
client_create(Seed, mtp_handler:unhex(HexSecret), Protocol, DcId);
5552
client_create(Seed, Secret, Protocol, DcId) when byte_size(Seed) == 58,
5653
byte_size(Secret) == 16,
57-
DcId > -10,
58-
DcId < 10,
54+
DcId >= -32768,
55+
DcId =< 32767,
5956
is_atom(Protocol) ->
6057
<<L:56/binary, R:2/binary>> = Seed,
6158
ProtocolBin = encode_protocol(Protocol),
@@ -92,7 +89,6 @@ encode_protocol(mtp_secure) ->
9289
%% 4byte
9390
encode_dc_id(DcId) ->
9491
<<DcId:16/signed-little-integer>>.
95-
-endif.
9692

9793
%% @doc creates new obfuscated stream (MTProto proxy format)
9894
-spec from_header(binary(), binary()) -> {ok, integer(), mtp_codec:packet_codec(), codec()}

0 commit comments

Comments
 (0)