mirror of
https://github.com/seriyps/mtproto_proxy.git
synced 2026-05-13 08:46:46 +00:00
Send TLS decode_error alert on malformed ClientHello and missing SNI
Scanners probe for fake-TLS proxies by sending structurally malformed
ClientHellos (e.g. ExtensionsLen=0 with trailing extension bytes). A
real TLS server responds with a fatal decode_error alert; previously
the proxy crashed the handler process silently, making it detectable.
Changes:
- mtp_fake_tls: add TLS_REC_ALERT, TLS_ALERT_FATAL, TLS_ALERT_DECODE_ERROR
macros; export tls_decode_error_alert/0 which builds the 7-byte alert
frame from macros
- mtp_fake_tls: add second clause to parse_client_hello/1 that throws
{protocol_error, tls_bad_client_hello, bad_client_hello} instead of
letting a bare function_clause propagate
- mtp_fake_tls: tighten parse_sni/1 catch to match the specific tagged
error rather than a catch-all error:_
- mtp_handler: add attempt_fronting clauses for tls_bad_client_hello and
tls_no_sni — both send the decode_error alert before closing
- mtp_handler: effective_secret/2 now raises tls_bad_client_hello (not
tls_invalid_digest) when per_sni_secrets=on and the ClientHello has
no SNI, so it also gets the alert treatment
- single_dc_SUITE: new malformed_tls_hello_decode_error_case/1 verifies
the alert bytes are sent and the metric is incremented
- AGENTS.md: document test organisation, process architecture diagram,
and upstream/downstream naming note
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
36b30e3f5f
commit
dfe8ebf034
4 changed files with 158 additions and 4 deletions
|
|
@ -26,7 +26,8 @@
|
|||
domain_fronting_fragmented_case/1,
|
||||
domain_fronting_replay_case/1,
|
||||
per_sni_secrets_on_case/1,
|
||||
per_sni_secrets_wrong_secret_case/1
|
||||
per_sni_secrets_wrong_secret_case/1,
|
||||
malformed_tls_hello_decode_error_case/1
|
||||
]).
|
||||
|
||||
-export([set_env/2,
|
||||
|
|
@ -735,6 +736,51 @@ per_sni_secrets_wrong_secret_case(Cfg) when is_list(Cfg) ->
|
|||
1, mtp_test_metric:get_tags(
|
||||
count, [?APP, protocol_error, total], [?FUNCTION_NAME, tls_invalid_digest])).
|
||||
|
||||
%% @doc A structurally malformed ClientHello (ExtensionsLen=0 but data follows) must cause
|
||||
%% the proxy to send a TLS fatal decode_error alert and then close the connection,
|
||||
%% rather than crashing silently.
|
||||
malformed_tls_hello_decode_error_case({pre, Cfg}) ->
|
||||
setup_single(?FUNCTION_NAME, 10000 + ?LINE, #{}, Cfg);
|
||||
malformed_tls_hello_decode_error_case({post, Cfg}) ->
|
||||
stop_single(Cfg);
|
||||
malformed_tls_hello_decode_error_case(Cfg) when is_list(Cfg) ->
|
||||
Host = ?config(mtp_host, Cfg),
|
||||
Port = ?config(mtp_port, Cfg),
|
||||
%% Build a ClientHello that is structurally valid at the TLS record layer
|
||||
%% (correct lengths, version bytes) but lies about ExtensionsLen=0 while
|
||||
%% trailing bytes follow — this is the exact pattern seen from real scanners.
|
||||
TlsPacketLen = 512,
|
||||
HelloLen = 508, % TlsPacketLen - 4 (hello type + hello len field)
|
||||
Random = crypto:strong_rand_bytes(32),
|
||||
SessId = crypto:strong_rand_bytes(32),
|
||||
CipherSuites = <<19, 1>>, % TLS_AES_128_GCM_SHA256, 2 bytes
|
||||
%% Padding fills the rest of the TLS frame after ExtensionsLen to hit TlsPacketLen exactly.
|
||||
%% Consumed so far inside the frame: hello_type(1)+hello_len(3)+version(2)+random(32)
|
||||
%% +sessid_len(1)+sessid(32)+cs_len(2)+cs(2)+comp_len(1)+comp(1)+ext_len(2) = 79
|
||||
PaddingLen = TlsPacketLen - 79,
|
||||
Padding = binary:copy(<<0>>, PaddingLen),
|
||||
MalformedHello = <<22, 3, 1, TlsPacketLen:16, % TLS record header (handshake, TLS1.0)
|
||||
1, HelloLen:24, % ClientHello type + length
|
||||
3, 3, % legacy version (TLS1.2)
|
||||
Random/binary, % 32-byte random
|
||||
32, SessId/binary, % session ID
|
||||
2:16, CipherSuites/binary, % cipher suites
|
||||
1, 0, % compression methods
|
||||
0:16, % ExtensionsLen = 0 (lie)
|
||||
Padding/binary>>, % trailing bytes that should be extensions
|
||||
{ok, Sock} = gen_tcp:connect(Host, Port, [binary, {active, false}], 2000),
|
||||
ok = gen_tcp:send(Sock, MalformedHello),
|
||||
%% Proxy must send back a TLS fatal decode_error alert (21, 3, 3, 0, 2, 2, 50)
|
||||
ExpectedAlert = mtp_fake_tls:tls_decode_error_alert(),
|
||||
{ok, Response} = gen_tcp:recv(Sock, byte_size(ExpectedAlert), 5000),
|
||||
?assertEqual(ExpectedAlert, Response),
|
||||
%% Then close the connection
|
||||
?assertEqual({error, closed}, gen_tcp:recv(Sock, 0, 2000)),
|
||||
gen_tcp:close(Sock),
|
||||
?assertEqual(
|
||||
1, mtp_test_metric:get_tags(
|
||||
count, [?APP, protocol_error, total], [?FUNCTION_NAME, tls_bad_client_hello])).
|
||||
|
||||
setup_single(Name, MtpPort, DcCfg0, Cfg) ->
|
||||
setup_single(Name, "127.0.0.1", MtpPort, DcCfg0, Cfg).
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue