742 lines
38 KiB
Python
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())
|