diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b55b21..716b487 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/README.md b/README.md index a5b5632..2103ccb 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/rebar.config b/rebar.config index 7bc3918..eda1262 100644 --- a/rebar.config +++ b/rebar.config @@ -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] diff --git a/rebar.lock b/rebar.lock index 638a76d..8e6d5f0 100644 --- a/rebar.lock +++ b/rebar.lock @@ -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">>}, diff --git a/rebar3 b/rebar3 index a83d554..2054dc7 100755 Binary files a/rebar3 and b/rebar3 differ diff --git a/src/mtp_fake_tls.erl b/src/mtp_fake_tls.erl index b7d07b6..66ae6bd 100644 --- a/src/mtp_fake_tls.erl +++ b/src/mtp_fake_tls.erl @@ -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(<> %% _/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 = <>, 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) -> - <>). --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(<> = 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), - <> = 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 + <> = 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), + <> = 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(<> = Data, #state{stage = init} = S) -> parse_upstream_data(Data, S#state{stage = tls_hello}); parse_upstream_data(<>, diff --git a/test/mtp_test_client.erl b/test/mtp_test_client.erl index 8ef14de..ba7f228 100644 --- a/test/mtp_test_client.erl +++ b/test/mtp_test_client.erl @@ -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), diff --git a/test/prop_mtp_fake_tls.erl b/test/prop_mtp_fake_tls.erl index 05f1710..0745317 100644 --- a/test/prop_mtp_fake_tls.erl +++ b/test/prop_mtp_fake_tls.erl @@ -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. diff --git a/test/single_dc_SUITE.erl b/test/single_dc_SUITE.erl index e07cfde..63091fd 100644 --- a/test/single_dc_SUITE.erl +++ b/test/single_dc_SUITE.erl @@ -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);