ZaStoGram_desktop/Telegram/SourceFiles/tests/test_mtproxy_tls_psk.py
loop-uh c0cba8b94e Close the gap just under the canonical hello length
The padding block skipped itself whenever the extension header alone would
carry the hello past the canonical length. That reads as harmless and is
not: at a body of 513 to 516 bytes it left the hello at 513 to 516, which
is under the floor, and the floor is the one number that has to be cleared.

The floor is not folklore. The reference relay enters its fake TLS branch
only for

    (packet_len & 0xFFFFFF) == 0x010316 && (packet_len >> 24) >= 2

where the first half is the 16 03 01 prefix and the second is the high byte
of the record length, so the record must be at least 0x0200 and the hello
at least five bytes past that. Under it the relay does not reject the
client, it never sees one.

The block is now written whenever the hello is short of the floor, with no
zeros in it when the header is already enough. No template in the tree
lands in that range today - the short profiles top out around five hundred
bytes with the longest domain a name can have - so this changes no bytes on
the wire. It removes a length that would have failed silently the day a
template grew into it.

The same source puts a ceiling there too: a hello past 4096 bytes is read
truncated and then rejected for not matching what it declared. The
generator already refuses to grow past that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 01:11:41 +03:00

362 lines
16 KiB
Python

from pathlib import Path
SOURCE_DIR = Path(__file__).resolve().parents[1]
MTPROXY_DIR = SOURCE_DIR / "mtproto" / "proxy" / "mtproxy"
TLS_SOCKET_CPP = MTPROXY_DIR / "tls_socket.cpp"
TLS_SOCKET_H = MTPROXY_DIR / "tls_socket.h"
CLIENT_HELLO_BUILDER_CPP = MTPROXY_DIR / "client_hello_builder.cpp"
CLIENT_HELLO_FRAGMENTATION_CPP = MTPROXY_DIR / "client_hello_fragmentation.cpp"
CLIENT_HELLO_RULES_CPP = MTPROXY_DIR / "client_hello_rules.cpp"
TLS_SOCKET_HANDSHAKE_CPP = MTPROXY_DIR / "tls_socket_handshake.cpp"
TLS_SOCKET_PSK_CPP = MTPROXY_DIR / "tls_socket_psk.cpp"
TLS_SOCKET_PSK_H = MTPROXY_DIR / "tls_socket_psk.h"
TLS_SOCKET_RECORDS_CPP = MTPROXY_DIR / "tls_socket_records.cpp"
TCP_SOCKET_H = SOURCE_DIR / "mtproto" / "transport" / "details" / "mtproto_tcp_socket.h"
PROXY_DATA_H = SOURCE_DIR / "mtproto" / "runtime" / "proxy_data.h"
CONNECTION_BOX_CPP = SOURCE_DIR / "boxes" / "connection_box.cpp"
CORE_SETTINGS_CPP = SOURCE_DIR / "core" / "core_settings.cpp"
README = SOURCE_DIR.parents[1] / "README.md"
def psk_header():
return TLS_SOCKET_PSK_H.read_text(encoding="utf-8")
def test_qtcp_socket_members_have_direct_header_include():
tcp_header = TCP_SOCKET_H.read_text(encoding="utf-8")
tls_header = TLS_SOCKET_H.read_text(encoding="utf-8")
assert "#include <QtNetwork/QTcpSocket>" in tcp_header
assert "QTcpSocket _socket;" in tcp_header
assert "#include <QtNetwork/QTcpSocket>" not in tls_header
assert "QTcpSocket _socket;" not in tls_header
assert "std::unique_ptr<TlsSocketTransport> _transport;" in tls_header
def test_server_hello_length_uses_non_narrow_storage():
header = TLS_SOCKET_H.read_text(encoding="utf-8")
assert "int _serverHelloLength = 0;" in header
assert "int16 _serverHelloLength" not in header
def test_browser_profiles_use_dynamic_psk_marker_instead_of_inline_psk():
source = CLIENT_HELLO_RULES_CPP.read_text(encoding="utf-8")
builder = CLIENT_HELLO_BUILDER_CPP.read_text(encoding="utf-8")
rules_body = function_body(
source,
"MTPTlsClientHello PrepareClientHelloRulesInternal(\n\t\tProxyTlsProfile profile)")
padding_body = function_body(
builder,
"void Generator::Part::writeBlock(const MTPDtlsBlockPadding &data)")
permutation_body = function_body(
builder,
"void Generator::Part::writeBlock(const MTPDtlsBlockPermutation &data)")
prepare_body = function_body(builder, "ClientHello PrepareClientHello(")
assert 'S("\\x00\\x29"_q);' not in rules_body
for marker in (
"case ProxyTlsProfile::Firefox: {",
"case ProxyTlsProfile::FirefoxAndroid: {",
"case ProxyTlsProfile::AndroidOkHttp: {",
"case ProxyTlsProfile::Yandex: {",
"case ProxyTlsProfile::ChromeModern: {",
"case ProxyTlsProfile::AndroidChrome:",
):
profile_body = block_after(rules_body, marker)
assert "P();" in profile_body
assert "ShouldPadBeforeSyntheticPsk(profile)" in prepare_body
# The flag decides whether the padding goes in front of a synthetic PSK,
# never whether the hello is padded at all: without the padding the short
# profiles go out well below the canonical length and relays proxy them
# on to the camouflage domain instead of answering.
assert "if (!psk || _padBeforeSyntheticPsk) {" in padding_body
assert "writeCanonicalPadding();" in padding_body
canonical_body = function_body(
builder,
"void Generator::Part::writeCanonicalPadding()")
# No length may come out under the floor, including the few just below
# it where the extension header alone is enough to clear it.
assert "length >= kCanonicalClientHelloLength" in canonical_body
assert "const auto zeros = std::max(" in canonical_body
assert "nullptr" in permutation_body
def test_synthetic_psk_offer_is_cached_per_endpoint_sni_and_profile():
psk = TLS_SOCKET_PSK_CPP.read_text(encoding="utf-8")
handshake = TLS_SOCKET_HANDSHAKE_CPP.read_text(encoding="utf-8")
socket = TLS_SOCKET_CPP.read_text(encoding="utf-8")
plain_connected = function_body(handshake, "void TlsSocket::plainConnected()")
send_client_hello = function_body(handshake, "void TlsSocket::sendClientHello()")
terminal_body = function_body(socket, "bool TlsSocket::finishTerminal(")
hello_digest = function_body(handshake, "void TlsSocket::checkHelloDigest()")
assert "struct Ticket" in psk_header()
assert "class SyntheticPskCache final" in psk_header()
assert "std::map<QString, Entry> _entries;" in psk_header()
assert "std::map<QString, SyntheticPskCacheEntry>" not in psk
assert "kSyntheticPskPoolSize" in psk
assert "kSyntheticPskMinLifetime" in psk
assert "kSyntheticPskMaxLifetime" in psk
assert "SyntheticPskCache::prepareOffer(" in psk
assert "SyntheticPskCache::noteDataPathSuccess(" in psk
assert "SyntheticPskCache::clear(" in psk
assert "sendClientHello();" in plain_connected
assert "_sentTlsProfile = profile;" in send_client_hello
assert "if (_stealth.syntheticPsk)" in send_client_hello
assert "_runtime->proxyServices().syntheticPsks().prepareOffer(" in (
send_client_hello)
assert "MtProxy::EndpointKey(_endpointId.canonical)" in send_client_hello
assert "domainFromSecret()" in send_client_hello
assert "profile" in send_client_hello
assert "std::move(pskOffer)" in send_client_hello
assert "clearSyntheticPskOnFailure(reason)" in terminal_body
assert "_sentTlsProfile" in function_body(
socket,
"bool TlsSocket::clearSyntheticPskOnFailure(")
assert "NoteSyntheticPskHandshakeSuccess(" not in hello_digest
assert "noteDataPathSuccess(" not in hello_digest
def test_synthetic_psk_cache_is_cleared_on_post_handshake_failure():
source = TLS_SOCKET_CPP.read_text(encoding="utf-8")
terminal_body = function_body(source, "bool TlsSocket::finishTerminal(")
assert "clearSyntheticPskOnFailure(reason)" in terminal_body
def test_synthetic_psk_offer_failures_clear_remaining_tickets():
header = TLS_SOCKET_H.read_text(encoding="utf-8")
socket = TLS_SOCKET_CPP.read_text(encoding="utf-8")
handshake = TLS_SOCKET_HANDSHAKE_CPP.read_text(encoding="utf-8")
send_client_hello = function_body(handshake, "void TlsSocket::sendClientHello()")
disconnected_body = function_body(handshake, "void TlsSocket::plainDisconnected()")
clear_helper = function_body(
socket,
"bool TlsSocket::clearSyntheticPskOnFailure(")
terminal_body = function_body(socket, "bool TlsSocket::finishTerminal(")
assert "bool _syntheticPskOffered = false;" in header
assert "_syntheticPskOffered = pskOffer.has_value();" in send_client_hello
assert "_syntheticPskOffered = false;" in disconnected_body
assert "clearSyntheticPskOnFailure(reason)" in terminal_body
assert "if (!_syntheticPskOffered || IsProxyCheck(_endpointUse))" in clear_helper
for reason in (
"ClientHelloSentNoServerHello",
"TlsAlertAfterClientHello",
"ServerHelloHmacMismatch",
):
assert f"case MtProxy::FailureReason::{reason}:" in clear_helper
assert "case MtProxy::FailureReason::ServerHelloOkNoAppData:" not in (
clear_helper)
assert "_runtime->proxyServices().syntheticPsks().clear(" in clear_helper
assert "_syntheticPskOffered = false;" in clear_helper
def test_synthetic_psk_uses_cached_identity_and_plausible_age():
source = CLIENT_HELLO_BUILDER_CPP.read_text(encoding="utf-8")
tls_socket = TLS_SOCKET_PSK_CPP.read_text(encoding="utf-8")
helper = function_body(
source,
"void Generator::Part::writeSyntheticPskExtension()")
offer_body = function_body(
tls_socket,
"std::optional<SyntheticPskOffer> SyntheticPskCache::prepareOffer(")
assert "const auto binderLengths = std::array{ 32, 48 };" in helper
assert "!_pskOffer" in helper
assert "!*_pskOffer" in helper
assert "write16(uint16(0x0029));" in helper
assert "write16(uint16(extensionLength));" in helper
assert "write16(uint16(identitiesLength));" in helper
assert "write16(uint16(bindersLength));" in helper
assert "offer.identity" in helper
assert "write32(offer.obfuscatedTicketAge);" in helper
assert "random(identityLength);" not in helper
assert "random(4);" not in helper
assert "binderPrefix[0] = bytes::type(binderLength);" in helper
assert "random(binderLength);" in helper
assert "ticketAgeAdd" in tls_socket
assert "issuedAt" in tls_socket
assert "expiresAt" in tls_socket
assert "obfuscatedTicketAge" in offer_body
assert "crl::now()" in offer_body
def test_synthetic_psk_ticket_is_consumed_after_offer():
source = TLS_SOCKET_PSK_CPP.read_text(encoding="utf-8")
offer_body = function_body(
source,
"std::optional<SyntheticPskOffer> SyntheticPskCache::prepareOffer(")
assert "const auto ticket = entry.tickets[index];" in offer_body
assert "entry.tickets.erase(" in offer_body
assert "entry.nextIndex = (entry.nextIndex + 1) % count;" not in offer_body
def test_synthetic_psk_does_not_take_over_mtproxy_digest_slot():
source = CLIENT_HELLO_BUILDER_CPP.read_text(encoding="utf-8")
helper = function_body(
source,
"void Generator::Part::writeSyntheticPskExtension()")
zero_body = function_body(
source,
"void Generator::Part::writeBlock(const MTPDtlsBlockZero &data)")
finalize_body = function_body(
source,
"void Generator::Part::finalize(bytes::const_span key)")
assert "_digestPosition" not in helper
assert "MTP_tlsBlockZero" not in helper
assert "length == kClientHelloDigestLength && !_digestPosition" in zero_body
assert finalize_body.index("writeDigest(key);") < finalize_body.index(
"injectTimestamp();")
def test_client_hello_fragmentation_targets_sni_hostname():
source = CLIENT_HELLO_FRAGMENTATION_CPP.read_text(encoding="utf-8")
builder_header = (
MTPROXY_DIR / "client_hello_builder.h").read_text(encoding="utf-8")
tls_socket = TLS_SOCKET_HANDSHAKE_CPP.read_text(encoding="utf-8")
header = TLS_SOCKET_H.read_text(encoding="utf-8")
split_body = function_body(source, "int ClientHelloFragmentSplit(")
plan_body = function_body(
source,
"ClientHelloFragmentationPlan PrepareClientHelloFragmentation(")
write_body = function_body(
tls_socket,
"void TlsSocket::writeClientHello(const QByteArray &data)")
disconnected_body = function_body(
tls_socket,
"void TlsSocket::plainDisconnected()")
assert "ClientHelloSniHostRange(" in source
assert "kClientHelloFragmentDelayMin" in source
assert "kClientHelloFragmentDelayMax" in source
assert "const auto range = ClientHelloSniHostRange(data);" in split_body
assert "range.offset + 1 + base::RandomIndex(range.length - 1)" in split_body
assert "base::RandomIndex(delayRange)" in plan_body
assert "secondDelay" in builder_header
assert "QByteArray _clientHelloTail;" in header
assert "RuntimeTimer _clientHelloFragmentTimer;" in header
assert "PrepareClientHelloFragmentation(" in write_body
assert "plan.firstSize" in write_body
assert "_clientHelloFragmentTimer.callOnce(plan.secondDelay);" in write_body
assert "writeClientHelloTail();" in write_body
assert "_clientHelloTail = QByteArray();" in disconnected_body
assert "_clientHelloFragmentTimer.cancel();" in disconnected_body
assert "base::RandomIndex(range)" not in tls_socket
def test_mtproxy_profile_wording_does_not_claim_ja4_validation():
box = CONNECTION_BOX_CPP.read_text(encoding="utf-8")
readme = README.read_text(encoding="utf-8")
assert 'u"TLS ClientHello profile"_q' in box
assert "TLS fingerprint (JA4)" not in box
assert "JA4" not in readme
assert "ClientHello" in readme
def test_manual_tls_profiles_have_independent_transport_cases():
source = CLIENT_HELLO_RULES_CPP.read_text(encoding="utf-8")
box = CONNECTION_BOX_CPP.read_text(encoding="utf-8")
rules_body = function_body(
source,
"MTPTlsClientHello PrepareClientHelloRulesInternal(\n\t\tProxyTlsProfile profile)")
for profile, label in (
("Auto", "Auto (Chrome)"),
("AutoRotate", "Auto-rotate"),
("ChromeModern", "Chrome Modern"),
("AndroidChrome", "Android Chrome"),
("Firefox", "Firefox"),
("FirefoxAndroid", "Firefox Android"),
("Yandex", "Yandex"),
("AndroidOkHttp", "Android OkHttp"),
):
assert f"case ProxyTlsProfile::{profile}: {{" in rules_body
assert f'addTls(Profile::{profile}, u"{label}"_q);' in box
assert "case ProxyTlsProfile::Auto:\n\tcase ProxyTlsProfile::AndroidChrome:" not in rules_body
assert "case ProxyTlsProfile::AndroidChrome:\n\tcase ProxyTlsProfile::AutoRotate:" not in rules_body
def test_stealth_record_timing_and_startup_cover_default_off():
data = PROXY_DATA_H.read_text(encoding="utf-8")
settings = CORE_SETTINGS_CPP.read_text(encoding="utf-8")
assert "ProxyRecordSizing recordSizing = ProxyRecordSizing::Off;" in data
assert "ProxyTiming timing = ProxyTiming::Off;" in data
assert "ProxyStartupCover startupCover = ProxyStartupCover::Off;" in data
assert 'read(\n\t\t"mtproxy/startupCover",\n\t\tint(result.startupCover),' in settings
def test_admission_plan_drives_tls_socket_spacing_and_diagnostics():
header = TLS_SOCKET_H.read_text(encoding="utf-8")
source = TLS_SOCKET_HANDSHAKE_CPP.read_text(encoding="utf-8")
socket = TLS_SOCKET_CPP.read_text(encoding="utf-8")
connected_body = function_body(source, "void TlsSocket::plainConnected()")
terminal_body = function_body(socket, "bool TlsSocket::finishTerminal(")
clear_body = function_body(
socket,
"bool TlsSocket::clearSyntheticPskOnFailure(")
parts12_body = function_body(source, "void TlsSocket::checkHelloParts12(int parts1Size)")
digest_body = function_body(source, "void TlsSocket::checkHelloDigest()")
assert "MtProxyAttemptPlan _mtproxyPlan;" in header
assert "ProxyTlsProfile _configuredTlsProfile" in header
assert "applyAdaptiveRecipe" not in source
assert "reportTransportEvent(" in source
assert "const auto delay = MtProxy::ConnectionSpacing(_connectionPattern);" in connected_body
assert "_clientHelloTimer.callOnce(delay);" in connected_body
assert "clearSyntheticPskOnFailure(reason)" in terminal_body
assert "_sentTlsProfile" in clear_body
assert "MtProxy::FailureReason::TlsAlertAfterClientHello" in parts12_body
assert "MtProxy::FailureReason::ProxyProtocolBadResponse" in parts12_body
assert "MtProxy::FailureReason::ServerHelloHmacMismatch" in digest_body
def function_body(text: str, signature: str) -> str:
start = text.index(signature)
brace = text.index(" {\n", start) + 1
return body_from_brace(text, brace)
def block_after(text: str, marker: str) -> str:
start = text.index(marker)
brace = text.index("{", start)
return body_from_brace(text, brace)
def body_from_brace(text: str, brace: int) -> str:
depth = 0
for index in range(brace, len(text)):
char = text[index]
if char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return text[brace + 1:index]
raise AssertionError("body not found")
if __name__ == "__main__":
test_qtcp_socket_members_have_direct_header_include()
test_server_hello_length_uses_non_narrow_storage()
test_browser_profiles_use_dynamic_psk_marker_instead_of_inline_psk()
test_synthetic_psk_offer_is_cached_per_endpoint_sni_and_profile()
test_synthetic_psk_cache_is_cleared_on_post_handshake_failure()
test_synthetic_psk_uses_cached_identity_and_plausible_age()
test_synthetic_psk_ticket_is_consumed_after_offer()
test_synthetic_psk_does_not_take_over_mtproxy_digest_slot()
test_client_hello_fragmentation_targets_sni_hostname()
test_mtproxy_profile_wording_does_not_claim_ja4_validation()
test_manual_tls_profiles_have_independent_transport_cases()
test_stealth_record_timing_and_startup_cover_default_off()
test_admission_plan_drives_tls_socket_spacing_and_diagnostics()