mirror of
https://github.com/seriyps/mtproto_proxy.git
synced 2026-05-13 08:46:46 +00:00
- 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 - Extension order shuffled per-connection (match tdesktop fingerprint evasion) - supported_versions: TLS 1.3 + 1.2 only (drop 1.0 and 1.1) - Add compress_certificate extension (brotli) - 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. - Improve parse_server_hello to correctly handle fragmented TLS responses: replace the fixed 517-byte incomplete threshold with structure-aware record counting (tls_records_complete/2); distinguish tls_domain_forwarding (proxy forwarded to SNI host), tls_alert (proxy rejected ClientHello), and not_proxy_response; propagate these to mtp_ping error messages. - Document mtp_ping in README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
151 lines
5.6 KiB
Erlang
151 lines
5.6 KiB
Erlang
%% @doc property-based tests for mtp_fake_tls
|
|
-module(prop_mtp_fake_tls).
|
|
-include_lib("proper/include/proper.hrl").
|
|
-include_lib("stdlib/include/assert.hrl").
|
|
|
|
-export([prop_codec_small/1, prop_codec_big/1, prop_stream/1,
|
|
prop_variable_length_hello/1,
|
|
prop_parse_sni_valid/1,
|
|
prop_parse_sni_garbage/1,
|
|
prop_derive_sni_secret/1]).
|
|
|
|
prop_codec_small(doc) ->
|
|
"Tests that any binary below 65535 bytes can be encoded and decoded back as single frame".
|
|
|
|
prop_codec_small() ->
|
|
?FORALL(Bin, mtp_prop_gen:binary(8, 16 * 1024), codec_small(Bin)).
|
|
|
|
codec_small(Bin) ->
|
|
%% fake_tls can split big packets to multiple TLS frames of 2^14b
|
|
Codec = mtp_fake_tls:new(),
|
|
{Data, Codec1} = mtp_fake_tls:encode_packet(Bin, Codec),
|
|
{ok, Decoded, <<>>, _} = mtp_fake_tls:try_decode_packet(iolist_to_binary(Data), Codec1),
|
|
Decoded == Bin.
|
|
|
|
|
|
prop_codec_big(doc) ->
|
|
"Tests that big binaries will be split to multiple chunks".
|
|
|
|
prop_codec_big() ->
|
|
?FORALL(Bin, mtp_prop_gen:binary(16 * 1024, 65535), codec_big(Bin)).
|
|
|
|
codec_big(Bin) ->
|
|
Codec = mtp_fake_tls:new(),
|
|
{Data, Codec1} = mtp_fake_tls:encode_packet(Bin, Codec),
|
|
Chunks = decode_stream(iolist_to_binary(Data), Codec1, []),
|
|
?assert(length(Chunks) > 1),
|
|
?assertEqual(Bin, iolist_to_binary(Chunks)),
|
|
true.
|
|
|
|
|
|
prop_stream(doc) ->
|
|
"Tests that set of packets of size below 2^14b can be encoded and decoded back".
|
|
|
|
prop_stream() ->
|
|
?FORALL(Stream, proper_types:list(mtp_prop_gen:binary(8, 16000)),
|
|
codec_stream(Stream)).
|
|
|
|
codec_stream(Stream) ->
|
|
Codec = mtp_fake_tls:new(),
|
|
{BinStream, Codec1} =
|
|
lists:foldl(
|
|
fun(Bin, {Acc, Codec1}) ->
|
|
{Data, Codec2} = mtp_fake_tls:encode_packet(Bin, Codec1),
|
|
{<<Acc/binary, (iolist_to_binary(Data))/binary>>,
|
|
Codec2}
|
|
end, {<<>>, Codec}, Stream),
|
|
DecodedStream = decode_stream(BinStream, Codec1, []),
|
|
Stream == DecodedStream.
|
|
|
|
decode_stream(BinStream, Codec, Acc) ->
|
|
case mtp_fake_tls:try_decode_packet(BinStream, Codec) of
|
|
{incomplete, _} ->
|
|
lists:reverse(Acc);
|
|
{ok, DecPacket, Tail, Codec1} ->
|
|
decode_stream(Tail, Codec1, [DecPacket | Acc])
|
|
end.
|
|
|
|
|
|
prop_variable_length_hello(doc) ->
|
|
"Tests that ClientHello generated with various secrets/domains can be parsed correctly".
|
|
|
|
prop_variable_length_hello() ->
|
|
?FORALL({Secret, Domain},
|
|
{proper_types:binary(16),
|
|
<<"example.com">>},
|
|
variable_length_hello(Secret, Domain)).
|
|
|
|
variable_length_hello(Secret, Domain) ->
|
|
Timestamp = erlang:system_time(second),
|
|
SessionId = crypto:strong_rand_bytes(32),
|
|
ClientHello = mtp_fake_tls:make_client_hello(Timestamp, SessionId, Secret, Domain),
|
|
%% Verify handshake can be parsed
|
|
{ok, _Response, Meta, _Codec} = mtp_fake_tls:from_client_hello(ClientHello, Secret),
|
|
%% Verify metadata
|
|
?assertEqual(SessionId, maps:get(session_id, Meta)),
|
|
?assertEqual(Timestamp, maps:get(timestamp, Meta)),
|
|
?assertEqual(Domain, maps:get(sni_domain, Meta)),
|
|
true.
|
|
|
|
|
|
prop_parse_sni_valid(doc) ->
|
|
"parse_sni/1 returns {ok, Domain} for any valid ClientHello with SNI".
|
|
|
|
prop_parse_sni_valid() ->
|
|
?FORALL({Secret, Domain},
|
|
{proper_types:binary(16),
|
|
<<"example.com">>},
|
|
parse_sni_valid(Secret, Domain)).
|
|
|
|
parse_sni_valid(Secret, Domain) ->
|
|
Timestamp = erlang:system_time(second),
|
|
SessionId = crypto:strong_rand_bytes(32),
|
|
%% Build a ClientHello with a WRONG secret so from_client_hello/2 would throw
|
|
WrongSecret = crypto:strong_rand_bytes(16),
|
|
ClientHello = mtp_fake_tls:make_client_hello(Timestamp, SessionId, WrongSecret, Domain),
|
|
%% parse_sni/1 must still extract the domain regardless of the secret
|
|
?assertEqual({ok, Domain}, mtp_fake_tls:parse_sni(ClientHello)),
|
|
%% Also works on a correctly-signed hello
|
|
ValidHello = mtp_fake_tls:make_client_hello(Timestamp, SessionId, Secret, Domain),
|
|
?assertEqual({ok, Domain}, mtp_fake_tls:parse_sni(ValidHello)),
|
|
true.
|
|
|
|
|
|
prop_parse_sni_garbage(doc) ->
|
|
"parse_sni/1 returns {error, bad_hello} for arbitrary garbage binaries".
|
|
|
|
prop_parse_sni_garbage() ->
|
|
?FORALL(Bin, proper_types:binary(), parse_sni_garbage(Bin)).
|
|
|
|
parse_sni_garbage(Bin) ->
|
|
Result = mtp_fake_tls:parse_sni(Bin),
|
|
?assert(Result =:= {error, bad_hello} orelse Result =:= {error, no_sni}),
|
|
true.
|
|
|
|
|
|
prop_derive_sni_secret(doc) ->
|
|
"derive_sni_secret/3 produces a 16-byte secret that is stable and domain/salt/secret-specific".
|
|
|
|
prop_derive_sni_secret() ->
|
|
?FORALL({Secret, Sni, Salt},
|
|
{proper_types:binary(16),
|
|
proper_types:non_empty(proper_types:binary()),
|
|
proper_types:non_empty(proper_types:binary())},
|
|
derive_sni_secret(Secret, Sni, Salt)).
|
|
|
|
derive_sni_secret(Secret, Sni, Salt) ->
|
|
Derived = mtp_fake_tls:derive_sni_secret(Secret, Sni, Salt),
|
|
%% Always 16 bytes
|
|
?assertEqual(16, byte_size(Derived)),
|
|
%% Deterministic
|
|
?assertEqual(Derived, mtp_fake_tls:derive_sni_secret(Secret, Sni, Salt)),
|
|
%% Different SNI → different secret
|
|
OtherSni = <<Sni/binary, "_other">>,
|
|
?assertNotEqual(Derived, mtp_fake_tls:derive_sni_secret(Secret, OtherSni, Salt)),
|
|
%% Different salt → different secret
|
|
OtherSalt = <<Salt/binary, "_other">>,
|
|
?assertNotEqual(Derived, mtp_fake_tls:derive_sni_secret(Secret, Sni, OtherSalt)),
|
|
%% Different base secret → different derived secret
|
|
OtherSecret = crypto:strong_rand_bytes(16),
|
|
?assertNotEqual(Derived, mtp_fake_tls:derive_sni_secret(OtherSecret, Sni, Salt)),
|
|
true.
|