Fake TLS protocol

* pretends being TLS1.3 + http2
* mtp_handler state-machine refactored
parent c711054b
......@@ -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, S#codec{crypto_state = CryptoSt,
{ok, Dec2, assert_overflow(
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,
......
%%% @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,
......@@ -261,9 +265,11 @@ state_timeout(stop) ->
%% Handle telegram client -> proxy stream
handle_upstream_data(Bin, #state{stage = tunnel,
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,
......
%% @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.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment