mirror of
https://github.com/seriyps/mtproto_proxy.git
synced 2026-05-13 08:46:46 +00:00
Modernisations
* Add variable-length ClientHello * Make it compile on OTP 27+ * Upgrade OTP versions in CI
This commit is contained in:
parent
f9c2d32d4f
commit
0cc2e02c8c
10 changed files with 126 additions and 33 deletions
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
BIN
rebar3
Binary file not shown.
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>>,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue