Modernisations

* Add variable-length ClientHello
* Make it compile on OTP 27+
* Upgrade OTP versions in CI
This commit is contained in:
Sergey Prokhorov 2026-02-17 01:04:19 +01:00
parent f9c2d32d4f
commit 0cc2e02c8c
No known key found for this signature in database
GPG key ID: 1C570244E4EF3337
10 changed files with 126 additions and 33 deletions

View file

@ -18,16 +18,13 @@ jobs:
fail-fast: false
matrix:
os:
- "ubuntu-20.04"
rebar3: ["3.20.0"]
- "ubuntu-22.04"
rebar3: ["3.24.0"]
otp:
- "28.3"
- "27.3"
- "26.2"
- "25.3"
- "24.3"
include:
- otp: "23.3"
rebar3: "3.18.0"
os: "ubuntu-20.04"
env:
SHELL: /bin/sh # needed for erlexec
steps:

View file

@ -5,6 +5,13 @@ This part of code was extracted from [@socksy_bot](https://t.me/socksy_bot).
Support: https://t.me/erlang_mtproxy .
‼️ DON'T USE TELEGRAM FOR SENSITIVE DATA AND POLITICAL ACTIVITY
---------------------------------------------------------------
Telegram is known to cooperate with governments, especially with russian.
Telegram is NOT neutral and NOT fully independent. Please only use Telegram
for non-sensitive messaging. For private messaging prefer Signal or Session.
Features
--------
@ -94,7 +101,7 @@ your server's OS (see below).
How to start OS-install - quick
-----------------------------------
You need at least Erlang version 20! Recommended OS is Ubuntu 18.04.
You need at least Erlang version 25! Recommended OS is Ubuntu 24.04.
```bash
sudo apt install erlang-nox erlang-dev build-essential

View file

@ -6,7 +6,7 @@
{deps, [{ranch, "1.7.0"},
{hut, "1.3.0"},
{lager, "3.9.1"},
{erlang_psq, "1.0.0"}
{erlang_psq, {git, "https://github.com/seriyps/psq", {branch, "master"}}}
]}.
{project_plugins, [rebar3_proper,
rebar3_bench]

View file

@ -1,18 +1,19 @@
{"1.2.0",
[{<<"erlang_psq">>,{pkg,<<"erlang_psq">>,<<"1.0.0">>},0},
[{<<"erlang_psq">>,
{git,"https://github.com/seriyps/psq",
{ref,"46329ccb60f27d7d4c3f3643441ccef6c5f9c89c"}},
0},
{<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},1},
{<<"hut">>,{pkg,<<"hut">>,<<"1.3.0">>},0},
{<<"lager">>,{pkg,<<"lager">>,<<"3.9.1">>},0},
{<<"ranch">>,{pkg,<<"ranch">>,<<"1.7.0">>},0}]}.
[
{pkg_hash,[
{<<"erlang_psq">>, <<"995E328461A5949A54BDFC7686609A08EFB82313914F9AEAD494A2644629EA26">>},
{<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>},
{<<"hut">>, <<"71F2F054E657C03F959CF1ACC43F436EA87580696528CA2A55C8AFB1B06C85E7">>},
{<<"lager">>, <<"5885BC71308CD38F9D025C8ECDE4E5CCE1CE8565F80BFC6199865C845D6DBE95">>},
{<<"ranch">>, <<"9583F47160CA62AF7F8D5DB11454068EAA32B56EEADF984D4F46E61A076DF5F2">>}]},
{pkg_hash_ext,[
{<<"erlang_psq">>, <<"03DA24C3AA84313D57603B6A4B51EB46B4B787FA95BF5668D03E101A466DDFB2">>},
{<<"goldrush">>, <<"99CB4128CFFCB3227581E5D4D803D5413FA643F4EB96523F77D9E6937D994CEB">>},
{<<"hut">>, <<"7E15D28555D8A1F2B5A3A931EC120AF0753E4853A4C66053DB354F35BF9AB563">>},
{<<"lager">>, <<"3F59BA75A04A99E5F18BF91C89F46DCE536F83C6CB415FE26E6E75A62BEF37DC">>},

BIN
rebar3

Binary file not shown.

View file

@ -20,7 +20,9 @@
encode_packet/2]).
-ifdef(TEST).
-export([make_client_hello/2,
make_client_hello/3,
make_client_hello/4,
make_client_hello/5,
parse_server_hello/1]).
-endif.
@ -135,15 +137,15 @@ from_client_hello(Data, Secret) ->
{ok, Response, Meta, new()}.
parse_client_hello(<<?TLS_REC_HANDSHAKE, ?TLS_10_VERSION, 512:?u16, %Frame
?TLS_TAG_CLI_HELLO, 508:?u24, ?TLS_12_VERSION,
parse_client_hello(<<?TLS_REC_HANDSHAKE, ?TLS_10_VERSION, TlsFrameLen:?u16, %Frame
?TLS_TAG_CLI_HELLO, HelloLen:?u24, ?TLS_12_VERSION,
Random:?DIGEST_LEN/binary,
SessIdLen, SessId:SessIdLen/binary,
CipherSuitesLen:?u16, CipherSuites:CipherSuitesLen/binary,
CompMethodsLen, CompMethods:CompMethodsLen/binary,
ExtensionsLen:?u16, Extensions:ExtensionsLen/binary>>
%% _/binary>>
) ->
) when TlsFrameLen >= 512, HelloLen >= 508 ->
#client_hello{
pseudorandom = Random,
session_id = SessId,
@ -243,10 +245,24 @@ make_client_hello(Secret, SniDomain) ->
crypto:strong_rand_bytes(32),
Secret, SniDomain).
%% Generate Fake-TLS "ClientHello" with custom TLS packet length. Used for tests only.
make_client_hello(Secret, SniDomain, TlsPacketLen) ->
make_client_hello(erlang:system_time(second),
crypto:strong_rand_bytes(32),
Secret, SniDomain, TlsPacketLen).
make_client_hello(Timestamp, SessionId, HexSecret, SniDomain) when byte_size(HexSecret) == 32 ->
make_client_hello(Timestamp, SessionId, mtp_handler:unhex(HexSecret), SniDomain);
make_client_hello(Timestamp, SessionId, Secret, SniDomain) when byte_size(SessionId) == 32,
byte_size(Secret) == 16 ->
make_client_hello(Timestamp, SessionId, Secret, SniDomain, 512).
%% @doc Generate ClientHello with custom TLS packet length (for testing variable-length support)
make_client_hello(Timestamp, SessionId, HexSecret, SniDomain, TlsPacketLen) when byte_size(HexSecret) == 32 ->
make_client_hello(Timestamp, SessionId, mtp_handler:unhex(HexSecret), SniDomain, TlsPacketLen);
make_client_hello(Timestamp, SessionId, Secret, SniDomain, TlsPacketLen) when byte_size(SessionId) == 32,
byte_size(Secret) == 16,
TlsPacketLen >= 512 ->
%% Wireshark capture from Telegram Desktop
CipherSuites =
mtp_handler:unhex(<<"eaea130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f0035000a">>),
@ -259,15 +275,21 @@ make_client_hello(Timestamp, SessionId, Secret, SniDomain) when byte_size(Sessio
<<"0033002b00295a5a000100001d0020a4146c3e8573565bb5f5c877a88a98dcbbd46a9b3ca1ab3df7217cc33b4b6d2c">>),
SupportedVersions =
mtp_handler:unhex(<<"002b000b0a1a1a0304030303020301">>),
ExtLen = 401, % From wireshark
%% Calculate extensions length based on desired TLS packet length
%% TLS Frame = Type(1) + Version(2) + Length(2) + Payload
%% Payload = Hello(1) + HelloLen(3) + Version(2) + Random(32) + SessIdLen(1) + SessId
%% + CSLen(2) + CS + CompMethodsLen(1) + CompMethods(1) + ExtLen(2) + Extensions
%% TlsPacketLen = Payload size (everything after the Length field)
HelloLen = TlsPacketLen - 4, % Subtract Hello(1) + HelloLen(3) = 4
ExtLen = TlsPacketLen - (1 + 3 + 2 + 32 + 1 + byte_size(SessionId) + 2 + CSLen + 1 + 1 + 2),
RealExtensions = <<KeyShare/binary, SupportedVersions/binary, SNI/binary>>,
Extensions = add_padding_ext(RealExtensions, ExtLen),
(ExtLen == byte_size(Extensions)) orelse error({bad_ext_len, byte_size(Extensions)}),
(ExtLen == byte_size(Extensions)) orelse error({bad_ext_len, byte_size(Extensions), ExtLen}),
SessIdLen = byte_size(SessionId),
Pack = fun(FakeRandom) ->
<<?TLS_REC_HANDSHAKE, ?TLS_10_VERSION, 512:?u16,
?TLS_TAG_CLI_HELLO, 508:?u24, ?TLS_12_VERSION,
<<?TLS_REC_HANDSHAKE, ?TLS_10_VERSION, TlsPacketLen:?u16,
?TLS_TAG_CLI_HELLO, HelloLen:?u24, ?TLS_12_VERSION,
FakeRandom:?DIGEST_LEN/binary,
SessIdLen, SessionId:SessIdLen/binary,
CSLen:?u16, CipherSuites:CSLen/binary,

View file

@ -30,8 +30,8 @@
-define(HEALTH_CHECK_INTERVAL, 5000).
% telegram server responds with "l\xfe\xff\xff" if client packet MTProto is invalid
-define(SRV_ERROR, <<108, 254, 255, 255>>).
-define(TLS_START, 22, 3, 1, 2, 0, 1, 0, 1, 252, 3, 3).
-define(TLS_CLIENT_HELLO_LEN, 512).
-define(TLS_START, 22, 3, 1).
-define(TLS_CLIENT_HELLO_MIN_LEN, 512).
-define(APP, mtproto_proxy).
@ -311,16 +311,29 @@ handle_upstream_data(Bin, #state{codec = Codec0} = S0) ->
parse_upstream_data(<<?TLS_START, _/binary>> = AllData,
#state{stage = tls_hello, secret = Secret, codec = Codec0,
addr = {Ip, _}, listener = Listener} = S) when
byte_size(AllData) >= (?TLS_CLIENT_HELLO_LEN + 5) ->
assert_protocol(mtp_fake_tls),
<<Data:(?TLS_CLIENT_HELLO_LEN + 5)/binary, Tail/binary>> = AllData,
{ok, Response, Meta, TlsCodec} = mtp_fake_tls:from_client_hello(Data, Secret),
check_tls_policy(Listener, Ip, Meta),
Codec1 = mtp_codec:replace(tls, true, TlsCodec, Codec0),
Codec = mtp_codec:push_back(tls, Tail, Codec1),
ok = up_send_raw(Response, S), %FIXME: if this send fail, we will get counter policy leak
{ok, S#state{codec = Codec, stage = init,
policy_state = {ok, maps:get(sni_domain, Meta, undefined)}}};
byte_size(AllData) >= 5 ->
%% TLS record format: Type(1) + Version(2) + Length(2) + Payload(Length)
%% We need at least 5 bytes to read the header
<<?TLS_START, TlsPacketLen:16/unsigned-big, _/binary>> = AllData,
%% Validate minimum length
(TlsPacketLen >= ?TLS_CLIENT_HELLO_MIN_LEN) orelse
error({protocol_error, tls_client_hello_too_short, TlsPacketLen}),
FullPacketSize = 5 + TlsPacketLen,
case byte_size(AllData) >= FullPacketSize of
true ->
assert_protocol(mtp_fake_tls),
<<Data:FullPacketSize/binary, Tail/binary>> = AllData,
{ok, Response, Meta, TlsCodec} = mtp_fake_tls:from_client_hello(Data, Secret),
check_tls_policy(Listener, Ip, Meta),
Codec1 = mtp_codec:replace(tls, true, TlsCodec, Codec0),
Codec = mtp_codec:push_back(tls, Tail, Codec1),
ok = up_send_raw(Response, S), %FIXME: if this send fail, we will get counter policy leak
{ok, S#state{codec = Codec, stage = init,
policy_state = {ok, maps:get(sni_domain, Meta, undefined)}}};
false ->
%% Wait for more data
{incomplete, S}
end;
parse_upstream_data(<<?TLS_START, _/binary>> = Data, #state{stage = init} = S) ->
parse_upstream_data(Data, S#state{stage = tls_hello});
parse_upstream_data(<<Header:64/binary, Rest/binary>>,

View file

@ -33,7 +33,7 @@ connect(Host, Port, Secret, DcId, Protocol) ->
-spec connect(inet:socket_address() | inet:hostname(),
inet:port_number(),
binary(), binary(), integer(),
mtp_codec:packet_codec() | {mtp_fake_tls, binary()}) -> client().
mtp_codec:packet_codec() | {mtp_fake_tls, binary()} | {mtp_fake_tls, binary(), pos_integer()}) -> client().
connect(Host, Port, Seed, Secret, DcId, Protocol0) ->
Opts = [{packet, raw},
{mode, binary},
@ -51,6 +51,14 @@ connect(Host, Port, Seed, Secret, DcId, Protocol0) ->
%% TODO: if Tail is not empty, use codec:push_back(first, ..)
{_HS, _CC, _D, <<>>} = mtp_fake_tls:parse_server_hello(ServerHello),
{mtp_secure, true, mtp_fake_tls:new()};
{mtp_fake_tls, Domain, TlsPacketLen} ->
ClientHello = mtp_fake_tls:make_client_hello(Secret, Domain, TlsPacketLen),
ok = gen_tcp:send(Sock, ClientHello),
%% Let's hope whole server hello will arrive in a single chunk
{ok, ServerHello} = gen_tcp:recv(Sock, 0, 5000),
%% TODO: if Tail is not empty, use codec:push_back(first, ..)
{_HS, _CC, _D, <<>>} = mtp_fake_tls:parse_server_hello(ServerHello),
{mtp_secure, true, mtp_fake_tls:new()};
_ -> {Protocol0, false, undefined}
end,
{Header0, _, _, CryptoLayer} = mtp_obfuscated:client_create(Seed, Secret, Protocol, DcId),

View file

@ -3,7 +3,7 @@
-include_lib("proper/include/proper.hrl").
-include_lib("stdlib/include/assert.hrl").
-export([prop_codec_small/1, prop_codec_big/1, prop_stream/1]).
-export([prop_codec_small/1, prop_codec_big/1, prop_stream/1, prop_variable_length_hello/1]).
prop_codec_small(doc) ->
"Tests that any binary below 65535 bytes can be encoded and decoded back as single frame".
@ -60,3 +60,28 @@ decode_stream(BinStream, Codec, Acc) ->
{ok, DecPacket, Tail, Codec1} ->
decode_stream(Tail, Codec1, [DecPacket | Acc])
end.
prop_variable_length_hello(doc) ->
"Tests that ClientHello with various packet lengths can be parsed correctly".
prop_variable_length_hello() ->
?FORALL({TlsPacketLen, Secret, Domain},
{proper_types:integer(512, 4096),
proper_types:binary(16),
<<"example.com">>},
variable_length_hello(TlsPacketLen, Secret, Domain)).
variable_length_hello(TlsPacketLen, Secret, Domain) ->
Timestamp = erlang:system_time(second),
SessionId = crypto:strong_rand_bytes(32),
ClientHello = mtp_fake_tls:make_client_hello(Timestamp, SessionId, Secret, Domain, TlsPacketLen),
%% Verify packet has correct length
?assertEqual(5 + TlsPacketLen, byte_size(ClientHello)),
%% 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.

View file

@ -13,6 +13,7 @@
echo_secure_case/1,
echo_abridged_many_packets_case/1,
echo_tls_case/1,
echo_tls_long_hello_case/1,
ipv6_connect_case/1,
packet_too_large_case/1,
policy_max_conns_case/1,
@ -138,6 +139,25 @@ echo_tls_case(Cfg) when is_list(Cfg) ->
ok = mtp_test_client:close(Cli1).
%% @doc Test TLS handshake with long ClientHello (2000 bytes) to simulate newer Telegram clients
echo_tls_long_hello_case({pre, Cfg}) ->
setup_single(?FUNCTION_NAME, 10000 + ?LINE, #{}, Cfg);
echo_tls_long_hello_case({post, Cfg}) ->
stop_single(Cfg);
echo_tls_long_hello_case(Cfg) when is_list(Cfg) ->
DcId = ?config(dc_id, Cfg),
Host = ?config(mtp_host, Cfg),
Port = ?config(mtp_port, Cfg),
Secret = ?config(mtp_secret, Cfg),
%% Test with 2000-byte ClientHello (newer Telegram clients send longer packets)
Cli0 = mtp_test_client:connect(Host, Port, Secret, DcId, {mtp_fake_tls, <<"example.com">>, 2000}),
Cli1 = ping(Cli0),
?assertEqual(
1, mtp_test_metric:get_tags(
count, [?APP, protocol_ok, total], [?FUNCTION_NAME, mtp_secure_fake_tls])),
ok = mtp_test_client:close(Cli1).
%% @doc test that client trying to send too big packets will be force-disconnected
packet_too_large_case({pre, Cfg}) ->
setup_single(?FUNCTION_NAME, 10000 + ?LINE, #{}, Cfg);