mirror of
https://github.com/seriyps/mtproto_proxy.git
synced 2026-08-04 07:08:10 +00:00
Fake TLS protocol
* pretends being TLS1.3 + http2 * mtp_handler state-machine refactored
This commit is contained in:
parent
c711054bf2
commit
0a1604d4be
11 changed files with 457 additions and 82 deletions
|
|
@ -11,12 +11,11 @@
|
|||
|
||||
-module(mtp_codec).
|
||||
|
||||
-export([new/4,
|
||||
decompose/1,
|
||||
-export([new/4, new/7,
|
||||
info/2, replace/4, push_back/3, is_empty/1,
|
||||
try_decode_packet/2,
|
||||
encode_packet/2,
|
||||
fold_packets/4,
|
||||
is_empty/1]).
|
||||
fold_packets/4]).
|
||||
-export_type([codec/0]).
|
||||
|
||||
-type state() :: any().
|
||||
|
|
@ -26,18 +25,27 @@
|
|||
-type packet_codec() :: mtp_abridged
|
||||
| mtp_full
|
||||
| mtp_intermediate
|
||||
| mtp_secure.
|
||||
| mtp_secure
|
||||
| mtp_noop_codec.
|
||||
-type layer() :: tls | crypto | packet.
|
||||
|
||||
-define(MAX_BUFS_SIZE, 2 * 1024 * 1024).
|
||||
|
||||
-record(codec,
|
||||
{crypto_mod :: crypto_codec(),
|
||||
{have_tls :: boolean(),
|
||||
tls_state :: mtp_fake_tls:codec() | undefined,
|
||||
tls_buf = <<>> :: binary(),
|
||||
crypto_mod :: crypto_codec(),
|
||||
crypto_state :: any(),
|
||||
crypto_buf = <<>> :: binary(),
|
||||
packet_mod :: packet_codec(),
|
||||
packet_state :: any(),
|
||||
packet_buf = <<>> :: binary()}).
|
||||
packet_buf = <<>> :: binary(),
|
||||
limit = ?MAX_BUFS_SIZE :: pos_integer()}).
|
||||
|
||||
-define(APP, mtproto_proxy).
|
||||
|
||||
|
||||
-callback try_decode_packet(binary(), state()) ->
|
||||
{ok, Packet :: binary(), Tail :: binary(), state()}
|
||||
| {incomplete, state()}.
|
||||
|
|
@ -47,27 +55,77 @@
|
|||
|
||||
-opaque codec() :: #codec{}.
|
||||
|
||||
|
||||
-spec new(crypto_codec(), state(), packet_codec(), state()) -> codec().
|
||||
new(CryptoMod, CryptoState, PacketMod, PacketState) ->
|
||||
#codec{crypto_mod = CryptoMod,
|
||||
new(CryptoMod, CryptoState, PacketMod, PacketState, false, undefined, ?MAX_BUFS_SIZE).
|
||||
|
||||
-spec new(crypto_codec(), state(), packet_codec(), state(), boolean(), any(), pos_integer()) -> codec().
|
||||
new(CryptoMod, CryptoState, PacketMod, PacketState, UseTls, TlsState, Limit) ->
|
||||
#codec{have_tls = UseTls,
|
||||
tls_state = TlsState,
|
||||
crypto_mod = CryptoMod,
|
||||
crypto_state = CryptoState,
|
||||
packet_mod = PacketMod,
|
||||
packet_state = PacketState}.
|
||||
packet_state = PacketState,
|
||||
limit = Limit}.
|
||||
|
||||
-spec replace(layer(), module() | boolean(), any(), codec()) -> codec().
|
||||
replace(tls, HaveTls, St, #codec{tls_buf = <<>>} = Codec) ->
|
||||
Codec#codec{have_tls = HaveTls, tls_state = St};
|
||||
replace(crypto, Mod, St, #codec{crypto_buf = <<>>} = Codec) ->
|
||||
Codec#codec{crypto_mod = Mod, crypto_state = St};
|
||||
replace(packet, Mod, St, #codec{packet_buf = <<>>} = Codec) ->
|
||||
Codec#codec{packet_mod = Mod, packet_state = St}.
|
||||
|
||||
|
||||
-spec info(layer(), codec()) -> {module() | boolean(), state()}.
|
||||
info(tls, #codec{have_tls = HaveTls, tls_state = TlsState}) ->
|
||||
{HaveTls, TlsState};
|
||||
info(crypto, #codec{crypto_mod = CryptoMod, crypto_state = CryptoState}) ->
|
||||
{CryptoMod, CryptoState};
|
||||
info(packet, #codec{packet_mod = PacketMod, packet_state = PacketState}) ->
|
||||
{PacketMod, PacketState}.
|
||||
|
||||
|
||||
%% @doc Push already produced data back to one of codec's input buffers
|
||||
-spec push_back(layer() | first, binary(), codec()) -> codec().
|
||||
push_back(tls, Data, #codec{tls_buf = Buf} = Codec) ->
|
||||
assert_overflow(Codec#codec{tls_buf = <<Data/binary, Buf/binary>>});
|
||||
push_back(crypto, Data, #codec{crypto_buf = Buf} = Codec) ->
|
||||
assert_overflow(Codec#codec{crypto_buf = <<Data/binary, Buf/binary>>});
|
||||
push_back(packet, Data, #codec{packet_buf = Buf} = Codec) ->
|
||||
assert_overflow(Codec#codec{packet_buf = <<Data/binary, Buf/binary>>});
|
||||
push_back(first, Data, #codec{have_tls = HaveTls} = Codec) ->
|
||||
Destination =
|
||||
case HaveTls of
|
||||
true -> tls;
|
||||
false -> crypto
|
||||
end,
|
||||
push_back(Destination, Data, Codec).
|
||||
|
||||
-spec decompose(codec()) -> {crypto_codec(), state(), packet_codec(), state()}.
|
||||
decompose(#codec{crypto_mod = CryptoMod, crypto_state = CryptoState,
|
||||
packet_mod = PacketMod, packet_state = PacketState}) ->
|
||||
{CryptoMod, CryptoState, PacketMod, PacketState}.
|
||||
|
||||
|
||||
%% try_decode_packet(Inner) |> try_decode_packet(Outer)
|
||||
-spec try_decode_packet(binary(), codec()) -> {ok, binary(), codec()} | {incomplete, codec()}.
|
||||
try_decode_packet(Bin, S) ->
|
||||
decode_crypto(Bin, S).
|
||||
decode_tls(Bin, S).
|
||||
|
||||
decode_tls(Bin, #codec{have_tls = false} = S) ->
|
||||
decode_crypto(Bin, S);
|
||||
decode_tls(<<>>, #codec{tls_buf = <<>>} = S) ->
|
||||
decode_crypto(<<>>, S);
|
||||
decode_tls(Bin, #codec{tls_state = TlsSt, tls_buf = <<>>} = S) ->
|
||||
case mtp_fake_tls:try_decode_packet(Bin, TlsSt) of
|
||||
{incomplete, TlsSt1} ->
|
||||
%% XXX: actually, TLS doesn't store any mutable state...
|
||||
decode_crypto(<<>>, S#codec{tls_state = TlsSt1});
|
||||
{ok, Dec, Tail, TlsSt1} ->
|
||||
decode_crypto(Dec, assert_overflow(S#codec{tls_state = TlsSt1, tls_buf = Tail}))
|
||||
end;
|
||||
decode_tls(Bin, #codec{tls_buf = Buf} = S) ->
|
||||
decode_tls(<<Buf/binary, Bin/binary>>, S#codec{tls_buf = <<>>}).
|
||||
|
||||
|
||||
decode_crypto(<<>>, #codec{crypto_state = CS, crypto_buf = <<>>} = S) ->
|
||||
%% There is smth in packet buffer
|
||||
%% There might be smth in packet buffer
|
||||
decode_packet(<<>>, CS, <<>>, S);
|
||||
decode_crypto(Bin, #codec{crypto_mod = CryptoMod,
|
||||
crypto_state = CryptoSt,
|
||||
|
|
@ -84,23 +142,25 @@ decode_crypto(Bin, #codec{crypto_buf = Buf} = S) ->
|
|||
|
||||
decode_packet(<<>>, CryptoSt, CryptoTail, #codec{packet_buf = <<>>} = S) ->
|
||||
%% Crypto produced nothing and there is nothing in packet buf
|
||||
{incomplete, S#codec{crypto_state = CryptoSt, crypto_buf = CryptoTail}};
|
||||
{incomplete, assert_overflow(S#codec{crypto_state = CryptoSt, crypto_buf = CryptoTail})};
|
||||
decode_packet(Bin, CryptoSt, CryptoTail, #codec{packet_mod = PacketMod,
|
||||
packet_state = PacketSt,
|
||||
packet_buf = <<>>} = S) ->
|
||||
%% Crypto produced smth, and there is nothing in pkt buf
|
||||
case PacketMod:try_decode_packet(Bin, PacketSt) of
|
||||
{incomplete, PacketSt1} ->
|
||||
{incomplete, S#codec{crypto_state = CryptoSt,
|
||||
{incomplete, assert_overflow(
|
||||
S#codec{crypto_state = CryptoSt,
|
||||
crypto_buf = CryptoTail,
|
||||
packet_state = PacketSt1,
|
||||
packet_buf = Bin
|
||||
})};
|
||||
{ok, Dec2, Tail, PacketSt1} ->
|
||||
{ok, Dec2, assert_overflow(
|
||||
S#codec{crypto_state = CryptoSt,
|
||||
crypto_buf = CryptoTail,
|
||||
packet_state = PacketSt1,
|
||||
packet_buf = Bin
|
||||
}};
|
||||
{ok, Dec2, Tail, PacketSt1} ->
|
||||
{ok, Dec2, S#codec{crypto_state = CryptoSt,
|
||||
crypto_buf = CryptoTail,
|
||||
packet_state = PacketSt1,
|
||||
packet_buf = Tail}}
|
||||
packet_buf = Tail})}
|
||||
end;
|
||||
decode_packet(Bin, CSt, CTail, #codec{packet_buf = Buf} = S) ->
|
||||
decode_packet(<<Buf/binary, Bin/binary>>, CSt, CTail, S#codec{packet_buf = <<>>}).
|
||||
|
|
@ -108,13 +168,21 @@ decode_packet(Bin, CSt, CTail, #codec{packet_buf = Buf} = S) ->
|
|||
|
||||
%% encode_packet(Outer) |> encode_packet(Inner)
|
||||
-spec encode_packet(iodata(), codec()) -> {iodata(), codec()}.
|
||||
encode_packet(Bin, #codec{packet_mod = PacketMod,
|
||||
encode_packet(Bin, #codec{have_tls = HaveTls,
|
||||
tls_state = TlsSt,
|
||||
packet_mod = PacketMod,
|
||||
packet_state = PacketSt,
|
||||
crypto_mod = CryptoMod,
|
||||
crypto_state = CryptoSt} = S) ->
|
||||
{Enc1, PacketSt1} = PacketMod:encode_packet(Bin, PacketSt),
|
||||
{Enc2, CryptoSt1} = CryptoMod:encode_packet(Enc1, CryptoSt),
|
||||
{Enc2, S#codec{crypto_state = CryptoSt1, packet_state = PacketSt1}}.
|
||||
case HaveTls of
|
||||
false ->
|
||||
{Enc2, S#codec{crypto_state = CryptoSt1, packet_state = PacketSt1}};
|
||||
true ->
|
||||
{Enc3, TlsSt1} = mtp_fake_tls:encode_packet(Enc2, TlsSt),
|
||||
{Enc3, S#codec{crypto_state = CryptoSt1, packet_state = PacketSt1, tls_state = TlsSt1}}
|
||||
end.
|
||||
|
||||
|
||||
-spec fold_packets(fun( (binary(), FoldSt, codec()) -> FoldSt ),
|
||||
|
|
@ -132,5 +200,14 @@ fold_packets(Fun, FoldSt, Data, Codec) ->
|
|||
end.
|
||||
|
||||
-spec is_empty(codec()) -> boolean().
|
||||
is_empty(#codec{packet_buf = <<>>, crypto_buf = <<>>}) -> true;
|
||||
is_empty(#codec{packet_buf = <<>>, crypto_buf = <<>>, tls_buf = <<>>}) -> true;
|
||||
is_empty(_) -> false.
|
||||
|
||||
assert_overflow(#codec{packet_buf = PB, crypto_buf = CB, tls_buf = TB, limit = Limit} = Codec) ->
|
||||
Size = byte_size(PB) + byte_size(CB) + byte_size(TB),
|
||||
case Size > Limit of
|
||||
true ->
|
||||
error({protocol_error, max_buffers_size, Size});
|
||||
false ->
|
||||
Codec
|
||||
end.
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
-define(CONN_TIMEOUT, 10000).
|
||||
-define(SEND_TIMEOUT, 15000).
|
||||
-define(MAX_SOCK_BUF_SIZE, 1024 * 500). % Decrease if CPU is cheaper than RAM
|
||||
-define(MAX_CODEC_BUFFERS, 5 * 1024 * 1024).
|
||||
|
||||
-ifndef(OTP_RELEASE). % pre-OTP21
|
||||
-define(WITH_STACKTRACE(T, R, S), T:R -> S = erlang:get_stacktrace(), ).
|
||||
|
|
@ -484,7 +485,8 @@ down_handshake1(S) ->
|
|||
S1 = S#state{stage = handshake_1,
|
||||
%% Use fake encryption codec
|
||||
codec = mtp_codec:new(mtp_noop_codec, mtp_noop_codec:new(),
|
||||
mtp_full, mtp_full:new(-2, -2)),
|
||||
mtp_full, mtp_full:new(-2, -2),
|
||||
false, undefined, ?MAX_CODEC_BUFFERS),
|
||||
stage_state = {KeySelector, Nonce, CryptoTs, Key}},
|
||||
down_send(Msg, S1).
|
||||
|
||||
|
|
@ -503,10 +505,8 @@ down_handshake2(Pkt, #state{stage_state = {MyKeySelector, CliNonce, MyTs, Key},
|
|||
clt_ip => MyIpBin, clt_port => MyPort, secret => Key},
|
||||
{EncKey, EncIv} = get_middle_key(Args#{purpose => <<"CLIENT">>}),
|
||||
{DecKey, DecIv} = get_middle_key(Args#{purpose => <<"SERVER">>}),
|
||||
{_, _, PacketMod, PacketState} = mtp_codec:decompose(Codec1),
|
||||
CryptoState = mtp_aes_cbc:new(EncKey, EncIv, DecKey, DecIv, 16),
|
||||
Codec = mtp_codec:new(mtp_aes_cbc, CryptoState,
|
||||
PacketMod, PacketState),
|
||||
Codec = mtp_codec:replace(crypto, mtp_aes_cbc, CryptoState, Codec1),
|
||||
SenderPID = PeerPID = <<"IPIPPRPDTIME">>,
|
||||
Handshake = mtp_rpc:encode_handshake({handshake, SenderPID, PeerPID}),
|
||||
down_send(Handshake,
|
||||
|
|
|
|||
175
src/mtp_fake_tls.erl
Normal file
175
src/mtp_fake_tls.erl
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
%%% @author sergey <me@seriyps.ru>
|
||||
%%% @copyright (C) 2019, sergey
|
||||
%%% @doc
|
||||
%%% Fake TLS 'CBC' stream codec
|
||||
%%% https://github.com/telegramdesktop/tdesktop/commit/69b6b487382c12efc43d52f472cab5954ab850e2
|
||||
%%% It's not real TLS, but it looks like TLS1.3 from outside
|
||||
%%% @end
|
||||
%%% Created : 24 Jul 2019 by sergey <me@seriyps.ru>
|
||||
|
||||
-module(mtp_fake_tls).
|
||||
|
||||
-behaviour(mtp_codec).
|
||||
|
||||
-export([from_client_hello/2,
|
||||
new/0,
|
||||
try_decode_packet/2,
|
||||
encode_packet/2]).
|
||||
-export_type([codec/0]).
|
||||
|
||||
-include_lib("hut/include/hut.hrl").
|
||||
|
||||
-dialyzer(no_improper_lists).
|
||||
|
||||
-record(st, {}).
|
||||
|
||||
-record(client_hello,
|
||||
{pseudorandom :: binary(),
|
||||
session_id :: binary(),
|
||||
cipher_suites :: list(),
|
||||
compression_methods :: list(),
|
||||
extensions :: [{non_neg_integer(), any()}]
|
||||
}).
|
||||
|
||||
-define(MAX_PACKET_SIZE, 65535). % sizeof(uint16) - 1
|
||||
|
||||
-define(TLS_10_VERSION, 3, 1).
|
||||
-define(TLS_12_VERSION, 3, 3).
|
||||
-define(TLS_REC_CHANGE_CIPHER, 20).
|
||||
-define(TLS_REC_HANDSHAKE, 22).
|
||||
-define(TLS_REC_DATA, 23).
|
||||
|
||||
-define(TLS_12_DATA, ?TLS_REC_DATA, ?TLS_12_VERSION).
|
||||
|
||||
-define(DIGEST_POS, 11).
|
||||
-define(DIGEST_LEN, 32).
|
||||
|
||||
-define(TLS_TAG_CLI_HELLO, 1).
|
||||
-define(TLS_TAG_SRV_HELLO, 2).
|
||||
-define(TLS_CIPHERSUITE, 192, 47).
|
||||
-define(TLS_EXTENSIONS,
|
||||
0, 18, % Extensions length
|
||||
255, 1, 0, 1, 0, % renegotiation_info
|
||||
0, 5, 0, 0, % status_request
|
||||
0, 16, 0, 5, 0, 3, 2, 104, 50 % ALPN
|
||||
).
|
||||
-define(TLS_CHANGE_CIPHER, ?TLS_REC_CHANGE_CIPHER, ?TLS_12_VERSION, 0, 1, 1).
|
||||
|
||||
-define(EXT_SNI, 0).
|
||||
|
||||
-define(APP, mtproto_proxy).
|
||||
|
||||
-opaque codec() :: #st{}.
|
||||
|
||||
-spec from_client_hello(binary(), binary()) ->
|
||||
{ok, iodata(), binary(), non_neg_integer(), codec()}.
|
||||
from_client_hello(Data, Secret) ->
|
||||
#client_hello{pseudorandom = ClientDigest,
|
||||
session_id = SessionId} = CliHlo = parse_client_hello(Data),
|
||||
?log(debug, "TLS ClientHello=~p", [CliHlo]),
|
||||
ServerDigest = make_server_digest(Data, Secret),
|
||||
<<Zeroes:(?DIGEST_LEN - 4)/binary, _/binary>> = XoredDigest =
|
||||
crypto:exor(ClientDigest, ServerDigest),
|
||||
lists:all(fun(B) -> B == 0 end, binary_to_list(Zeroes)) orelse
|
||||
error({protocol_error, invalid_tls_digest, XoredDigest}),
|
||||
<<_:(?DIGEST_LEN - 4)/binary, Timestamp:32/unsigned-little>> = XoredDigest,
|
||||
FakeHttpData = crypto:strong_rand_bytes(rand:uniform(256)),
|
||||
SrvHello0 = make_srv_hello(binary:copy(<<0>>, ?DIGEST_LEN), SessionId),
|
||||
Response0 = [_, CC, DD] =
|
||||
[as_tls_frame(?TLS_REC_HANDSHAKE, SrvHello0),
|
||||
as_tls_frame(?TLS_REC_CHANGE_CIPHER, [1]),
|
||||
as_tls_frame(?TLS_REC_DATA, FakeHttpData)],
|
||||
SrvHelloDigest = crypto:hmac(sha256, Secret, [ClientDigest | Response0]),
|
||||
SrvHello = make_srv_hello(SrvHelloDigest, SessionId),
|
||||
Response = [as_tls_frame(?TLS_REC_HANDSHAKE, SrvHello),
|
||||
CC,
|
||||
DD],
|
||||
{ok, Response, SessionId, Timestamp, new()}.
|
||||
|
||||
|
||||
parse_client_hello(<<?TLS_REC_HANDSHAKE, ?TLS_10_VERSION, 512:16/unsigned-big, %Frame
|
||||
?TLS_TAG_CLI_HELLO, 508:24/unsigned-big, ?TLS_12_VERSION,
|
||||
Random:?DIGEST_LEN/binary,
|
||||
SessIdLen, SessId:SessIdLen/binary,
|
||||
CipherSuitesLen:16/unsigned-big, CipherSuites:CipherSuitesLen/binary,
|
||||
CompMethodsLen, CompMethods:CompMethodsLen/binary,
|
||||
ExtensionsLen:16/unsigned-big, Extensions:ExtensionsLen/binary>>
|
||||
%% _/binary>>
|
||||
) ->
|
||||
#client_hello{
|
||||
pseudorandom = Random,
|
||||
session_id = SessId,
|
||||
cipher_suites = parse_suites(CipherSuites),
|
||||
compression_methods = parse_compression(CompMethods),
|
||||
extensions = parse_extensions(Extensions)
|
||||
}.
|
||||
|
||||
parse_suites(Bin) ->
|
||||
[Suite || <<Suite:16/unsigned-big>> <= Bin].
|
||||
|
||||
parse_compression(Bin) ->
|
||||
[Bin]. %TODO: just binary_to_list(Bin)
|
||||
|
||||
parse_extensions(Exts) ->
|
||||
[{Type, parse_extension(Type, Data)}
|
||||
|| <<Type:16/unsigned-big, Length:16/unsigned-big, Data:Length/binary>> <= Exts].
|
||||
|
||||
parse_extension(?EXT_SNI, <<ListLen:16/unsigned-big, List:ListLen/binary>>) ->
|
||||
SNIList = [{Type, Value}
|
||||
|| <<Type, Len:16/unsigned-big, Value:Len/binary>> <= List],
|
||||
SNIList;
|
||||
parse_extension(_Type, Data) ->
|
||||
Data.
|
||||
|
||||
|
||||
make_server_digest(<<Left:?DIGEST_POS/binary, _:?DIGEST_LEN/binary, Right/binary>>, Secret) ->
|
||||
Msg = [Left, binary:copy(<<0>>, ?DIGEST_LEN), Right],
|
||||
crypto:hmac(sha256, Secret, Msg).
|
||||
|
||||
make_srv_hello(Digest, SessionId) ->
|
||||
SessionSize = byte_size(SessionId),
|
||||
Payload = <<?TLS_12_VERSION,
|
||||
Digest:?DIGEST_LEN/binary,
|
||||
SessionSize,
|
||||
SessionId:SessionSize/binary,
|
||||
?TLS_CIPHERSUITE,
|
||||
0,
|
||||
?TLS_EXTENSIONS>>,
|
||||
[<<?TLS_TAG_SRV_HELLO, (byte_size(Payload)):24/unsigned-big>> | Payload].
|
||||
|
||||
|
||||
-spec new() -> codec().
|
||||
new() ->
|
||||
#st{}.
|
||||
|
||||
-spec try_decode_packet(binary(), codec()) -> {ok, binary(), binary(), codec()}
|
||||
| {incomplete, codec()}.
|
||||
try_decode_packet(<<?TLS_REC_DATA, ?TLS_12_VERSION, Size:16/unsigned-big,
|
||||
Data:Size/binary, Tail/binary>>, St) ->
|
||||
{ok, Data, Tail, St};
|
||||
try_decode_packet(<<?TLS_REC_CHANGE_CIPHER, ?TLS_12_VERSION, Size:16/unsigned-big,
|
||||
_Data:Size/binary, Tail/binary>>, St) ->
|
||||
%% "Change cipher" are ignored
|
||||
try_decode_packet(Tail, St);
|
||||
try_decode_packet(Bin, St) when byte_size(Bin) =< ?MAX_PACKET_SIZE ->
|
||||
{incomplete, St};
|
||||
try_decode_packet(Bin, _St) ->
|
||||
error({protocol_error, tls_max_size, byte_size(Bin)}).
|
||||
|
||||
|
||||
-spec encode_packet(binary(), codec()) -> {iodata(), codec()}.
|
||||
encode_packet(Bin, St) ->
|
||||
{encode_as_frames(Bin), St}.
|
||||
|
||||
encode_as_frames(Bin) when byte_size(Bin) =< ?MAX_PACKET_SIZE ->
|
||||
as_tls_data_frame(Bin);
|
||||
encode_as_frames(<<Chunk:?MAX_PACKET_SIZE/binary, Tail/binary>>) ->
|
||||
[as_tls_data_frame(Chunk) | encode_as_frames(Tail)].
|
||||
|
||||
as_tls_data_frame(Bin) ->
|
||||
as_tls_frame(?TLS_REC_DATA, Bin).
|
||||
|
||||
-spec as_tls_frame(byte(), iodata()) -> iodata().
|
||||
as_tls_frame(Type, Data) ->
|
||||
Size = iolist_size(Data),
|
||||
[<<Type, ?TLS_12_VERSION, Size:16/unsigned-big>> | Data].
|
||||
|
|
@ -30,14 +30,14 @@
|
|||
-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(APP, mtproto_proxy).
|
||||
|
||||
-record(state,
|
||||
{stage = init :: stage(),
|
||||
stage_state = <<>> :: any(),
|
||||
acc = <<>> :: any(),
|
||||
|
||||
secret :: binary(),
|
||||
listener :: atom(),
|
||||
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
srv_error_filter :: first | on | off}).
|
||||
|
||||
-type transport() :: module().
|
||||
-type stage() :: init | tunnel.
|
||||
-type stage() :: init | tls_hello | tunnel.
|
||||
|
||||
|
||||
%% APIs
|
||||
|
|
@ -104,10 +104,14 @@ init({Socket, Transport, [Name, Secret, Tag]}) ->
|
|||
#{timeout => {env, ?APP, TimeoutKey, TimeoutDefault}}),
|
||||
Filter = application:get_env(?APP, replay_check_server_error_filter, off),
|
||||
NowMs = erlang:system_time(millisecond),
|
||||
NoopSt = mtp_noop_codec:new(),
|
||||
Codec = mtp_codec:new(mtp_noop_codec, NoopSt,
|
||||
mtp_noop_codec, NoopSt),
|
||||
State = #state{sock = Socket,
|
||||
secret = unhex(Secret),
|
||||
listener = Name,
|
||||
transport = Transport,
|
||||
codec = Codec,
|
||||
ad_tag = unhex(Tag),
|
||||
addr = {Ip, Port},
|
||||
started_at = NowMs,
|
||||
|
|
@ -260,10 +264,12 @@ state_timeout(stop) ->
|
|||
|
||||
%% Handle telegram client -> proxy stream
|
||||
handle_upstream_data(Bin, #state{stage = tunnel,
|
||||
codec = UpCodec} = S) ->
|
||||
codec = UpCodec} = S) ->
|
||||
?log(debug, "tunneling ~p; codec=~p", [Bin, UpCodec]),
|
||||
{ok, S3, UpCodec1} =
|
||||
mtp_codec:fold_packets(
|
||||
fun(Decoded, S1, Codec1) ->
|
||||
?log(debug, "raw tunneled packet ~p", [Decoded]),
|
||||
mtp_metric:histogram_observe(
|
||||
[?APP, tg_packet_size, bytes],
|
||||
byte_size(Decoded),
|
||||
|
|
@ -272,29 +278,85 @@ handle_upstream_data(Bin, #state{stage = tunnel,
|
|||
{S2, S2#state.codec}
|
||||
end, S, Bin, UpCodec),
|
||||
{ok, S3#state{codec = UpCodec1}};
|
||||
handle_upstream_data(<<Header:64/binary, Rest/binary>>, #state{stage = init, stage_state = <<>>,
|
||||
secret = Secret, listener = Listener} = S) ->
|
||||
handle_upstream_data(Bin, #state{codec = Codec0} = S0) ->
|
||||
?log(debug, "Codec0: ~p", [Codec0]),
|
||||
{ok, S, Codec} =
|
||||
mtp_codec:fold_packets(
|
||||
fun(Decoded, S1, Codec1) ->
|
||||
?log(debug, "Codec1: ~p, unfolded: ~p", [Codec1, Decoded]),
|
||||
case parse_upstream_data(Decoded, S1#state{codec = Codec1}) of
|
||||
{ok, S2} ->
|
||||
?log(debug, "Codec2: ~p", [S2#state.codec]),
|
||||
{S2, S2#state.codec};
|
||||
{error, Err} ->
|
||||
error(Err)
|
||||
end
|
||||
end, S0, Bin, Codec0),
|
||||
?log(debug, "Codec: ~p", [Codec]),
|
||||
case mtp_codec:is_empty(Codec) of
|
||||
true ->
|
||||
{ok, S#state{codec = Codec}};
|
||||
false ->
|
||||
handle_upstream_data(<<>>, S#state{codec = Codec})
|
||||
end.
|
||||
|
||||
|
||||
parse_upstream_data(<<?TLS_START, _/binary>> = AllData,
|
||||
#state{stage = tls_hello, secret = Secret, codec = Codec0} = S) when
|
||||
byte_size(AllData) >= (?TLS_CLIENT_HELLO_LEN + 5) ->
|
||||
{ok, AllowedProtocols} = application:get_env(?APP, allowed_protocols),
|
||||
lists:member(mtp_fake_tls, AllowedProtocols) orelse
|
||||
error({protocol_error, disabled_protocol, mtp_fake_tls}),
|
||||
<<Data:(?TLS_CLIENT_HELLO_LEN + 5)/binary, Tail/binary>> = AllData,
|
||||
case mtp_fake_tls:from_client_hello(Data, Secret) of
|
||||
{ok, Response, SessionId, Timestamp, TlsCodec} ->
|
||||
maybe_check_tls_replay(SessionId, Timestamp),
|
||||
Codec1 = mtp_codec:replace(tls, true, TlsCodec, Codec0),
|
||||
Codec = mtp_codec:push_back(tls, Tail, Codec1),
|
||||
ok = up_send_raw(Response, S),
|
||||
{ok, S#state{codec = Codec, stage = init}};
|
||||
{error, _} = Err ->
|
||||
Err
|
||||
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>>,
|
||||
#state{stage = init, secret = Secret, listener = Listener, codec = Codec0,
|
||||
ad_tag = Tag, addr = Addr} = S) ->
|
||||
case mtp_obfuscated:from_header(Header, Secret) of
|
||||
{ok, DcId, PacketLayerMod, CryptoCodecSt} ->
|
||||
maybe_check_replay(Header),
|
||||
ProtoToReport = case mtp_codec:info(tls, Codec0) of
|
||||
{true, _} when PacketLayerMod == mtp_secure ->
|
||||
mtp_secure_fake_tls;
|
||||
{false, _} ->
|
||||
PacketLayerMod
|
||||
end,
|
||||
mtp_metric:count_inc([?APP, protocol_ok, total],
|
||||
1, #{labels => [Listener, PacketLayerMod]}),
|
||||
1, #{labels => [Listener, ProtoToReport]}),
|
||||
Codec1 = mtp_codec:replace(crypto, mtp_obfuscated, CryptoCodecSt, Codec0),
|
||||
PacketCodec = PacketLayerMod:new(),
|
||||
Codec = mtp_codec:new(mtp_obfuscated, CryptoCodecSt,
|
||||
PacketLayerMod, PacketCodec),
|
||||
handle_upstream_header(
|
||||
DcId,
|
||||
S#state{codec = Codec,
|
||||
acc = Rest,
|
||||
stage_state = undefined});
|
||||
Codec2 = mtp_codec:replace(packet, PacketLayerMod, PacketCodec, Codec1),
|
||||
Codec = mtp_codec:push_back(crypto, Rest, Codec2),
|
||||
?log(debug, "Hdr=~p, codec=~p", [Header, Codec]),
|
||||
Opts = #{ad_tag => Tag,
|
||||
addr => Addr},
|
||||
{RealDcId, Pool, Downstream} = mtp_config:get_downstream_safe(DcId, Opts),
|
||||
handle_upstream_data(
|
||||
<<>>,
|
||||
switch_timer(
|
||||
S#state{down = Downstream,
|
||||
dc_id = {RealDcId, Pool},
|
||||
codec = Codec,
|
||||
stage = tunnel},
|
||||
hibernate));
|
||||
{error, Reason} = Err ->
|
||||
mtp_metric:count_inc([?APP, protocol_error, total], 1, #{labels => [Reason]}),
|
||||
Err
|
||||
end;
|
||||
handle_upstream_data(Bin, #state{stage = init, stage_state = <<>>} = S) ->
|
||||
{ok, S#state{stage_state = Bin}};
|
||||
handle_upstream_data(Bin, #state{stage = init, stage_state = Buf} = S) ->
|
||||
handle_upstream_data(<<Buf/binary, Bin/binary>> , S#state{stage_state = <<>>}).
|
||||
parse_upstream_data(Bin, #state{stage = Stage, codec = Codec0} = S) when Stage =/= tunnel ->
|
||||
Codec = mtp_codec:push_back(first, Bin, Codec0),
|
||||
{ok, S#state{codec = Codec}}.
|
||||
|
||||
maybe_check_replay(Packet) ->
|
||||
%% Check for session replay attack: attempt to connect with the same 1st 64byte packet
|
||||
|
|
@ -306,21 +368,26 @@ maybe_check_replay(Packet) ->
|
|||
ok
|
||||
end.
|
||||
|
||||
maybe_check_tls_replay(_SessionId, _Timestamp) ->
|
||||
%% TODO
|
||||
ok.
|
||||
|
||||
up_send(Packet, #state{stage = tunnel,
|
||||
codec = UpCodec,
|
||||
sock = Sock,
|
||||
transport = Transport,
|
||||
listener = Listener} = S) ->
|
||||
up_send(Packet, #state{stage = tunnel, codec = UpCodec} = S) ->
|
||||
%% ?log(debug, ">Up: ~p", [Packet]),
|
||||
{Encoded, UpCodec1} = mtp_codec:encode_packet(Packet, UpCodec),
|
||||
ok = up_send_raw(Encoded, S),
|
||||
{ok, S#state{codec = UpCodec1}}.
|
||||
|
||||
up_send_raw(Data, #state{sock = Sock,
|
||||
transport = Transport,
|
||||
listener = Listener} = S) ->
|
||||
mtp_metric:rt([?APP, upstream_send_duration, seconds],
|
||||
fun() ->
|
||||
case Transport:send(Sock, Encoded) of
|
||||
case Transport:send(Sock, Data) of
|
||||
ok ->
|
||||
mtp_metric:count_inc(
|
||||
[?APP, sent, upstream, bytes],
|
||||
iolist_size(Encoded), #{labels => [Listener]}),
|
||||
iolist_size(Data), #{labels => [Listener]}),
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
is_atom(Reason) andalso
|
||||
|
|
@ -330,8 +397,7 @@ up_send(Packet, #state{stage = tunnel,
|
|||
?log(warning, "Upstream send error: ~p", [Reason]),
|
||||
throw({stop, normal, S})
|
||||
end
|
||||
end, #{labels => [Listener]}),
|
||||
{ok, S#state{codec = UpCodec1}}.
|
||||
end, #{labels => [Listener]}).
|
||||
|
||||
down_send(Packet, #state{down = Down} = S) ->
|
||||
%% ?log(debug, ">Down: ~p", [Packet]),
|
||||
|
|
@ -358,20 +424,6 @@ handle_unknown_upstream(#state{down = Down, sock = USock, transport = UTrans} =
|
|||
%% Internal
|
||||
|
||||
|
||||
handle_upstream_header(DcId, #state{acc = Acc, ad_tag = Tag, addr = Addr} = S) ->
|
||||
Opts = #{ad_tag => Tag,
|
||||
addr => Addr},
|
||||
{RealDcId, Pool, Downstream} = mtp_config:get_downstream_safe(DcId, Opts),
|
||||
handle_upstream_data(
|
||||
Acc,
|
||||
switch_timer(
|
||||
S#state{down = Downstream,
|
||||
dc_id = {RealDcId, Pool},
|
||||
acc = <<>>,
|
||||
stage = tunnel},
|
||||
hibernate)).
|
||||
|
||||
|
||||
%% @doc Terminate if message queue is too big
|
||||
maybe_check_health(#state{last_queue_check = LastCheck} = S) ->
|
||||
NowMs = erlang:system_time(millisecond),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ new() ->
|
|||
?MODULE.
|
||||
|
||||
-spec try_decode_packet(binary(), codec()) -> {ok, binary(), binary(), codec()}.
|
||||
try_decode_packet(<<>>, ?MODULE) ->
|
||||
{incomplete, ?MODULE};
|
||||
try_decode_packet(Data, ?MODULE) ->
|
||||
{ok, Data, <<>>, ?MODULE}.
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ decode_packet(<<?RPC_SIMPLE_ACK, ConnId:64/signed-little, Confirm:4/binary>>) ->
|
|||
encode_packet({data, Msg}, {{ConnId, ClientAddr, ProxyTag}, ProxyAddr}) ->
|
||||
%% See mtproto/mtproto-proxy.c:forward_mtproto_packet
|
||||
((iolist_size(Msg) rem 4) == 0)
|
||||
orelse error(not_aligned),
|
||||
orelse error({not_aligned, Msg}),
|
||||
Flags1 = (?FLAG_HAS_AD_TAG
|
||||
bor ?FLAG_MAGIC
|
||||
bor ?FLAG_EXTMODE2
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
%% only `{allowed_protocols, [mtp_secure]}` if you want to only allow
|
||||
%% connections to this proxy with "dd"-secrets. Connections by other
|
||||
%% protocols will be immediately closed.
|
||||
{allowed_protocols, [mtp_abridged, mtp_intermediate, mtp_secure]},
|
||||
{allowed_protocols, [mtp_abridged, mtp_intermediate, mtp_secure, mtp_fake_tls]},
|
||||
|
||||
{init_dc_connections, 2},
|
||||
{clients_per_dc_connection, 300},
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
packet_4b/0,
|
||||
stream_16b/0,
|
||||
packet_16b/0,
|
||||
binary/2,
|
||||
key/0,
|
||||
iv/0,
|
||||
secret/0,
|
||||
|
|
@ -33,6 +34,14 @@ packet_16b() ->
|
|||
stream_16b() ->
|
||||
proper_types:list(packet_16b()).
|
||||
|
||||
%% Binary of size between Min and Max
|
||||
binary(Min, Max) when Min < Max ->
|
||||
TailSize = Max - Min,
|
||||
?LET({First, Tail}, {proper_types:binary(Min),
|
||||
proper_types:resize(TailSize,
|
||||
proper_types:list(proper_types:byte()))},
|
||||
iolist_to_binary([First | Tail])).
|
||||
|
||||
%% 32-byte encryption key: `binary()`
|
||||
key() ->
|
||||
proper_types:binary(32).
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ connect(Host, Port, Seed, Secret, DcId, Protocol) ->
|
|||
ok = gen_tcp:send(Sock, Header),
|
||||
PacketLayer = Protocol:new(),
|
||||
Codec = mtp_codec:new(mtp_obfuscated, CryptoLayer,
|
||||
Protocol, PacketLayer),
|
||||
Protocol, PacketLayer, false, undefined, 25 * 1024 * 1024),
|
||||
#client{sock = Sock,
|
||||
codec = Codec}.
|
||||
|
||||
|
|
|
|||
|
|
@ -118,10 +118,8 @@ wait_nonce(info, {tcp, _Sock, TcpData},
|
|||
{DecKey, DecIv} = mtp_down_conn:get_middle_key(Args#{purpose => <<"CLIENT">>}),
|
||||
{EncKey, EncIv} = mtp_down_conn:get_middle_key(Args#{purpose => <<"SERVER">>}),
|
||||
%% Add encryption layer to codec
|
||||
{_, _, PacketMod, PacketState} = mtp_codec:decompose(Codec2),
|
||||
CryptoState = mtp_aes_cbc:new(EncKey, EncIv, DecKey, DecIv, 16),
|
||||
Codec3 = mtp_codec:new(mtp_aes_cbc, CryptoState,
|
||||
PacketMod, PacketState),
|
||||
Codec3 = mtp_codec:replace(crypto, mtp_aes_cbc, CryptoState, Codec2),
|
||||
|
||||
{next_state, wait_handshake,
|
||||
activate(S1#hs_state{codec = Codec3,
|
||||
|
|
|
|||
62
test/prop_mtp_fake_tls.erl
Normal file
62
test/prop_mtp_fake_tls.erl
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
%% @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_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, 65535), codec_small(Bin)).
|
||||
|
||||
codec_small(Bin) ->
|
||||
%% fake_tls can split big packets to multiple TLS frames of 64kb
|
||||
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(65536, 75000), 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 65535b can be encoded and decoded back".
|
||||
|
||||
prop_stream() ->
|
||||
?FORALL(Stream, proper_types:list(mtp_prop_gen:binary(8, 20000)),
|
||||
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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue