diff --git a/README.md b/README.md index 483846f..618e392 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@ Features * Promoted channels! See `mtproto_proxy_app.src` `tag` option. * "secure" randomized-packet-size protocol (34-symbol secrets starting with 'dd') to prevent detection by DPI -* Secure-only mode (only allow connections with 'dd'-secrets). See `allowed_protocols` option. +* Fake-TLS protocol (base64 secrets) - another protocol to prevent DPI detection +* Secure-only mode (only allow connections with 'dd' or fake-tls-base64-secrets). + See `allowed_protocols` option. * Multiple ports with unique secret and promo tag for each port * Very high performance - can handle tens of thousands connections! Scales to all CPU cores. 1Gbps, 90k connections on 4-core/8Gb RAM cloud server. @@ -47,7 +49,10 @@ Where * `-p 443` / `MTP_PORT=…` proxy port * `-s d0d6e111bada5511fcce9584deadbeef` / `MTP_SECRET=…` proxy secret (don't append `dd`! it should be 32 chars long!) * `-t dcbe8f1493fa4cd9ab300891c0b5b326` / `MTP_TAG=…` ad-tag that you get from [@MTProxybot](https://t.me/MTProxybot) -* `-d` / `MTP_DD_ONLY=t` only allow "secure" connections (dd-secrets) +* `-a dd` / `MTP_DD_ONLY=t` only allow "secure" connections (dd-secrets) +* `-a tls` / `MTP_TLS_ONLY=t` only allow "fake-TLS" connections (base64 secrets) + +It's ok to provide both `-a dd -a tls` to allow both protocols. If no `-a` option provided, all protocols will be allowed. ### To run with custom config-file @@ -234,6 +239,8 @@ Each section should have unique `name`! ### Only allow connections with 'dd'-secrets +This protocol uses randomized packet sizes, so it's more difficult to detect on DPI by +packet sizes. It might be useful in Iran, where proxies are detected by DPI. You should disable all protocols other than `mtp_secure` by providing `allowed_protocols` option: @@ -246,6 +253,22 @@ You should disable all protocols other than `mtp_secure` by providing `allowed_p <..> ``` +### Only allow fake-TLS connections with base64-secrets + +Another censorship circumvention technique. MTPRoto proxy protocol pretends to be +HTTPS web traffic (technically speaking, TLSv1.3 + HTTP/2). +It's possible to only allow connections with this protocol by changing `allowed_protocols` to +be list with only `mtp_fake_tls`: + +```erlang + {mtproto_proxy, + [ + {allowed_protocols, [mtp_fake_tls]}, + {ports, + [#{name => mtp_handler_1, + <..> +``` + ### IPv6 Currently proxy only supports client connections via IPv6, but can only connect to Telegram servers diff --git a/src/mtp_aes_cbc.erl b/src/mtp_aes_cbc.erl index 3abcb66..cd79364 100644 --- a/src/mtp_aes_cbc.erl +++ b/src/mtp_aes_cbc.erl @@ -67,7 +67,6 @@ do_decrypt(Data, Tail, #baes_st{decrypt = {DecKey, DecIv}} = S) -> NewDecIv = crypto:next_iv(aes_cbc, Data), {Decrypted, Tail, S#baes_st{decrypt = {DecKey, NewDecIv}}}. -%% To comply mtp_layer interface try_decode_packet(Bin, S) -> case decrypt(Bin, S) of {<<>>, _Tail, S1} -> diff --git a/src/mtp_handler.erl b/src/mtp_handler.erl index 792e2aa..574ee43 100644 --- a/src/mtp_handler.erl +++ b/src/mtp_handler.erl @@ -180,10 +180,7 @@ handle_info({tcp, Sock, Data}, #state{sock = Sock, transport = Transport, {ok, S1} -> ok = Transport:setopts(Sock, [{active, once}]), %% Consider checking health here as well - {noreply, bump_timer(S1)}; - {error, Reason} -> - ?log(info, "handle_data error ~p", [Reason]), - {stop, normal, S} + {noreply, bump_timer(S1)} catch error:{protocol_error, Type, Extra} -> mtp_metric:count_inc([?APP, protocol_error, total], 1, #{labels => [Type]}), ?log(warning, "~s: protocol_error ~p ~p", [inet:ntoa(Ip), Type, Extra]), @@ -265,11 +262,9 @@ 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), @@ -279,20 +274,16 @@ handle_upstream_data(Bin, #state{stage = tunnel, end, S, Bin, UpCodec), {ok, S3#state{codec = UpCodec1}}; 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}}; @@ -304,20 +295,14 @@ handle_upstream_data(Bin, #state{codec = Codec0} = S0) -> parse_upstream_data(<> = 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}), + assert_protocol(mtp_fake_tls), <> = 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; + {ok, Response, SessionId, Timestamp, TlsCodec} = mtp_fake_tls:from_client_hello(Data, Secret), + 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}}; parse_upstream_data(<> = Data, #state{stage = init} = S) -> parse_upstream_data(Data, S#state{stage = tls_hello}); parse_upstream_data(<>, @@ -326,19 +311,20 @@ parse_upstream_data(<>, 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, + ProtoToReport = + case mtp_codec:info(tls, Codec0) of + {true, _} when PacketLayerMod == mtp_secure -> + mtp_secure_fake_tls; + {false, _} -> + assert_protocol(PacketLayerMod), + PacketLayerMod + end, mtp_metric:count_inc([?APP, protocol_ok, total], 1, #{labels => [Listener, ProtoToReport]}), Codec1 = mtp_codec:replace(crypto, mtp_obfuscated, CryptoCodecSt, Codec0), PacketCodec = PacketLayerMod:new(), 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), @@ -358,6 +344,11 @@ parse_upstream_data(Bin, #state{stage = Stage, codec = Codec0} = S) when Stage = Codec = mtp_codec:push_back(first, Bin, Codec0), {ok, S#state{codec = Codec}}. +assert_protocol(Protocol) -> + {ok, AllowedProtocols} = application:get_env(?APP, allowed_protocols), + lists:member(Protocol, AllowedProtocols) + orelse error({protocol_error, disabled_protocol, Protocol}). + maybe_check_replay(Packet) -> %% Check for session replay attack: attempt to connect with the same 1st 64byte packet case application:get_env(?APP, replay_check_session_storage, off) of diff --git a/src/mtp_obfuscated.erl b/src/mtp_obfuscated.erl index 047d7c1..6321838 100644 --- a/src/mtp_obfuscated.erl +++ b/src/mtp_obfuscated.erl @@ -10,7 +10,6 @@ -export([client_create/3, client_create/4, from_header/2, - from_header/3, new/4, encrypt/2, decrypt/2, @@ -38,7 +37,7 @@ client_create(Secret, Protocol, DcId) -> client_create(crypto:strong_rand_bytes(58), Secret, Protocol, DcId). --spec client_create(binary(), binary(), mtp_layer:codec(), integer()) -> +-spec client_create(binary(), binary(), mtp_codec:packet_codec(), integer()) -> {Packet, {EncKey, EncIv}, {DecKey, DecIv}, @@ -93,13 +92,9 @@ encode_dc_id(DcId) -> <>. %% @doc creates new obfuscated stream (MTProto proxy format) -from_header(Header, Secret) -> - {ok, AllowedProtocols} = application:get_env(?APP, allowed_protocols), - from_header(Header, Secret, AllowedProtocols). - --spec from_header(binary(), binary()) -> {ok, integer(), mtp_layer:codec(), codec()} - | {error, unknown_protocol | disabled_protocol}. -from_header(Header, Secret, AllowedProtocols) when byte_size(Header) == 64 -> +-spec from_header(binary(), binary()) -> {ok, integer(), mtp_codec:packet_codec(), codec()} + | {error, unknown_protocol}. +from_header(Header, Secret) when byte_size(Header) == 64 -> %% 1) Encryption key %% [--- _: 8b ----|---------- b: 48b -------------|-- _: 8b --] = header: 64b %% b_r: 48b = reverse([---------- b ------------------]) @@ -124,13 +119,8 @@ from_header(Header, Secret, AllowedProtocols) when byte_size(Header) == 64 -> {error, unknown_protocol} = Err -> Err; Protocol -> - case lists:member(Protocol, AllowedProtocols) of - true -> - DcId = get_dc(Bin1), - {ok, DcId, Protocol, St1}; - false -> - {error, disabled_protocol} - end + DcId = get_dc(Bin1), + {ok, DcId, Protocol, St1} end. init_up_encrypt(Bin, Secret) -> @@ -173,7 +163,6 @@ decrypt(Encrypted, #st{decrypt = Dec} = St) -> {Dec1, Data} = crypto:stream_encrypt(Dec, Encrypted), {Data, <<>>, St#st{decrypt = Dec1}}. -%% To comply with mtp_layer interface -spec try_decode_packet(iodata(), codec()) -> {ok, Decoded :: binary(), Tail :: binary(), codec()} | {incomplete, codec()}. try_decode_packet(Encrypted, St) -> @@ -198,7 +187,7 @@ client_server_test() -> DcId = 4, Protocol = mtp_secure, {Packet, _, _, _CliCodec} = client_create(Secret, Protocol, DcId), - Srv = from_header(Packet, Secret, [Protocol]), + Srv = from_header(Packet, Secret), ?assertMatch({ok, DcId, Protocol, _}, Srv). -endif. diff --git a/src/mtproto_proxy.app.src b/src/mtproto_proxy.app.src index 30a7339..d4a93e1 100644 --- a/src/mtproto_proxy.app.src +++ b/src/mtproto_proxy.app.src @@ -52,11 +52,11 @@ {num_acceptors, 60}, {max_connections, 40960}, %% It's possible to forbid connection from telegram client to proxy - %% with some of the protocols. Might be useful to set this to - %% 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, mtp_fake_tls]}, + %% with some of the protocols. Ti's recommended to set this to + %% only `{allowed_protocols, [mtp_secure, mtp_fake_tls]}` because those + %% protocols are more resistant to DPI detection. Connections by other + %% protocols will be immediately disallowed. + {allowed_protocols, [mtp_fake_tls, mtp_secure, mtp_abridged, mtp_intermediate]}, {init_dc_connections, 2}, {clients_per_dc_connection, 300}, diff --git a/start.sh b/start.sh index 5ead358..64424cc 100755 --- a/start.sh +++ b/start.sh @@ -34,6 +34,7 @@ PORT=${MTP_PORT:-""} SECRET=${MTP_SECRET:-""} TAG=${MTP_TAG:-""} DD_ONLY=${MTP_DD_ONLY:-""} +TLS_ONLY=${MTP_TLS_ONLY:-""} # check command line options while getopts "p:s:t:dh" o; do @@ -47,7 +48,17 @@ while getopts "p:s:t:dh" o; do t) TAG=${OPTARG} ;; + a) + if [ "${OPTARG}" -e "dd" ]; then + DD_ONLY="y" + elif [ "${OPTARG}" -eq "tls" ]; then + TLS_ONLY="y" + else + error "Invalid -a value: '${OPTARG}'" + fi + ;; d) + echo "Warning: -d is deprecated! use '-a dd' instead" DD_ONLY="y" ;; h) @@ -56,10 +67,14 @@ while getopts "p:s:t:dh" o; do esac done -DD_ARG="" +PROTO_ARG="" -if [ -n "${DD_ONLY}" ]; then - DD_ARG='-mtproto_proxy allowed_protocols [mtp_secure]' +if [ -n "${DD_ONLY}" -a -n "${TLS_ONLY}" ]; then + PROTO_ARG="-mtproto_proxy allowed_protocols [mtp_fake_tls, mtp_secure]" +elif [ -n "${DD_ONLY}" ]; then + PROTO_ARG='-mtproto_proxy allowed_protocols [mtp_secure]' +elif [ -n "${TLS_ONLY}" ]; then + PROTO_ARG='-mtproto_proxy allowed_protocols [mtp_fake_tls]' fi # if at least one option is set... @@ -76,7 +91,7 @@ if [ -n "${PORT}" -o -n "${SECRET}" -o -n "${TAG}" ]; then [ -n "`echo $TAG | grep -x '[[:xdigit:]]\{32\}'`" ] || \ error "Invalid tag. Should be 32 chars of 0-9 a-f" - exec $CMD $DD_ARG -mtproto_proxy ports "[#{name => mtproto_proxy, port => $PORT, secret => <<\"$SECRET\">>, tag => <<\"$TAG\">>}]" + exec $CMD $PROTO_ARG -mtproto_proxy ports "[#{name => mtproto_proxy, port => $PORT, secret => <<\"$SECRET\">>, tag => <<\"$TAG\">>}]" else - exec $CMD $DD_ARG + exec $CMD $PROTO_ARG fi diff --git a/test/prop_mtp_obfuscated.erl b/test/prop_mtp_obfuscated.erl index 5aa2850..794085b 100644 --- a/test/prop_mtp_obfuscated.erl +++ b/test/prop_mtp_obfuscated.erl @@ -52,7 +52,7 @@ cs_hs_exchange(Secret, DcId, Protocol) -> %% io:format("Secret: ~p; DcId: ~p, Protocol: ~p~n", %% [Secret, DcId, Protocol]), {Packet, _, _, _CliCodec} = mtp_obfuscated:client_create(Secret, Protocol, DcId), - case mtp_obfuscated:from_header(Packet, Secret, [Protocol]) of + case mtp_obfuscated:from_header(Packet, Secret) of {ok, DcId, Protocol, _SrvCodec} -> true; _ -> @@ -78,7 +78,7 @@ cs_stream_exchange(Secret, DcId, Protocol, Stream) -> %% io:format("Secret: ~p; DcId: ~p, Protocol: ~p~n", %% [Secret, DcId, Protocol]), {Header, _, _, CliCodec} = mtp_obfuscated:client_create(Secret, Protocol, DcId), - {ok, DcId, Protocol, SrvCodec} = mtp_obfuscated:from_header(Header, Secret, [Protocol]), + {ok, DcId, Protocol, SrvCodec} = mtp_obfuscated:from_header(Header, Secret), %% Client to server {CliCodec1,