ZaStoGram/Tools/check_mtproxy_faketls_path.py
2026-08-05 19:18:49 +03:00

742 lines
38 KiB
Python

#!/usr/bin/env python3
"""Static guard for the active MTProxy FakeTLS transport path.
The working reference is tsrman/tg commit 9fe18931 for the risky transport
parts. ZaStoGram keeps the default wrapped-data behavior conservative, then adds
runtime-gated FakeTLS profile selection, ClientHello fragmentation, endpoint
startup scheduling, startup diagnostics, TLS write queueing, and optional
post-handshake data shaping layers.
"""
from pathlib import Path
import codecs
import hashlib
import re
import sys
ROOT = Path(__file__).resolve().parents[1]
CPP = ROOT / "TMessagesProj/jni/tgnet/ConnectionSocket.cpp"
HDR = ROOT / "TMessagesProj/jni/tgnet/ConnectionSocket.h"
MACHINE_HDR = ROOT / "TMessagesProj/jni/tgnet/ConnectionSocketStateMachine.h"
CONNECTION_CPP = ROOT / "TMessagesProj/jni/tgnet/Connection.cpp"
PROXY_CHECK_HDR = ROOT / "TMessagesProj/jni/tgnet/ProxyCheckInfo.h"
MTPROXY_OPTIONS = ROOT / "TMessagesProj/jni/mtproxy/MtProxyOptions.h"
MTPROXY_CLIENT_HELLO_POLICY_H = ROOT / "TMessagesProj/jni/mtproxy/MtProxyClientHelloPolicy.h"
MTPROXY_CLIENT_HELLO_POLICY_CPP = ROOT / "TMessagesProj/jni/mtproxy/MtProxyClientHelloPolicy.cpp"
MTPROXY_PHASE_CONTRACT_H = ROOT / "TMessagesProj/jni/mtproxy/MtProxyPhaseContract.h"
MTPROXY_SECRET_DOMAIN_H = ROOT / "TMessagesProj/jni/mtproxy/MtProxySecretDomain.h"
MTPROXY_SECRET_DOMAIN_CPP = ROOT / "TMessagesProj/jni/mtproxy/MtProxySecretDomain.cpp"
MTPROXY_SERVER_FLIGHT_PARSER_H = ROOT / "TMessagesProj/jni/mtproxy/MtProxyServerFlightParser.h"
MTPROXY_SERVER_FLIGHT_PARSER_CPP = ROOT / "TMessagesProj/jni/mtproxy/MtProxyServerFlightParser.cpp"
MTPROXY_HANDSHAKE_SCHEDULER_H = ROOT / "TMessagesProj/jni/mtproxy/MtProxyHandshakeScheduler.h"
MTPROXY_HANDSHAKE_SCHEDULER_CPP = ROOT / "TMessagesProj/jni/mtproxy/MtProxyHandshakeScheduler.cpp"
MTPROXY_DATA_PATH_SHAPER_CPP = ROOT / "TMessagesProj/jni/mtproxy/MtProxyDataPathShaper.cpp"
CMAKE = ROOT / "TMessagesProj/jni/CMakeLists.txt"
CM_JAVA = ROOT / "TMessagesProj/src/main/java/org/telegram/tgnet/ConnectionsManager.java"
CM_CPP = ROOT / "TMessagesProj/jni/tgnet/ConnectionsManager.cpp"
CM_HDR = ROOT / "TMessagesProj/jni/tgnet/ConnectionsManager.h"
WRAPPER = ROOT / "TMessagesProj/jni/TgNetWrapper.cpp"
PROFILE_FUNCTIONS = [
("firefox", "getFirefoxDefault", "getDefault"),
("android_chrome", "getDefault", "getAndroidChromeDefault"),
("chrome_modern", "getChromeModernDefault", "getLegacyNoGreaseDefault"),
("legacy_no_grease_no_4469_no_modern_extensions", "getLegacyNoGreaseDefault", "getFirefoxAndroidDefault"),
("firefox_android", "getFirefoxAndroidDefault", "getAndroidOkHttpDefault"),
("android_okhttp", "getAndroidOkHttpDefault", "getYandexDefault"),
("yandex", "getYandexDefault", None),
]
PROFILE_TEST_DOMAINS = ("exploralab.ru", "www.cloudflare.com", "tg.pepewtf.top")
ECH_TEST_LENGTHS = (144, 176, 208, 240)
PADDING_TEST_TARGETS = (512, 640, 768)
OP_RE = re.compile(
r'Op::string\("((?:\\.|[^"\\])*)"\s*,\s*(\d+)\)'
r"|Op::(random|zero|grease)\((\d+)\)"
r"|Op::(K|M|P|E|domain|begin_scope|end_scope)\(\)"
)
def main() -> int:
cpp = CPP.read_text(encoding="utf-8")
header = HDR.read_text(encoding="utf-8")
machine_header = MACHINE_HDR.read_text(encoding="utf-8")
connection_cpp = CONNECTION_CPP.read_text(encoding="utf-8")
proxy_check_header = PROXY_CHECK_HDR.read_text(encoding="utf-8")
mtproxy_options = MTPROXY_OPTIONS.read_text(encoding="utf-8")
client_hello_policy_h = MTPROXY_CLIENT_HELLO_POLICY_H.read_text(encoding="utf-8")
client_hello_policy_cpp = MTPROXY_CLIENT_HELLO_POLICY_CPP.read_text(encoding="utf-8")
phase_contract = MTPROXY_PHASE_CONTRACT_H.read_text(encoding="utf-8")
secret_domain_h = MTPROXY_SECRET_DOMAIN_H.read_text(encoding="utf-8")
secret_domain_cpp = MTPROXY_SECRET_DOMAIN_CPP.read_text(encoding="utf-8")
server_flight_parser_h = MTPROXY_SERVER_FLIGHT_PARSER_H.read_text(encoding="utf-8")
server_flight_parser_cpp = MTPROXY_SERVER_FLIGHT_PARSER_CPP.read_text(encoding="utf-8")
handshake_scheduler_h = MTPROXY_HANDSHAKE_SCHEDULER_H.read_text(encoding="utf-8")
handshake_scheduler_cpp = MTPROXY_HANDSHAKE_SCHEDULER_CPP.read_text(encoding="utf-8")
data_path_shaper_cpp = MTPROXY_DATA_PATH_SHAPER_CPP.read_text(encoding="utf-8")
cmake = CMAKE.read_text(encoding="utf-8")
java = CM_JAVA.read_text(encoding="utf-8")
manager_cpp = CM_CPP.read_text(encoding="utf-8")
manager_header = CM_HDR.read_text(encoding="utf-8")
wrapper = WRAPPER.read_text(encoding="utf-8")
combined = cpp + "\n" + header + "\n" + machine_header
errors: list[str] = []
def require(condition: bool, message: str) -> None:
if not condition:
errors.append(message)
def function_body(name: str, next_name: str | None = None) -> str:
start = cpp.find(f"static TlsHello {name}()")
if start < 0:
return ""
if next_name is None:
end = cpp.find("uint32_t writeToBuffer", start)
else:
end = cpp.find(f"static TlsHello {next_name}()", start)
return cpp[start:end if end > start else len(cpp)]
def connection_socket_defines(name: str) -> bool:
return re.search(rf"(?m)^(?:static[ \t]+)?[A-Za-z_][\w:<>, \t*&]*[ \t]+{re.escape(name)}[ \t]*\(", cpp) is not None
def balanced_scopes(name: str, next_name: str | None = None) -> bool:
body = function_body(name, next_name)
return bool(body) and body.count("Op::begin_scope()") == body.count("Op::end_scope()")
def c_string_bytes(value: str) -> bytes:
return codecs.decode(value, "unicode_escape").encode("latin1")
def grease_bytes(seed: int) -> bytes:
value = ((seed * 0x20) & 0xf0) | 0x0a
return bytes((value, value))
def append_scope_end(data: bytearray, scopes: list[int], profile: str) -> str | None:
if not scopes:
return f"{profile}: end_scope without matching begin_scope"
begin = scopes.pop()
size = len(data) - begin - 2
if size > 0xffff:
return f"{profile}: scope too large size={size}"
data[begin] = (size >> 8) & 0xff
data[begin + 1] = size & 0xff
return None
def render_profile(name: str, body: str, domain: str, ech_length: int, padding_target: int) -> tuple[bytes, str | None]:
data = bytearray()
scopes: list[int] = []
for match in OP_RE.finditer(body):
string_value, declared_length, sized_op, sized_value, bare_op = match.groups()
if string_value is not None:
raw = c_string_bytes(string_value)
expected = int(declared_length)
if len(raw) != expected:
return bytes(data), f"{name}: Op::string length mismatch declared={expected} actual={len(raw)}"
data.extend(raw)
elif sized_op == "random" or sized_op == "zero":
data.extend(b"\x00" * int(sized_value))
elif sized_op == "grease":
data.extend(grease_bytes(int(sized_value)))
elif bare_op == "K":
data.extend(b"\x11" * 32)
elif bare_op == "M":
data.extend(b"\x22" * 1184)
elif bare_op == "E":
data.extend(b"\x33" * ech_length)
elif bare_op == "P":
length = len(data)
if length < 517:
data.extend(b"\x00\x15")
scopes.append(len(data))
data.extend(b"\x00\x00")
data.extend(b"\x00" * max(0, 517 - 4 - length))
error = append_scope_end(data, scopes, name)
if error:
return bytes(data), error
elif bare_op == "domain":
data.extend(domain.encode("ascii")[:253])
elif bare_op == "begin_scope":
scopes.append(len(data))
data.extend(b"\x00\x00")
elif bare_op == "end_scope":
error = append_scope_end(data, scopes, name)
if error:
return bytes(data), error
if scopes:
return bytes(data), f"{name}: unclosed begin_scope count={len(scopes)}"
return bytes(data), None
def is_grease_value(value: int) -> bool:
high = (value >> 8) & 0xff
low = value & 0xff
return high == low and (low & 0x0f) == 0x0a
def is_relay_grease_cipher(value: int) -> bool:
return (value & 0x0f0f) == 0x0a0a
def validate_rendered_hello(name: str, data: bytes, domain: str) -> str | None:
size = len(data)
if size < 517 or size > 4096:
return f"{name}: invalid hello size={size}"
if data[0:3] != b"\x16\x03\x01" or data[5] != 0x01 or data[9:11] != b"\x03\x03":
return f"{name}: invalid hello prefix"
record_length = int.from_bytes(data[3:5], "big")
handshake_length = int.from_bytes(data[6:9], "big")
if record_length + 5 != size or handshake_length + 9 != size:
return f"{name}: invalid hello lengths record={record_length} handshake={handshake_length} size={size}"
pos = 11 + 32
session_len = data[pos]
if session_len != 32:
return f"{name}: session id must be 32 bytes, got {session_len}"
pos += 1 + session_len
if pos + 2 > size:
return f"{name}: missing cipher suites length"
if pos != 76:
return f"{name}: cipher suites offset changed offset={pos}"
cipher_len = int.from_bytes(data[pos:pos + 2], "big")
pos += 2
cipher_end = pos + cipher_len
if cipher_len < 2 or (cipher_len % 2) != 0 or cipher_end > size:
return f"{name}: invalid cipher suites length={cipher_len}"
first_cipher = 0
for cipher_pos in range(pos, cipher_end, 2):
cipher = int.from_bytes(data[cipher_pos:cipher_pos + 2], "big")
if not is_relay_grease_cipher(cipher):
first_cipher = cipher
break
if first_cipher not in (0x1301, 0x1302, 0x1303):
return f"{name}: invalid first non-GREASE cipher=0x{first_cipher:04x}"
pos = cipher_end
if pos >= size:
return f"{name}: missing compression methods"
compression_len = data[pos]
pos += 1 + compression_len
if pos + 2 > size:
return f"{name}: missing extensions length"
extensions_len = int.from_bytes(data[pos:pos + 2], "big")
pos += 2
extensions_end = pos + extensions_len
if extensions_end != size:
return f"{name}: invalid extensions length={extensions_len} parsed_end={extensions_end} size={size}"
expected_domain = domain.encode("ascii")
saw_exact_sni = False
while pos < extensions_end:
if pos + 4 > extensions_end:
return f"{name}: truncated extension header at offset={pos}"
extension_type = int.from_bytes(data[pos:pos + 2], "big")
extension_len = int.from_bytes(data[pos + 2:pos + 4], "big")
pos += 4
if pos + extension_len > extensions_end:
return f"{name}: extension 0x{extension_type:04x} overruns extensions block"
if extension_type == 0x0000:
value = data[pos:pos + extension_len]
if len(value) < 5 or int.from_bytes(value[0:2], "big") + 2 != len(value):
return f"{name}: malformed SNI extension"
name_length = int.from_bytes(value[3:5], "big")
if value[2] != 0 or 5 + name_length != len(value):
return f"{name}: malformed SNI host_name"
if value[5:] != expected_domain:
return f"{name}: SNI does not exactly match secret domain"
saw_exact_sni = True
pos += extension_len
if not saw_exact_sni:
return f"{name}: missing exact SNI domain"
return None
def sha12(value: str) -> str:
return hashlib.sha256(value.encode()).hexdigest()[:12] if value else "000000000000"
def ja4_for_client_hello(data: bytes) -> str:
position = 9
legacy_version = int.from_bytes(data[position:position + 2], "big")
position += 34
position += 1 + data[position]
cipher_length = int.from_bytes(data[position:position + 2], "big")
position += 2
ciphers = [
int.from_bytes(data[offset:offset + 2], "big")
for offset in range(position, position + cipher_length, 2)
]
position += cipher_length
position += 1 + data[position]
extensions_length = int.from_bytes(data[position:position + 2], "big")
position += 2
extensions_end = position + extensions_length
extensions: list[int] = []
signatures: list[int] = []
versions: list[int] = []
first_alpn = b""
while position + 4 <= extensions_end:
extension = int.from_bytes(data[position:position + 2], "big")
length = int.from_bytes(data[position + 2:position + 4], "big")
value = data[position + 4:position + 4 + length]
position += 4 + length
extensions.append(extension)
if extension == 0x002b and value:
versions = [
int.from_bytes(value[offset:offset + 2], "big")
for offset in range(1, min(1 + value[0], len(value)), 2)
if offset + 2 <= len(value)
]
elif extension == 0x000d and len(value) >= 2:
signatures_length = int.from_bytes(value[0:2], "big")
signatures = [
int.from_bytes(value[offset:offset + 2], "big")
for offset in range(2, min(2 + signatures_length, len(value)), 2)
if offset + 2 <= len(value)
]
elif extension == 0x0010 and len(value) >= 3:
first_alpn = value[3:3 + value[2]]
clean_ciphers = [value for value in ciphers if not is_grease_value(value)]
clean_extensions = [value for value in extensions if not is_grease_value(value)]
clean_versions = [value for value in versions if not is_grease_value(value)]
version = max(clean_versions) if clean_versions else legacy_version
version_code = {0x0304: "13", 0x0303: "12", 0x0302: "11", 0x0301: "10"}.get(version, "00")
if not first_alpn:
alpn_code = "00"
elif chr(first_alpn[0]).isalnum() and chr(first_alpn[-1]).isalnum():
alpn_code = chr(first_alpn[0]) + chr(first_alpn[-1])
else:
alpn_hex = first_alpn.hex()
alpn_code = alpn_hex[0] + alpn_hex[-1]
prefix = (
"t" + version_code
+ ("d" if 0x0000 in clean_extensions else "i")
+ f"{min(len(clean_ciphers), 99):02d}"
+ f"{min(len(clean_extensions), 99):02d}"
+ alpn_code
)
cipher_input = ",".join(sorted(f"{value:04x}" for value in clean_ciphers))
extension_input = ",".join(sorted(
f"{value:04x}" for value in clean_extensions if value not in (0x0000, 0x0010)
))
signature_input = ",".join(f"{value:04x}" for value in signatures if not is_grease_value(value))
if signature_input:
extension_input += "_" + signature_input
return "_".join((prefix, sha12(cipher_input), sha12(extension_input)))
def validate_profile_rendering() -> list[str]:
profile_errors: list[str] = []
for profile_name, function_name, next_name in PROFILE_FUNCTIONS:
body = function_body(function_name, next_name)
if not body:
profile_errors.append(f"{profile_name}: missing {function_name}")
continue
for domain in PROFILE_TEST_DOMAINS:
for ech_length in ECH_TEST_LENGTHS:
for padding_target in PADDING_TEST_TARGETS:
data, render_error = render_profile(profile_name, body, domain, ech_length, padding_target)
if render_error:
profile_errors.append(render_error)
continue
validate_error = validate_rendered_hello(profile_name, data, domain)
if validate_error:
profile_errors.append(
f"{validate_error} domain={domain} ech={ech_length} padding={padding_target}"
)
return profile_errors
require("MT_PROXY_TLS_PROFILE_AUTO" in java, "Java must define the auto MTProxy TLS profile")
require("MT_PROXY_TLS_PROFILE_FIREFOX" in java, "Java must define the Firefox MTProxy TLS profile")
require("MT_PROXY_TLS_PROFILE_ANDROID_CHROME" in java, "Java must define the Android Chrome MTProxy TLS profile")
require("MT_PROXY_TLS_PROFILE_YANDEX" in java, "Java must define the Yandex MTProxy TLS profile")
require("MT_PROXY_TLS_PROFILE_FIREFOX_ANDROID" in java, "Java must define the Firefox Android MTProxy TLS profile")
require("MT_PROXY_TLS_PROFILE_ANDROID_OKHTTP" in java, "Java must define the Android OkHttp MTProxy TLS profile")
require("MT_PROXY_TLS_PROFILE_AUTO_ROTATE" in java, "Java must define the auto-rotate MTProxy TLS profile")
require("MT_PROXY_TLS_PROFILE_CHROME_MODERN" in java, "Java must define the Chrome Modern MTProxy TLS profile")
require(
"resolveMtProxyTlsProfile" in java
and "return MT_PROXY_TLS_PROFILE_YANDEX;" in java
and "stableMtProxyTlsHash" not in java
and "MT_PROXY_TLS_PROFILE_SALT" not in java,
"Java Auto mode must use the same measured-safe Yandex default as tdesktop",
)
require(
"mtProxyDefaultTlsProfile" in client_hello_policy_cpp
and "return MT_PROXY_TLS_PROFILE_YANDEX;" in client_hello_policy_cpp
and "MT_PROXY_TLS_PROFILE_CHROME_MODERN" in client_hello_policy_cpp
and "MT_PROXY_TLS_PROFILE_ANDROID_CHROME" in client_hello_policy_cpp,
"native wire policy must default to Yandex and withhold measured-bad Chromium profiles",
)
require(
"native_setProxySettings(currentAccount, proxyAddress, proxyPort, proxyUsername, proxyPassword, proxySecret, MtProxyOptions.resolve(proxyAddress, proxyPort, proxySecret), activationGeneration, ProxyConnectionEvent.Origin.STARTUP_RESTORE.wireName)" in java
and "native_setProxySettings(a, address, port, username, password, secret, enabledOptions, activationGeneration, activationOrigin.wireName)" in java,
"Java must pass resolved MTProxy options into native proxy settings",
)
require(
"native_checkProxy(currentAccount, address, port, username, password, secret, MtProxyOptions.resolve(address, port, secret), requestTimeDelegate)" in java,
"Java proxy checks must use the same resolved MTProxy options as real connections",
)
require(
"native_setProxySettings(int currentAccount, String address, int port, String username, String password, String secret, MtProxyOptions options, int activationGeneration, String activationOrigin)" in java,
"Java native_setProxySettings declaration must take MtProxyOptions",
)
require(
'native_setProxySettings", "(ILjava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lorg/telegram/tgnet/MtProxyOptions;ILjava/lang/String;)V"' in wrapper,
"JNI native_setProxySettings signature must take MtProxyOptions",
)
require(
'native_checkProxy", "(ILjava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lorg/telegram/tgnet/MtProxyOptions;Lorg/telegram/tgnet/RequestTimeDelegate;)J"' in wrapper,
"JNI native_checkProxy signature must take MtProxyOptions",
)
require(
"setProxySettings(std::string address, uint16_t port, std::string username, std::string password, std::string secret, const MtProxyOptions &options, uint32_t activationGeneration, std::string activationOrigin)" in manager_header
and "ConnectionsManager::setProxySettings(std::string address, uint16_t port, std::string username, std::string password, std::string secret, const MtProxyOptions &options, uint32_t activationGeneration, std::string activationOrigin)" in manager_cpp,
"ConnectionsManager::setProxySettings must store MtProxyOptions",
)
require(
"MtProxyOptions proxyMtProxyOptions" in manager_header
and "optionsChanged" in manager_cpp
and "proxyMtProxyOptions = normalizedOptions" in manager_cpp,
"ConnectionsManager must keep option state and reconnect on option changes",
)
require(
"normalizeMtProxyTlsProfileOption" in mtproxy_options
and "MT_PROXY_TLS_PROFILE_AUTO_ROTATE" in mtproxy_options
and "MT_PROXY_TLS_PROFILE_ANDROID_OKHTTP" in mtproxy_options,
"Native MtProxyOptions normalization must accept auto, auto-rotate, and the manual profile pool",
)
require(
"MtProxyOptions mtProxyOptions" in proxy_check_header
and "setOverrideProxy(std::string address, uint16_t port, std::string username, std::string password, std::string secret, const MtProxyOptions &options)" in header
and "connection->setOverrideProxy(proxyCheckInfo->address, proxyCheckInfo->port, proxyCheckInfo->username, proxyCheckInfo->password, proxyCheckInfo->secret, proxyCheckInfo->mtProxyOptions)" in manager_cpp,
"Proxy check override connections must carry the selected MtProxyOptions",
)
require(
"getFirefoxDefault" in cpp
and "getAndroidChromeDefault" in cpp
and "getChromeModernDefault" in cpp
and "getYandexDefault" in cpp
and "getFirefoxAndroidDefault" in cpp
and "getAndroidOkHttpDefault" in cpp
and "selectMtProxyTlsHello" in cpp,
"FakeTLS must expose Firefox, Android Chrome, Yandex, Firefox Android, and Android OkHttp ClientHello profiles through a selector",
)
require(
"currentEffectiveProxyTlsProfile = MtProxyAdaptivePolicy::resolveEffectiveTlsProfile" in cpp
and "TlsHello hello = selectMtProxyTlsHello(currentEffectiveProxyTlsProfile)" in cpp,
"FakeTLS handshake must instantiate ClientHello through the effective sticky/rotating profile selector",
)
require(balanced_scopes("getFirefoxDefault", "getDefault"), "Firefox ClientHello scopes must be balanced")
require(balanced_scopes("getDefault", "getAndroidChromeDefault"), "Android Chrome ClientHello scopes must be balanced")
require(balanced_scopes("getChromeModernDefault", "getFirefoxAndroidDefault"), "Chrome Modern ClientHello scopes must be balanced")
require(balanced_scopes("getFirefoxAndroidDefault", "getAndroidOkHttpDefault"), "Firefox Android ClientHello scopes must be balanced")
require(balanced_scopes("getAndroidOkHttpDefault", "getYandexDefault"), "Android OkHttp ClientHello scopes must be balanced")
require(balanced_scopes("getYandexDefault"), "Yandex ClientHello scopes must be balanced")
for profile_error in validate_profile_rendering():
require(False, profile_error)
yandex_body = function_body("getYandexDefault")
yandex_hello, yandex_error = render_profile(
"yandex", yandex_body, "example.com", ECH_TEST_LENGTHS[0], PADDING_TEST_TARGETS[0]
)
require(yandex_error is None, yandex_error or "Yandex ClientHello rendering failed")
if yandex_error is None:
require(
ja4_for_client_hello(yandex_hello) == "t13d1516h2_8daaf6152771_d8a2da3f94cd",
"Yandex wire template must keep the working tdesktop JA4",
)
require(
'Op::grease(3)' in yandex_body
and 'Op::string("\\x00\\x00", 2)' in yandex_body
and 'Op::string("\\x00\\x01\\x00", 3)' not in yandex_body,
"Yandex final GREASE extension must stay deliberately empty",
)
require(
not re.search(r"\bTlsHello\s+TlsHello::pickProfile\s*\(", cpp),
"FakeTLS profile wrapper must stay out of the active transport path",
)
require("randomizeGrease" not in cpp and "randomizeGrease" not in header, "per-connection GREASE rewrite must stay disabled")
require(
"grease[i - 1]" in cpp and "grease[i + 1]" not in cpp,
"GREASE initialization must not read past the fixed GREASE array",
)
require(
"validateServerCompatibleHello" in cpp
and "mtProxyCheckClientHelloContract" in cpp
and "MT_PROXY_CANONICAL_CLIENT_HELLO_BYTES = 517" in client_hello_policy_h
and "MT_PROXY_MAX_RELAY_CLIENT_HELLO_BYTES = 4096" in client_hello_policy_h
and "mtProxyRelayGreaseCipher" in client_hello_policy_cpp
and "SessionIdNot32Bytes" in client_hello_policy_cpp
and "SniMismatch" in client_hello_policy_cpp
and "mtproxy/MtProxyClientHelloPolicy.cpp" in cmake,
"Each selected ClientHello must pass the extracted relay-contract guard",
)
require(
"mtproxy_startup profile" in cpp,
"MTProxy diagnostics must log selected FakeTLS profile",
)
require(
"logClientHelloFingerprint" in cpp
and "mtproxy_startup client_hello_fingerprint" in cpp
and "grease_values=" in cpp
and "grease_exts=" in cpp
and "has_4469=%d" in cpp
and "has_44cd=%d" in cpp,
"FakeTLS diagnostics must log a compact live ClientHello fingerprint",
)
require(
"MT_PROXY_TLS_PROFILE_CHROME_MODERN" in mtproxy_options
and "MT_PROXY_TLS_PROFILE_CHROME_MODERN" in cpp,
"native MTProxy options and selector must know Chrome Modern profile",
)
require(
"pendingClientHello" in combined
and "sendPendingClientHello" in combined
and "client_hello_send_progress" in cpp
and "stateMachine.sendBytes(pendingClientHello->bytes + pendingClientHelloOffset" in cpp,
"ClientHello must keep a pending buffer until the whole FakeTLS hello is sent",
)
require(
"send(socketFd, tempBuffer->bytes, size, 0)" not in cpp,
"ClientHello must not be sent through a single unchecked send()",
)
require(
"nextMtProxyTlsRecordPayloadSize" in data_path_shaper_cpp
and "uint32_t cap = 2878" in data_path_shaper_cpp
and "MtProxyRecordSizingDecision sizingDecision = nextMtProxyTlsRecordPayloadSize" in cpp
and "remaining > sizingDecision.payloadSize" in cpp,
"wrapped data path must keep the original cap as runtime-off default and apply sizing only through the explicit helper",
)
require(
"nextTlsRecordSize" not in combined
and "tlsRecordRemaining" not in combined
and "currentRecordSizingMode" in combined,
"old continuation-style dynamic record sizing must stay removed from the transport path",
)
require(
"nanosleep(&ts, nullptr);" not in cpp,
"proxy startup scheduling must not block the network thread",
)
require(
"scheduleProxyHandshakeAdmissionIfNeeded" in combined
and "cancelProxyHandshakeAdmission" in combined
and "Timer *timer" in machine_header
and '#include "Timer.h"' in cpp,
"proxy startup scheduling must use a cancellable nonblocking Timer",
)
require(
"MT_PROXY_HANDSHAKE_ADMISSION_ENABLED" not in cpp
and "mtProxyHandshakeSchedulerUsesAdmission" in cpp
and "admission_disabled" in cpp,
"FakeTLS endpoint admission controller must be controlled by the runtime connection-pattern setting",
)
require(
"MtProxyHandshakeEndpointState" in handshake_scheduler_cpp
and "proxyHandshakeSchedulerMutex" in handshake_scheduler_cpp
and "activeHandshakes" in handshake_scheduler_cpp
and "cooldownUntil" in handshake_scheduler_cpp
and "queuedRequests" in handshake_scheduler_cpp
and "MtProxyHandshakeAdmissionRequest" in handshake_scheduler_h
and "mtProxyHandshakeSchedulerAdmit" in cpp
and "proxyHandshakeSchedulerMutex" not in cpp,
"FakeTLS startup must use the extracted endpoint-level admission controller, not a single global jitter timestamp",
)
require(
"lastProxyConnectTime" not in cpp
and "proxyJitterMutex" not in cpp,
"old global jitter-only scheduler must stay out of the active FakeTLS path",
)
require(
"setMtProxyHandshakePriority" in combined
and "mtProxyRequestClassForConnectionType" in connection_cpp
and "mtProxyHandshakePriorityForRequestClass" in connection_cpp
and "ConnectionTypeGenericMedia" in connection_cpp
and "ConnectionTypeProxy" in connection_cpp,
"FakeTLS admission must receive MTProto connection priority before opening the socket",
)
require(
"MT_PROXY_HANDSHAKE_PRIORITY_BYPASS" in handshake_scheduler_h
and "proxyHandshakeAdmissionPriority == MT_PROXY_HANDSHAKE_PRIORITY_BYPASS" in cpp
and "case ConnectionTypeProxy:" in connection_cpp
and "mtProxyHandshakePriority = MT_PROXY_HANDSHAKE_PRIORITY_BYPASS;" in connection_cpp,
"Proxy checks must bypass hard FakeTLS admission queue to avoid false dead-proxy results",
)
require(
"releaseProxyHandshakeAdmission(true" in cpp
and "server_hello_hmac_ok" in cpp,
"FakeTLS admission slot must be released as soon as server_hello_hmac_ok is reached",
)
require(
"releaseProxyHandshakeAdmission(false" in cpp
and "closeSocket" in cpp,
"FakeTLS admission slot must be released on disconnect/error/timeout",
)
require(
"proxyHandshakeClientHelloSentTime" in combined
and "markProxyHandshakeClientHelloSent" in combined
and "freeze" in cpp.lower(),
"FakeTLS admission must record ClientHello time and detect freezes",
)
require(
"MT_PROXY_HANDSHAKE_FREEZE_COOLDOWN_ENABLED" not in cpp
and "MT_PROXY_HANDSHAKE_QUIET_FREEZE_COOLDOWN_MAX_MS" in handshake_scheduler_cpp
and "MT_PROXY_HANDSHAKE_STRICT_FREEZE_COOLDOWN_MAX_MS" in handshake_scheduler_cpp
and "mtProxyClampCooldown" in handshake_scheduler_cpp
and "mtProxyApplyFreezeCooldown(MtProxyHandshakeEndpointState &state, int64_t now, int32_t mode)" in handshake_scheduler_cpp
and "clientHelloElapsed >= MT_PROXY_HANDSHAKE_FREEZE_TIMEOUT_MS" in cpp
and "admission_freeze_cooldown" in cpp
and "admission_freeze_observed" in cpp,
"FakeTLS freeze cooldown must be bounded and mode-aware so temporary bans do not become minute-scale waits",
)
require(
"MT_PROXY_HANDSHAKE_CLOSE_ON_FREEZE_ENABLED = true" in cpp
and "server_hello_timeout_close" in cpp
and "closeSocket(1, ETIMEDOUT)" in cpp,
"FakeTLS server-hello freezes must close the dead socket instead of waiting for the generic timeout",
)
require(
"faketls_server_hello_wait_timeout" in cpp
and "admission_freeze_detected" not in cpp
and "didPauseDuringProxyServerHelloWait" in cpp
and "lastMonotonicPauseTime" in cpp
and '"background_handshake_aborted"' in cpp
and 'releaseProxyHandshakeAdmission(false, pausedDuringHandshake ? "background_handshake_aborted" : "freeze_timeout")' in cpp
and '"background_handshake_aborted"' in (ROOT / "TMessagesProj/jni/mtproxy/MtProxyPhaseClassification.h").read_text(encoding="utf-8"),
"FakeTLS ServerHello wait timeout must be named accurately and screen-off/background aborts must stay local",
)
require(
"recv_eof" in cpp
and "closeSocket(1, 0)" in cpp,
"TCP EOF must close the socket immediately instead of waiting for the generic timeout",
)
require(
"err == EINTR" in cpp
and "continue;" in cpp,
"FakeTLS send/recv loops must retry EINTR instead of treating it as a socket failure",
)
require(
"pending_hello=%u/%u" in cpp
and "first_tls_sent=%d first_tls_recv=%d" in cpp,
"MTProxy disconnect diagnostics must include pending ClientHello progress and first post-handshake TLS activity",
)
require(
"pacingDeferred" not in combined,
"old deferred pacing path must stay out of MTProxy connect",
)
require(
"mtproxy_startup connect_start" in cpp
and "mtproxy_startup socket_connected" in cpp
and "mtproxy_startup client_hello_sent" in cpp
and "mtproxy_startup server_hello_hmac_ok" in cpp
and "mtproxy_startup on_connected" in cpp
and "mtproxy_disconnect" in cpp,
"MTProxy startup diagnostics must cover connect, TLS handshake, connected, and disconnect",
)
require(
"MtProxyEndpointPolicy::extractSslipIpv4Address" in cpp
and '".sslip.io"' in MTPROXY_OPTIONS.with_name("MtProxyEndpointPolicy.cpp").read_text(encoding="utf-8", errors="replace")
and "mtproxy_startup resolved_sslip" in cpp,
"proxy host resolution must bypass DNS for literal x.x.x.x.sslip.io hosts",
)
require(
"mtProxySecretKindName" in secret_domain_h
and "const char *mtProxySecretKindName" in secret_domain_cpp
and "secret_kind=%s" in cpp
and "is_faketls=%d" in cpp,
"MTProxy logs must classify secrets so JA4/FakeTLS diagnosis is limited to ee secrets",
)
forbidden_connection_socket_helpers = (
"mtProxyPunycodeDomain",
"mtProxyPunycodeLabel",
"mtProxyUtf8ToCodepoints",
"buildMtProxySecretDomainPlan",
"sanitizeMtProxySecretDomain",
"validateMtProxySecretDomain",
)
require(
not any(connection_socket_defines(name) for name in forbidden_connection_socket_helpers)
and "struct MtProxySecretDomainPlan" not in cpp
and "struct MtProxySecretDomainPlan" in secret_domain_h
and "buildMtProxySecretDomainPlan" in secret_domain_cpp
and "plan.originalDomain = rawDomain" in secret_domain_cpp
and "SNI_OPTIONAL_NO_SNI may" in secret_domain_cpp
and "validateMtProxySecretDomain" in secret_domain_cpp
and "MtProxyPhase::SecretParseInvalidDomainControlChar" in secret_domain_cpp
and "MtProxyPhase::SecretParseInvalidDomain" in secret_domain_cpp
and 'SecretParseInvalidDomainControlChar = "secret_parse_invalid_domain_control_char"' in phase_contract
and 'SecretParseInvalidDomain = "secret_parse_invalid_domain"' in phase_contract
and "mtproxy/MtProxySecretDomain.cpp" in cmake,
"exact secret-domain planning must live in MtProxySecretDomain and stay out of ConnectionSocket.cpp",
)
require(
"mtProxyDisconnectReasonName" in cpp
and "reason_text=%s" in cpp
and "timeout_waiting_connect_or_pending_requests" in cpp,
"MTProxy disconnect logs must decode reason=2 as a timeout boundary",
)
require(
"pendingTlsFrame" in combined
and "sendPendingTlsFrame" in combined
and "clearPendingTlsFrame" in combined,
"TLS writes must keep a pending frame for partial-send handling",
)
require(
"pendingTlsFrameOffset += (uint32_t) sentLength;\n lastEventTime = ConnectionsManager::getInstance(instanceNum).getCurrentTimeMonotonicMillis();" in cpp,
"TLS pending-frame sends must refresh lastEventTime so active writes are not timed out as idle",
)
require(
"struct MtProxyServerFlightParseResult" in server_flight_parser_h
and "mtProxyParseServerHelloFlight" in server_flight_parser_cpp
and "mtProxyVerifyServerHelloHmac" in server_flight_parser_cpp
and "mtProxyVerifyServerHelloHmac" not in cpp
and "MtProxyServerHelloParseResult" not in cpp
and "MtProxyServerFlightParseResult parseResult = mtProxyParseServerHelloFlight" in cpp
and "mtproxy/MtProxyServerFlightParser.cpp" in cmake
and "TLS server hello hmac wait" in cpp
and "TLS server hello wait for tail data" in cpp
and "server_hello_hmac_timeout" in cpp,
"FakeTLS ServerHello HMAC verification must be TLS-record-aware and tolerate profiled telemt tail records",
)
require(
"MT_PROXY_HANDSHAKE_TIMER_SERVER_HELLO" in cpp
and "MT_PROXY_SERVER_HELLO_HMAC_WAIT_MS" in cpp
and "markProxyServerHelloHmacTimeoutIfNeeded" in combined
and "serverHelloHmacMismatchTime" in combined,
"FakeTLS ServerHello HMAC mismatch must have a short timeout independent of the admission queue",
)
require(
"TLS response ChangeCipherSpec skipped" in cpp
and "TLS response empty application data skipped" in cpp
and "mtproxy_disconnect tls_alert" in cpp
and "tlsBufferRecordType" in combined,
"post-handshake FakeTLS reader must skip control records and never pass empty TLS records into MTProto",
)
require(
"mtproxy_startup first_tls_app_sent" in cpp
and "mtproxy_startup first_tls_app_recv" in cpp
and "mtproxyFirstTlsFrameSentLogged" in combined
and "mtproxyFirstTlsFrameSentTime" in combined
and "mtproxyFirstTlsDataReceivedLogged" in combined,
"FakeTLS diagnostics must mark the first post-handshake MTProto TLS write and read",
)
timeout_start = cpp.find("bool ConnectionSocket::checkTimeout")
timeout_end = cpp.find("bool ConnectionSocket::hasTlsHashMismatch", timeout_start)
timeout_block = cpp[timeout_start:timeout_end]
require(
"MT_PROXY_TLS_APPDATA_NO_RESPONSE_TIMEOUT_MS" in cpp
and "mtproxy_tls_appdata_no_response_timeout" in timeout_block
and "currentSecretIsFakeTls" in timeout_block
and "mtproxyFirstTlsFrameSentLogged" in timeout_block
and "!mtproxyFirstTlsDataReceivedLogged" in timeout_block
and ('proxyCheckDiagnostic == "post_handshake_no_appdata"' in timeout_block
or "proxyCheckDiagnostic == MtProxyPhase::PostHandshakeNoAppdata" in timeout_block),
"FakeTLS must not stay half-connected forever after first MTProto TLS data is sent without a server response",
)
if errors:
print("MTProxy FakeTLS path check failed:")
for error in errors:
print(f"- {error}")
return 1
print("MTProxy FakeTLS path check passed")
return 0
if __name__ == "__main__":
sys.exit(main())