560 lines
29 KiB
Python
560 lines
29 KiB
Python
#!/usr/bin/env python3
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
|
|
from mtproxy_phase_contract import java_phase_names, java_policy, native_phase_names
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SOCKET = ROOT / "TMessagesProj/jni/tgnet/ConnectionSocket.cpp"
|
|
ENDPOINT_POLICY = ROOT / "TMessagesProj/jni/mtproxy/MtProxyEndpointPolicy.cpp"
|
|
ENDPOINT_POLICY_H = ROOT / "TMessagesProj/jni/mtproxy/MtProxyEndpointPolicy.h"
|
|
PROBE_COORDINATOR = ROOT / "TMessagesProj/jni/mtproxy/MtProxyProbeCoordinator.cpp"
|
|
PROBE_COORDINATOR_H = ROOT / "TMessagesProj/jni/mtproxy/MtProxyProbeCoordinator.h"
|
|
ADAPTIVE_POLICY = ROOT / "TMessagesProj/jni/mtproxy/MtProxyAdaptivePolicy.cpp"
|
|
ADAPTIVE_POLICY_H = ROOT / "TMessagesProj/jni/mtproxy/MtProxyAdaptivePolicy.h"
|
|
SECRET_DOMAIN = ROOT / "TMessagesProj/jni/mtproxy/MtProxySecretDomain.cpp"
|
|
SERVER_FLIGHT_PARSER_H = ROOT / "TMessagesProj/jni/mtproxy/MtProxyServerFlightParser.h"
|
|
SERVER_FLIGHT_PARSER = ROOT / "TMessagesProj/jni/mtproxy/MtProxyServerFlightParser.cpp"
|
|
STATE_MACHINE_H = ROOT / "TMessagesProj/jni/tgnet/ConnectionSocketStateMachine.h"
|
|
DIAGNOSTICS = ROOT / "TMessagesProj/src/main/java/org/telegram/messenger/ProxyCheckDiagnostics.java"
|
|
PHASE_POLICY = ROOT / "TMessagesProj/src/main/java/org/telegram/messenger/ProxyPhasePolicy.java"
|
|
ENDPOINT_KEY = ROOT / "TMessagesProj/src/main/java/org/telegram/messenger/ProxyEndpointKey.java"
|
|
HEALTH = ROOT / "TMessagesProj/src/main/java/org/telegram/messenger/ProxyHealthStore.java"
|
|
RUNTIME_STORE = ROOT / "TMessagesProj/src/main/java/org/telegram/messenger/ProxyRuntimeStateStore.java"
|
|
VISIBLE_STORE = ROOT / "TMessagesProj/src/main/java/org/telegram/messenger/ProxyVisibleStateStore.java"
|
|
CONNECTION = ROOT / "TMessagesProj/jni/tgnet/Connection.cpp"
|
|
ANALYZER = ROOT / "Tools/analyze_mtproxy_markers.py"
|
|
CHECK_ALL = ROOT / "Tools/check_mtproxy_all.py"
|
|
STRINGS = ROOT / "TMessagesProj/src/main/res/values/strings.xml"
|
|
STRINGS_RU = ROOT / "TMessagesProj/src/main/res/values-ru/strings.xml"
|
|
NATIVE_PHASE_CONTRACT = ROOT / "TMessagesProj/jni/mtproxy/MtProxyPhaseContract.h"
|
|
HANDSHAKE_PLAN_H = ROOT / "TMessagesProj/jni/mtproxy/MtProxyHandshakePlan.h"
|
|
HANDSHAKE_PLAN = ROOT / "TMessagesProj/jni/mtproxy/MtProxyHandshakePlan.cpp"
|
|
RECOVERY_POLICY_H = ROOT / "TMessagesProj/jni/mtproxy/MtProxyRecoveryPolicy.h"
|
|
RECOVERY_POLICY = ROOT / "TMessagesProj/jni/mtproxy/MtProxyRecoveryPolicy.cpp"
|
|
ENDPOINT_RECORDER = ROOT / "TMessagesProj/jni/mtproxy/MtProxyEndpointRecorder.cpp"
|
|
|
|
RECIPE_FAILURES = {
|
|
"true_client_hello_timeout",
|
|
"faketls_server_hello_wait_timeout",
|
|
"server_closed_after_client_hello",
|
|
"client_hello_sent_no_server_hello",
|
|
"tls_alert_after_client_hello",
|
|
"short_tls_response_after_client_hello",
|
|
"unrecognized_response_after_client_hello",
|
|
"unrecognized_tls_response_after_client_hello",
|
|
"server_hello_hmac_mismatch",
|
|
}
|
|
LEGACY_OR_JAVA_ONLY_RECIPE_FAILURES = {
|
|
"true_client_hello_timeout",
|
|
"client_hello_sent_no_server_hello",
|
|
"unrecognized_tls_response_after_client_hello",
|
|
}
|
|
NO_BYTE_AFTER_CLIENT_HELLO_FAILURES = {
|
|
"true_client_hello_timeout",
|
|
"faketls_server_hello_wait_timeout",
|
|
"server_closed_after_client_hello",
|
|
"client_hello_sent_no_server_hello",
|
|
}
|
|
|
|
|
|
def read(path: Path) -> str:
|
|
return path.read_text(encoding="utf-8", errors="replace")
|
|
|
|
|
|
def require(condition: bool, message: str, failures: list[str]) -> None:
|
|
if not condition:
|
|
failures.append(message)
|
|
|
|
|
|
def block(source: str, start: str, end: str) -> str:
|
|
start_index = source.find(start)
|
|
if start_index < 0:
|
|
return ""
|
|
end_index = source.find(end, start_index + 1)
|
|
return source[start_index:end_index if end_index >= 0 else len(source)]
|
|
|
|
|
|
def method_body(source: str, signature: str) -> str:
|
|
start_index = source.find(signature)
|
|
if start_index < 0:
|
|
return ""
|
|
brace_index = source.find("{", start_index)
|
|
if brace_index < 0:
|
|
return ""
|
|
depth = 0
|
|
for index in range(brace_index, len(source)):
|
|
char = source[index]
|
|
if char == "{":
|
|
depth += 1
|
|
elif char == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return source[start_index:index + 1]
|
|
return source[start_index:]
|
|
|
|
|
|
def native_phase_constants(contract_h: str) -> dict[str, str]:
|
|
return {
|
|
value: name
|
|
for name, value in re.findall(r'constexpr const char \*([A-Za-z0-9_]+)\s*=\s*"([a-z0-9_]+)"', contract_h)
|
|
}
|
|
|
|
|
|
def has_phase(source: str, phase: str, native_constants: dict[str, str]) -> bool:
|
|
constant = native_constants.get(phase)
|
|
return f'"{phase}"' in source or (constant is not None and f"MtProxyPhase::{constant}" in source)
|
|
|
|
|
|
def main() -> int:
|
|
failures: list[str] = []
|
|
socket = read(SOCKET)
|
|
endpoint_policy = read(ENDPOINT_POLICY)
|
|
endpoint_policy_h = read(ENDPOINT_POLICY_H)
|
|
probe_coordinator = read(PROBE_COORDINATOR)
|
|
probe_coordinator_h = read(PROBE_COORDINATOR_H)
|
|
adaptive_policy = read(ADAPTIVE_POLICY)
|
|
adaptive_policy_h = read(ADAPTIVE_POLICY_H)
|
|
secret_domain = read(SECRET_DOMAIN)
|
|
server_flight_parser_h = read(SERVER_FLIGHT_PARSER_H)
|
|
server_flight_parser = read(SERVER_FLIGHT_PARSER)
|
|
state_machine_h = read(STATE_MACHINE_H)
|
|
diagnostics = read(DIAGNOSTICS)
|
|
phase_policy = read(PHASE_POLICY)
|
|
endpoint_key = read(ENDPOINT_KEY)
|
|
health = read(HEALTH)
|
|
runtime_store = read(RUNTIME_STORE)
|
|
visible_store = read(VISIBLE_STORE)
|
|
connection = read(CONNECTION)
|
|
analyzer = read(ANALYZER)
|
|
check_all = read(CHECK_ALL)
|
|
strings = read(STRINGS)
|
|
strings_ru = read(STRINGS_RU)
|
|
native_constants = native_phase_constants(read(NATIVE_PHASE_CONTRACT))
|
|
handshake_plan_h = read(HANDSHAKE_PLAN_H) if HANDSHAKE_PLAN_H.exists() else ""
|
|
handshake_plan = read(HANDSHAKE_PLAN) if HANDSHAKE_PLAN.exists() else ""
|
|
recovery_policy_h = read(RECOVERY_POLICY_H) if RECOVERY_POLICY_H.exists() else ""
|
|
recovery_policy = read(RECOVERY_POLICY) if RECOVERY_POLICY.exists() else ""
|
|
endpoint_recorder = read(ENDPOINT_RECORDER) if ENDPOINT_RECORDER.exists() else ""
|
|
recorder_failure = method_body(endpoint_recorder, "void MtProxyEndpointRecorder::recordFailure")
|
|
|
|
for phase in RECIPE_FAILURES | {"handshake_profiles_exhausted", "secret_parse_invalid_domain_control_char", "secret_parse_invalid_domain"}:
|
|
require(phase in java_phase_names(), f"phase contract must expose Java phase {phase}", failures)
|
|
if phase not in LEGACY_OR_JAVA_ONLY_RECIPE_FAILURES:
|
|
require(phase in native_phase_names(), f"phase contract must expose native phase {phase}", failures)
|
|
require(phase.upper() in diagnostics, f"ProxyCheckDiagnostics must define {phase}", failures)
|
|
require(phase in analyzer, f"analyzer must know {phase}", failures)
|
|
require(
|
|
"unsupported_for_current_client" in analyzer
|
|
and "legacy alias" in analyzer,
|
|
"analyzer must keep unsupported_for_current_client only as a legacy alias for old captures",
|
|
failures,
|
|
)
|
|
|
|
require("secret_domain_sanitized" in java_phase_names(), "phase contract must expose Java live phase secret_domain_sanitized", failures)
|
|
require("secret_domain_sanitized" in native_phase_names(), "phase contract must expose native live phase secret_domain_sanitized", failures)
|
|
require("SECRET_DOMAIN_SANITIZED" in diagnostics, "ProxyCheckDiagnostics must define secret_domain_sanitized", failures)
|
|
require("secret_domain_sanitized" in analyzer, "analyzer must know secret_domain_sanitized", failures)
|
|
|
|
require(
|
|
"mtproxy_tls_after_client_hello" in socket
|
|
and "hex=" in socket
|
|
and "record_len=" in socket
|
|
and "alert_level" in socket
|
|
and "alert_description" in socket,
|
|
"ConnectionSocket must log first post-ClientHello bytes with TLS record length and alert fields",
|
|
failures,
|
|
)
|
|
require(
|
|
"Probable TLS alert after ClientHello" in strings
|
|
and "Вероятный TLS alert после ClientHello" in strings_ru
|
|
and "probable TLS alert / non-ServerHello record" in analyzer,
|
|
"TLS alert wording must stay cautious until the raw post-ClientHello bytes are inspected",
|
|
failures,
|
|
)
|
|
require(
|
|
"mtProxyServerFlightLooksLikeTlsAlert" in socket
|
|
and '"tls_alert_after_client_hello"' in socket
|
|
and '"short_tls_response_after_client_hello"' in socket
|
|
and '"unrecognized_response_after_client_hello"' in socket
|
|
and '"unrecognized_tls_response_after_client_hello"' in probe_coordinator + adaptive_policy,
|
|
"ConnectionSocket must split alert, short, and unrecognized post-ClientHello responses while policy retains the legacy TLS alias",
|
|
failures,
|
|
)
|
|
require(
|
|
has_phase(socket, "faketls_server_hello_wait_timeout", native_constants)
|
|
and has_phase(socket, "server_closed_after_client_hello", native_constants)
|
|
and "bytesRead == 0" in block(socket, "void ConnectionSocket::markProxyHandshakeFreezeIfNeeded", "void ConnectionSocket::markProxyServerHelloHmacTimeoutIfNeeded"),
|
|
"ServerHello wait must split no-byte deadline from EOF-after-ClientHello instead of publishing true_client_hello_timeout",
|
|
failures,
|
|
)
|
|
|
|
require(
|
|
"enum class MtProxyRecoveryActionKind" in recovery_policy_h
|
|
and "struct MtProxyRecoveryAction" in recovery_policy_h
|
|
and "MtProxyRecoveryActionKind::AdvanceClientHelloOnly" in recovery_policy
|
|
and "MtProxyRecoveryActionKind::AdvanceParserAllowed" in recovery_policy
|
|
and "MtProxyRecoveryActionKind::PostHandshakeShapingBackoff" in recovery_policy,
|
|
"Stage 2 recovery policy must define typed recovery actions for cursor decisions",
|
|
failures,
|
|
)
|
|
for evidence, action in (
|
|
("PreTcpLocalWait", "IgnoreLocal"),
|
|
("DnsFailure", "KeepSameRecipeBackoff"),
|
|
("TcpFailure", "KeepSameRecipeBackoff"),
|
|
("NoBytesAfterClientHello", "AdvanceClientHelloOnly"),
|
|
("ServerBytesParserFailure", "AdvanceParserAllowed"),
|
|
("ServerHelloHmacMismatch", "AdvanceParserAllowed"),
|
|
("PostHandshakeNoAppData", "PostHandshakeShapingBackoff"),
|
|
("ConfigInvalidSecret", "TerminalConfigFailure"),
|
|
):
|
|
require(
|
|
f"MtProxyFailureEvidenceKind::{evidence}" in recovery_policy
|
|
and f"MtProxyRecoveryActionKind::{action}" in recovery_policy,
|
|
f"recovery policy must map {evidence} to {action}",
|
|
failures,
|
|
)
|
|
require(
|
|
"nextCursorForRecovery" in adaptive_policy_h
|
|
and "bool MtProxyAdaptivePolicy::nextCursorForRecovery" in adaptive_policy,
|
|
"adaptive policy must expose nextCursorForRecovery(...) as the typed cursor movement API",
|
|
failures,
|
|
)
|
|
# Defect B: a proxy that returned ZERO bytes after ClientHello is silent/unreachable,
|
|
# not incompatible. It must still count toward the FakeTLS terminal budget so repeated
|
|
# ClientHello attempts stop, while recipe cursor advancement stays separate and can be
|
|
# held for no-bytes failures.
|
|
require(
|
|
"context.responseBytes = bytesRead" in socket
|
|
and "bool silentAfterClientHello = context.responseBytes == 0" in recorder_failure
|
|
and "evidenceKind == MtProxyFailureEvidenceKind::NoBytesAfterClientHello" in recorder_failure
|
|
and "bool budgetEligible = context.fakeTls" in recorder_failure
|
|
and "MtProxyProbeCoordinator::failureCountsTowardHandshakeBudget(phase, context.responseSignature)" in recorder_failure
|
|
and "bool recipeAdvanceAllowed = !silentAfterClientHello" in recorder_failure
|
|
and "MtProxyProbeCoordinator::completeFailure(" in recorder_failure
|
|
and "silent_after_client_hello" in recorder_failure,
|
|
"zero-bytes-after-ClientHello failures must count toward terminal budget while recipe advancement remains separately gated",
|
|
failures,
|
|
)
|
|
recipe_body = block(probe_coordinator, "bool MtProxyProbeCoordinator::failureNeedsRecipe", "MtProxyAdaptivePolicy::RecipeCursor MtProxyProbeCoordinator::recipeCursorForProbe")
|
|
adaptive_recipe_body = block(adaptive_policy, "bool MtProxyAdaptivePolicy::failureNeedsRecipe", "int32_t MtProxyAdaptivePolicy::compatibilityTlsProfile")
|
|
next_cursor_body = block(adaptive_policy, "bool MtProxyAdaptivePolicy::nextCursor(RecipeCursor *cursor", "static int32_t greaseProbeTlsProfile")
|
|
next_cursor_for_recovery_body = block(adaptive_policy, "bool MtProxyAdaptivePolicy::nextCursorForRecovery", "bool MtProxyAdaptivePolicy::nextCursor(RecipeCursor *cursor")
|
|
cooldown_body = block(endpoint_policy, "bool MtProxyEndpointPolicy::failureNeedsCooldown", "int64_t MtProxyEndpointPolicy::cooldownMs")
|
|
for phase in RECIPE_FAILURES:
|
|
require(phase in recipe_body, f"native probe coordinator must treat {phase} as a recipe failure", failures)
|
|
require(phase in adaptive_recipe_body, f"native adaptive policy compatibility wrapper must still recognize recipe phase {phase}", failures)
|
|
if phase not in LEGACY_OR_JAVA_ONLY_RECIPE_FAILURES:
|
|
require(not has_phase(cooldown_body, phase, native_constants), f"{phase} must not cooldown/quarantine the endpoint directly", failures)
|
|
require(
|
|
"post_handshake_no_appdata" not in recipe_body
|
|
and "post_handshake_no_appdata" not in adaptive_recipe_body,
|
|
"post_handshake_no_appdata must be PostHandshakeShapingBackoff, not a recipe cursor trigger",
|
|
failures,
|
|
)
|
|
for phase in ("secret_parse_invalid_domain_control_char", "secret_parse_invalid_domain"):
|
|
require(has_phase(cooldown_body, phase, native_constants), f"native endpoint policy must quarantine invalid secret phase {phase}", failures)
|
|
require(
|
|
has_phase(cooldown_body, "handshake_profiles_exhausted", native_constants)
|
|
and '"handshake_profiles_exhausted"' not in recipe_body,
|
|
"only recipe exhaustion should become a recovery endpoint failure",
|
|
failures,
|
|
)
|
|
require(
|
|
"recipeExhausted" in probe_coordinator_h
|
|
and "workingCursor" in probe_coordinator
|
|
and "workingRecipe" in probe_coordinator
|
|
and "cachedCursor" in probe_coordinator_h,
|
|
"probe coordinator must track failed recipe exhaustion and cache the full working recipe descriptor",
|
|
failures,
|
|
)
|
|
require(
|
|
has_phase(endpoint_recorder, "handshake_profiles_exhausted", native_constants)
|
|
and "recipe_exhausted" in endpoint_recorder,
|
|
"MtProxyEndpointRecorder must publish handshake_profiles_exhausted after recipe exhaustion",
|
|
failures,
|
|
)
|
|
require(
|
|
'next=unsupported_for_current_client' not in socket
|
|
and 'proxyCheckDiagnostic = "unsupported_for_current_client"' not in socket,
|
|
"ConnectionSocket must not publish unsupported_for_current_client from the active recipe-exhaustion path",
|
|
failures,
|
|
)
|
|
|
|
parser_variant_body = block(adaptive_policy, "static bool serverHelloParserVariantAllowed", "uint32_t MtProxyAdaptivePolicy::sniVariantMask")
|
|
require(
|
|
"MtProxyRecoveryAction action" in parser_variant_body
|
|
and "MtProxyRecoveryActionKind::AdvanceParserAllowed" in parser_variant_body
|
|
and "diagnostic" not in parser_variant_body,
|
|
"parser variant allowance must be recovery-action gated, not diagnostic-string gated",
|
|
failures,
|
|
)
|
|
for phase in NO_BYTE_AFTER_CLIENT_HELLO_FAILURES:
|
|
require(
|
|
phase not in parser_variant_body,
|
|
f"{phase} must keep parserVariant == PARSER_STANDARD_HMAC because no server bytes were available",
|
|
failures,
|
|
)
|
|
require(
|
|
"true_client_hello_timeout" not in parser_variant_body
|
|
and has_phase(socket, "faketls_server_hello_wait_timeout", native_constants)
|
|
and "server_closed_after_client_hello" in block(socket, "if (proxyAuthState == 11 && proxyHandshakeClientHelloSentTime != 0 && bytesRead == 0)", "closeSocket(1, 0);"),
|
|
"no-byte-after-ClientHello phases must stay isolated from parser variants",
|
|
failures,
|
|
)
|
|
require(
|
|
"post_handshake_no_appdata" not in parser_variant_body,
|
|
"post_handshake_no_appdata must not be part of the ServerHello parser cross-product expansion",
|
|
failures,
|
|
)
|
|
|
|
ladder_body = block(adaptive_policy, "static std::vector<MtProxyAdaptivePolicy::RecipeCursor> buildRecipeCursorLadder", "MtProxyAdaptivePolicy::RecipeCursor MtProxyAdaptivePolicy::initialCursor")
|
|
require(
|
|
"serverHelloParserVariantAllowed(action)" in ladder_body
|
|
and "parserLimit" in ladder_body
|
|
and "PARSER_STANDARD_HMAC + 1" in ladder_body,
|
|
"cursor ladder must restrict parser variants through recovery action evidence",
|
|
failures,
|
|
)
|
|
require(
|
|
"mtProxyRecoveryActionAdvancesRecipe(action)" in next_cursor_for_recovery_body
|
|
and "buildRecipeCursorLadder(allowedSniVariants, classicFallbackAllowed, action)" in next_cursor_for_recovery_body
|
|
and "MtProxyRecoveryAction action = mtProxyRecoveryActionForPhase(diagnostic, 0)" in next_cursor_body,
|
|
"nextCursor must be a compatibility wrapper around nextCursorForRecovery and recovery action gating",
|
|
failures,
|
|
)
|
|
require(
|
|
"MtProxyAdaptivePolicy::nextCursorForRecovery" in probe_coordinator
|
|
and "MtProxyAdaptivePolicy::nextCursor(&nextCursor, diagnostic" not in probe_coordinator,
|
|
"probe coordinator must move recipe cursors through typed recovery actions",
|
|
failures,
|
|
)
|
|
require(
|
|
"mtProxyRecoveryActionAdvancesRecipe(recoveryAction)" in recorder_failure,
|
|
"MtProxyEndpointRecorder must use recovery action before entering the recipe-failure path",
|
|
failures,
|
|
)
|
|
require(
|
|
"struct MtProxyServerFlightParseResult" in server_flight_parser_h
|
|
and "mtProxyParseServerHelloFlight" in server_flight_parser
|
|
and "mtProxyVerifyServerHelloHmac" in server_flight_parser
|
|
and "MtProxyServerHelloParseResult" not in socket
|
|
and "mtProxyVerifyServerHelloHmac" not in socket
|
|
and "MtProxyServerFlightParseResult parseResult = mtProxyParseServerHelloFlight" in socket,
|
|
"server-flight parsing and HMAC verification must live in MtProxyServerFlightParser, not ConnectionSocket",
|
|
failures,
|
|
)
|
|
|
|
require(
|
|
"RecipeCursor" in adaptive_policy_h
|
|
and "CompatibilityRecipe" in adaptive_policy_h
|
|
and "nextCursor" in adaptive_policy_h
|
|
and "recipeForCursor" in adaptive_policy_h,
|
|
"compatibility ladder must expose explicit cursor and recipe descriptor APIs instead of a fixed level count",
|
|
failures,
|
|
)
|
|
client_hello_prepare_block = block(socket, "if (proxyAuthState == 10)", "if (proxyAuthState == 11 && pendingClientHello != nullptr)")
|
|
require(
|
|
"struct MtProxyHandshakePlan" in handshake_plan_h
|
|
and "MtProxyAdaptivePolicy::RecipeCursor cursor" in handshake_plan_h
|
|
and "MtProxyAdaptivePolicy::CompatibilityRecipe recipe" in handshake_plan_h
|
|
and "std::string recipeId" in handshake_plan_h
|
|
and "mtProxyBuildHandshakePlan" in handshake_plan,
|
|
"Stage 2 must introduce an immutable MtProxyHandshakePlan selected once per FakeTLS attempt",
|
|
failures,
|
|
)
|
|
require(
|
|
"applyMtProxyPhaseAdaptiveRecipe();" not in client_hello_prepare_block
|
|
and "recipeCursorForProbe(currentMtProxyProbeKey)" not in client_hello_prepare_block
|
|
and "readGreaseProbeState(currentMtProxyProbeKey)" not in client_hello_prepare_block,
|
|
"ClientHello send path must use the selected immutable handshake plan instead of recalculating recipe/coordinator state",
|
|
failures,
|
|
)
|
|
require(
|
|
"currentRecipeFamily =" not in block(socket, "markProxyHandshakeClientHelloSent();", "void ConnectionSocket::writeBuffer")
|
|
and "currentRecipeSniVariant =" not in block(socket, "markProxyHandshakeClientHelloSent();", "void ConnectionSocket::writeBuffer")
|
|
and "currentRecipeParserVariant =" not in block(socket, "markProxyHandshakeClientHelloSent();", "void ConnectionSocket::writeBuffer"),
|
|
"recipe cursor fields must not mutate after client_hello_sent for the same attempt",
|
|
failures,
|
|
)
|
|
require(
|
|
"struct MtProxyRecipe" in adaptive_policy_h
|
|
and "transportMode" in adaptive_policy_h
|
|
and "tlsProfile" in adaptive_policy_h
|
|
and "fragmentClientHello" in adaptive_policy_h
|
|
and "useGrease" in adaptive_policy_h
|
|
and "useModernExtensions" in adaptive_policy_h
|
|
and "serverHelloParser" in adaptive_policy_h
|
|
and "sni" in adaptive_policy_h,
|
|
"adaptive policy must expose an explicit MtProxyRecipe identity",
|
|
failures,
|
|
)
|
|
require(
|
|
"recipeCacheKey" in endpoint_policy_h
|
|
and "ProbeKey" in probe_coordinator_h
|
|
and "currentMtProxyRecipeCacheKey" in state_machine_h
|
|
and "currentMtProxyRecipeCacheKey" in socket
|
|
and "mtProxySecretHashForRecipeKey" in socket,
|
|
"recipe cache must be keyed separately by host:port + secret_hash + SNI without changing the public live endpoint key",
|
|
failures,
|
|
)
|
|
require(
|
|
"probeKey.key = currentMtProxyProbeKey" in socket
|
|
and "recipeCursorForProbe(currentMtProxyProbeKey)" in socket
|
|
and "lastRecipeDiagnosticForProbe(currentMtProxyProbeKey)" in socket,
|
|
"recipe failure/adaptation must read and write the probe key, not the public endpoint key",
|
|
failures,
|
|
)
|
|
require(
|
|
"recipe_failed" in endpoint_recorder
|
|
and "next_family" in endpoint_recorder
|
|
and "next_sni_variant" in endpoint_recorder
|
|
and "next_parser_variant" in endpoint_recorder
|
|
and "next_classic_variant" in endpoint_recorder
|
|
and "recipe_id=" in endpoint_recorder
|
|
and "server_hello_parser=" in socket,
|
|
"MtProxyEndpointRecorder must log each recipe failure with the current recipe identity and next cursor",
|
|
failures,
|
|
)
|
|
require(
|
|
"CompatibilityRecipe MtProxyAdaptivePolicy::recipeForCursor" in adaptive_policy
|
|
and "MtProxyRecipe MtProxyAdaptivePolicy::recipeForResult" in adaptive_policy
|
|
and "std::string MtProxyAdaptivePolicy::recipeId" in adaptive_policy
|
|
and "standard_hmac_parser" in adaptive_policy
|
|
and "lenient_record_parser" in adaptive_policy
|
|
and "tolerate_fragmented_server_hello" in adaptive_policy,
|
|
"adaptive policy must derive a stable recipe id including named parser variants",
|
|
failures,
|
|
)
|
|
require(
|
|
"CLIENT_HELLO_CHROME_MODERN_SOFT_FRAGMENT" in adaptive_policy_h
|
|
and "CLIENT_HELLO_LEGACY_NO_GREASE_NO_MODERN_EXTENSIONS" in adaptive_policy_h
|
|
and "MT_PROXY_TLS_PROFILE_FIREFOX_ANDROID" in adaptive_policy
|
|
and "MT_PROXY_TLS_PROFILE_ANDROID_CHROME" in adaptive_policy,
|
|
"adaptive policy must try explicit ClientHello families and known-compatible TLS profiles",
|
|
failures,
|
|
)
|
|
require(
|
|
"SNI_OPTIONAL_NO_SNI" in adaptive_policy_h
|
|
and "experimentalNoSni" in adaptive_policy_h
|
|
and "optional_no_sni" in adaptive_policy,
|
|
"optional no-SNI must be an explicit experimental recipe output, not a separate probe key",
|
|
failures,
|
|
)
|
|
handshake_ok_body = block(probe_coordinator, "void MtProxyProbeCoordinator::completeSuccess", "void MtProxyProbeCoordinator::completeProfilesExhausted")
|
|
require(
|
|
"server_hello_hmac_ok" in handshake_ok_body
|
|
and "probeKey" in handshake_ok_body
|
|
and "state.workingCursor = state.cursor" in handshake_ok_body
|
|
and "state.workingRecipe = recipe" in handshake_ok_body,
|
|
"server_hello_hmac_ok must cache the full current working recipe for the exact probe key",
|
|
failures,
|
|
)
|
|
|
|
for phase in RECIPE_FAILURES:
|
|
require(phase in java_phase_names(), f"Java phase policy must classify {phase}", failures)
|
|
require(
|
|
java_policy("tls_alert_after_client_hello") == ("failure", "exact", False, False)
|
|
and "ProxyPhaseClassification.isKnownJavaPhase" in phase_policy,
|
|
"recipe failures must be visible but must not directly backoff/rotate the endpoint in Java",
|
|
failures,
|
|
)
|
|
require(
|
|
java_policy("handshake_profiles_exhausted") == ("failure", "exact", True, True)
|
|
and "terminalExactConfig" in phase_policy,
|
|
"handshake_profiles_exhausted must be a normal exact recovery failure with rotation hysteresis",
|
|
failures,
|
|
)
|
|
require(
|
|
"case ProxyCheckDiagnostics.HANDSHAKE_PROFILES_EXHAUSTED:" not in method_body(phase_policy, "public static boolean isOneShotTerminal")
|
|
and "case ProxyCheckDiagnostics.HANDSHAKE_PROFILES_EXHAUSTED:" not in method_body(phase_policy, "private static boolean isTerminalExactConfigPhase"),
|
|
"handshake_profiles_exhausted must not become terminalExactConfig or isOneShotTerminal in Java",
|
|
failures,
|
|
)
|
|
require(
|
|
"unsupported_for_current_client" not in java_phase_names()
|
|
or java_policy("unsupported_for_current_client")[0] != "neutral",
|
|
"unsupported_for_current_client must not remain a terminal exact-config Java verdict",
|
|
failures,
|
|
)
|
|
classification = read(ROOT / "TMessagesProj/jni/mtproxy/MtProxyPhaseClassification.h")
|
|
generated_backoff = block(classification, "inline bool needsReconnectBackoff", "inline bool isObservationFacadePhase")
|
|
require(
|
|
not has_phase(generated_backoff, "tls_alert_after_client_hello", native_constants)
|
|
and has_phase(generated_backoff, "handshake_profiles_exhausted", native_constants),
|
|
"connection reconnect backoff must wait for recipe exhaustion before endpoint-level backoff",
|
|
failures,
|
|
)
|
|
require(
|
|
"INVALID_SECRET_FAILURE_BACKOFF_MS = 15 * 60 * 1000L" in health
|
|
and "INVALID_SECRET_ROTATED_AWAY_HOLD_MS = 15 * 60 * 1000L" in health
|
|
and "rotatedAwayHoldMs(normalized)" in health
|
|
and "failureBackoffMs(state.lastDiagnostic" in health,
|
|
"existing Java endpoint health policy must reserve long exact-endpoint hold/backoff for invalid-secret phases",
|
|
failures,
|
|
)
|
|
require(
|
|
java_policy("secret_parse_invalid_domain_control_char") == ("failure", "exact", True, True)
|
|
and java_policy("secret_parse_invalid_domain") == ("failure", "exact", True, True),
|
|
"invalid secret-domain phases must backoff and rotate/quarantine the exact proxy config in Java",
|
|
failures,
|
|
)
|
|
require(
|
|
"ProxyHealthStore.isEndpointRotatedAway(proxyInfo, now)" in block(visible_store, "static boolean markConnectionUsable", "private static void promotePendingDnsVisiblePhase")
|
|
and "if (!ProxyVisibleStateStore.markConnectionUsable(proxyInfo, normalized, now, activationGeneration))" in block(runtime_store, "public static void markConnectionUsable", "public static void markEndpointCooldown")
|
|
and "ProxyHealthStore.clearEndpointBackoff(proxyInfo, normalized, now)" in block(runtime_store, "public static void markConnectionUsable", "public static void markEndpointCooldown")
|
|
and "source=usable_success" in visible_store,
|
|
"late usable-success callbacks from a rotated-away endpoint must not clear quarantine/backoff",
|
|
failures,
|
|
)
|
|
|
|
require(
|
|
"buildMtProxySecretDomainPlan" in socket
|
|
and "plan.originalDomain = rawDomain" in secret_domain
|
|
and "plan.canonicalDomain = plan.originalDomain" in secret_domain
|
|
and "SNI_ORIGINAL" in secret_domain
|
|
and has_phase(secret_domain, "secret_parse_invalid_domain_control_char", native_constants)
|
|
and "validateMtProxySecretDomain" in secret_domain,
|
|
"native FakeTLS setup must preserve and validate the exact secret SNI before ClientHello construction",
|
|
failures,
|
|
)
|
|
require(
|
|
"domainPlan.terminalDiagnostic != nullptr" in socket
|
|
and "MtProxyPhase::SecretParseInvalidDomainControlChar" in socket
|
|
and "closeSocket(1, -1)" in socket,
|
|
"native FakeTLS setup must reject an invalid/control-char secret instead of changing its wire SNI",
|
|
failures,
|
|
)
|
|
require(
|
|
"recordSecretDomainSanitized" in endpoint_policy
|
|
and "recordSecretDomainSanitized" in endpoint_policy_h,
|
|
"endpoint policy must deduplicate secret_domain_sanitized logs by endpoint key",
|
|
failures,
|
|
)
|
|
require(
|
|
"sanitizeSecretDomainForLiveStage" in endpoint_key
|
|
and "IDN.toASCII" in endpoint_key
|
|
and "Character.isISOControl" in endpoint_key,
|
|
"Java endpoint key must sanitize control chars and normalize IDN domains",
|
|
failures,
|
|
)
|
|
|
|
require(
|
|
'"check_mtproxy_compatibility_recipe.py"' in check_all,
|
|
"full MTProxy guard suite must include compatibility recipe guard",
|
|
failures,
|
|
)
|
|
|
|
if failures:
|
|
print("MTProxy compatibility recipe guard failed:", file=sys.stderr)
|
|
for failure in failures:
|
|
print(f" - {failure}", file=sys.stderr)
|
|
return 1
|
|
print("MTProxy compatibility recipe guard passed.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|