ZaStoGram/Tools/check_proxy_usable_success_hold.py

581 lines
31 KiB
Python

#!/usr/bin/env python3
from pathlib import Path
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parents[1]
MESSENGER = ROOT / "TMessagesProj/src/main/java/org/telegram/messenger"
NATIVE = ROOT / "TMessagesProj/jni/tgnet"
ANALYZER = ROOT / "Tools/analyze_mtproxy_markers.py"
RUNTIME_LOG_VERIFIER = ROOT / "Tools/verify_mtproxy_runtime_logs.py"
LIVE_PHASES_HELD_BY_USABLE_SUCCESS = (
("dns_cache_hit", "DNS_CACHE_HIT"),
("dns_cache_store", "DNS_CACHE_STORE"),
("dns_coalesce_wait", "DNS_COALESCE_WAIT"),
("connect_start", "CONNECT_START"),
("socket_connect_start", "SOCKET_CONNECT_START"),
("tcp_connect_gate", "TCP_CONNECT_GATE"),
("socket_connected", "SOCKET_CONNECTED"),
("client_hello_sent", "CLIENT_HELLO_SENT"),
("server_hello_hmac_ok", "SERVER_HELLO_HMAC_OK"),
("on_connected", "ON_CONNECTED"),
("first_tls_app_sent", "FIRST_TLS_APP_SENT"),
("admission_queue", "ADMISSION_QUEUE"),
("endpoint_cooldown", "ENDPOINT_COOLDOWN"),
("host_resolve_start", "HOST_RESOLVE_START"),
)
USABLE_SUCCESS_PHASES = (
("first_tls_app_recv", "FIRST_TLS_APP_RECV"),
("first_mtproxy_packet_recv", "FIRST_MTPROXY_PACKET_RECV"),
)
PUNITIVE_FAILURE_PHASES = (
("tcp_not_connected", "TCP_NOT_CONNECTED"),
("tcp_connection_refused", "TCP_CONNECTION_REFUSED"),
("tcp_connect_timeout", "TCP_CONNECT_TIMEOUT"),
("host_resolve_failed", "HOST_RESOLVE_FAILED"),
("host_resolve_timeout", "HOST_RESOLVE_TIMEOUT"),
("handshake_profiles_exhausted", "HANDSHAKE_PROFILES_EXHAUSTED"),
("mtproxy_packet_sent_no_response", "MTPROXY_PACKET_SENT_NO_RESPONSE"),
("post_handshake_no_appdata", "POST_HANDSHAKE_NO_APPDATA"),
("dropped_early_after_appdata", "DROPPED_EARLY_AFTER_APPDATA"),
)
def read(path: Path) -> str:
return path.read_text(encoding="utf-8", errors="replace") if path.exists() else ""
def require(condition: bool, message: str, failures: list[str]) -> None:
if not condition:
failures.append(message)
def method_body(text: str, signature: str) -> str:
start = text.find(signature)
if start == -1:
return ""
brace = text.find("{", start)
if brace == -1:
return ""
depth = 0
for index in range(brace, len(text)):
char = text[index]
if char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return text[start:index + 1]
return text[start:]
def run_analyzer_shadow_check(failures: list[str]) -> None:
with tempfile.TemporaryDirectory() as tmp:
session = Path(tmp)
markers = session / "mtproxy_markers.txt"
markers.write_text(
"\n".join(
[
"logcat.txt:1: 06-25 20:31:30.000 connection(0x1) connecting via proxy sberbank.dns.army:45631 secret[34] secret_kind=ee",
"logcat.txt:2: 06-25 20:31:30.010 connection(0x1) mtproxy_startup connect_start profile=firefox_android address=sberbank.dns.army port=45631",
"logcat.txt:3: 06-25 20:31:30.020 connection(0x1) mtproxy_startup socket_connect_start",
"logcat.txt:4: 06-25 20:31:30.030 connection(0x1) mtproxy_startup socket_connected",
"logcat.txt:5: 06-25 20:31:30.040 connection(0x1) mtproxy_startup client_hello_sent bytes=2206",
"logcat.txt:6: 06-25 20:31:30.060 connection(0x1) mtproxy_startup server_hello_hmac_ok bytes=196 len1=122 len2=58 flight=58 extra=0",
"logcat.txt:7: 06-25 20:31:30.070 connection(0x1) mtproxy_startup on_connected tls=1",
"logcat.txt:8: 06-25 20:31:30.080 connection(0x1) mtproxy_startup first_tls_app_sent payload=244 frame=249",
"logcat.txt:9: 06-25 20:31:30.090 connection(0x1) mtproxy_startup first_tls_app_recv payload=1015",
"logcat.txt:10: 06-25 20:31:30.100 proxy_control decision=visible_usable_success source=native_stage account=0 phase=first_tls_app_recv endpoint=sberbank.dns.army:45631:ee:sberbank.dns.army",
"logcat.txt:11: 06-25 20:31:30.110 proxy_control decision=held_live_by_usable_success source=connect_start phase=connect_start endpoint=sberbank.dns.army:45631:ee:sberbank.dns.army held_by=first_tls_app_recv",
"logcat.txt:12: 06-25 20:31:30.120 proxy_control decision=held_live_by_usable_success source=native_stage account=0 phase=tcp_connect_gate endpoint=sberbank.dns.army:45631 held_by=first_tls_app_recv",
"logcat.txt:13: 06-25 20:31:30.200 connection(0x2) connecting via proxy sberbank.dns.army:45631 secret[34] secret_kind=ee",
"logcat.txt:14: 06-25 20:31:30.210 connection(0x2) mtproxy_startup endpoint_failure_shadowed_by_success key=sberbank.dns.army:45631 phase=tcp_not_connected reason=closeSocket hold_ms=44900",
"logcat.txt:15: 06-25 20:31:30.220 proxy_control decision=held_by_usable_success source=native_stage account=0 phase=tcp_not_connected endpoint=sberbank.dns.army:45631:ee:sberbank.dns.army held_by=first_tls_app_recv",
]
)
+ "\n",
encoding="utf-8",
)
result = subprocess.run(
[sys.executable, str(ANALYZER), str(markers), "--out-dir", str(session)],
cwd=ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
require(result.returncode == 0, result.stderr.strip() or result.stdout, failures)
require("ok: 1" in result.stdout, "analyzer must keep the proven usable attempt as ok", failures)
require("tcp_not_connected: 1" not in result.stdout, "shadowed sibling failure must not count as tcp_not_connected", failures)
require("endpoint_failure_shadowed_by_success" in result.stdout, "analyzer must preserve the shadow marker", failures)
require("held_by_usable_success" in result.stdout, "analyzer must preserve Java usable-success hold decisions", failures)
require("held_live_by_usable_success" in result.stdout, "analyzer must preserve Java live-stage hold decisions", failures)
def runtime_log_lines(proxy_control_tail: str) -> str:
return (
"\n".join(
[
"logcat.txt:1: 06-25 20:31:30.000 connection(0x1) mtproxy_disconnect transport_state=closed epoll_registered=0 admission_active=0 tcp_gate_active=0",
"logcat.txt:2: 06-25 20:31:30.010 connection(0x1) mtproxy_startup server_hello_hmac_ok bytes=196 len1=122 len2=58 flight=58 extra=0",
"logcat.txt:3: 06-25 20:31:30.020 connection(0x1) mtproxy_startup endpoint_handshake_ok reason=server_hello_hmac_ok",
"logcat.txt:4: 06-25 20:31:30.090 connection(0x1) mtproxy_startup first_tls_app_recv payload=1015",
"logcat.txt:5: 06-25 20:31:30.100 connection(0x1) mtproxy_startup endpoint_data_path_success network_key=sberbank.dns.army:45631 key=sberbank.dns.army:45631:ee:sberbank.dns.army reason=first_tls_app_recv",
"logcat.txt:6: 06-25 20:31:30.110 proxy_control decision=visible_usable_success source=native_stage account=0 phase=first_tls_app_recv endpoint=sberbank.dns.army:45631:ee:sberbank.dns.army",
proxy_control_tail,
]
)
+ "\n"
)
def run_runtime_log_visible_hold_check(failures: list[str]) -> None:
with tempfile.TemporaryDirectory() as tmp:
session = Path(tmp)
bad = session / "bad_markers.txt"
good = session / "good_markers.txt"
bad_connect_start = session / "bad_connect_start_markers.txt"
good_connect_start = session / "good_connect_start_markers.txt"
bad_failure_overwrite = session / "bad_failure_overwrite_markers.txt"
good_failure_shadow = session / "good_failure_shadow_markers.txt"
bad_failure_anchor = session / "bad_failure_anchor_markers.txt"
bad.write_text(
runtime_log_lines(
"logcat.txt:7: 06-25 20:31:31.110 proxy_control decision=visible_only source=native_stage account=0 phase=tcp_connect_gate endpoint=sberbank.dns.army:45631"
),
encoding="utf-8",
)
good.write_text(
runtime_log_lines(
"logcat.txt:7: 06-25 20:31:31.110 proxy_control decision=held_live_by_usable_success source=native_stage account=0 phase=tcp_connect_gate endpoint=sberbank.dns.army:45631 held_by=first_tls_app_recv"
),
encoding="utf-8",
)
bad_connect_start.write_text(
runtime_log_lines(
"logcat.txt:7: 06-25 20:31:31.110 proxy_control decision=visible_only source=connect_start phase=connect_start endpoint=sberbank.dns.army:45631:ee:sberbank.dns.army"
),
encoding="utf-8",
)
good_connect_start.write_text(
runtime_log_lines(
"logcat.txt:7: 06-25 20:31:31.110 proxy_control decision=held_live_by_usable_success source=connect_start phase=connect_start endpoint=sberbank.dns.army:45631:ee:sberbank.dns.army held_by=first_tls_app_recv"
),
encoding="utf-8",
)
bad_failure_overwrite.write_text(
runtime_log_lines(
"logcat.txt:7: 06-25 20:31:31.110 proxy_control decision=visible_only source=native_stage account=0 phase=client_hello_sent_no_server_hello endpoint=sberbank.dns.army:45631:ee:sberbank.dns.army"
),
encoding="utf-8",
)
good_failure_shadow.write_text(
runtime_log_lines(
"logcat.txt:7: 06-25 20:31:31.110 proxy_control decision=shadowed_by_usable_success source=native_stage account=0 phase=client_hello_sent_no_server_hello endpoint=sberbank.dns.army:45631:ee:sberbank.dns.army held_by=first_tls_app_recv"
),
encoding="utf-8",
)
bad_failure_anchor.write_text(
runtime_log_lines(
"logcat.txt:7: 06-25 20:31:31.110 proxy_control decision=held_by_usable_success source=native_stage account=0 phase=client_hello_sent_no_server_hello endpoint=sberbank.dns.army:45631:ee:sberbank.dns.army held_by=client_hello_sent_no_server_hello"
),
encoding="utf-8",
)
bad_result = subprocess.run(
[sys.executable, str(RUNTIME_LOG_VERIFIER), str(bad)],
cwd=ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
require(
bad_result.returncode != 0
and "visible usable success overwritten by live visible_only" in bad_result.stderr,
"runtime log verifier must fail when a fresh usable success is overwritten by a live visible_only phase within 45s",
failures,
)
good_result = subprocess.run(
[sys.executable, str(RUNTIME_LOG_VERIFIER), str(good)],
cwd=ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
require(
good_result.returncode == 0,
good_result.stderr.strip() or "runtime log verifier must allow held_live_by_usable_success after visible usable success",
failures,
)
bad_connect_start_result = subprocess.run(
[sys.executable, str(RUNTIME_LOG_VERIFIER), str(bad_connect_start)],
cwd=ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
require(
bad_connect_start_result.returncode != 0
and "visible usable success overwritten by live visible_only" in bad_connect_start_result.stderr,
"runtime log verifier must fail when Java connect_start overwrites a fresh usable success within 45s",
failures,
)
good_connect_start_result = subprocess.run(
[sys.executable, str(RUNTIME_LOG_VERIFIER), str(good_connect_start)],
cwd=ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
require(
good_connect_start_result.returncode == 0,
good_connect_start_result.stderr.strip() or "runtime log verifier must allow held_live_by_usable_success for Java connect_start after visible usable success",
failures,
)
bad_failure_result = subprocess.run(
[sys.executable, str(RUNTIME_LOG_VERIFIER), str(bad_failure_overwrite)],
cwd=ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
require(
bad_failure_result.returncode != 0
and "visible usable success overwritten by failure visible_only" in bad_failure_result.stderr,
"runtime log verifier must fail when a fresh usable success is overwritten by a failure visible_only phase within 45s",
failures,
)
good_failure_result = subprocess.run(
[sys.executable, str(RUNTIME_LOG_VERIFIER), str(good_failure_shadow)],
cwd=ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
require(
good_failure_result.returncode == 0,
good_failure_result.stderr.strip() or "runtime log verifier must allow shadowed_by_usable_success for post-success handshake failures",
failures,
)
bad_anchor_result = subprocess.run(
[sys.executable, str(RUNTIME_LOG_VERIFIER), str(bad_failure_anchor)],
cwd=ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
require(
bad_anchor_result.returncode != 0
and "fresh usable hold anchored to failure phase" in bad_anchor_result.stderr,
"runtime log verifier must fail when held_by is a failure diagnostic instead of a usable phase",
failures,
)
def main() -> int:
failures: list[str] = []
store = read(MESSENGER / "ProxyRuntimeStateStore.java")
reducer = read(MESSENGER / "ProxyEventReducer.java")
visible = read(MESSENGER / "ProxyVisibleStateStore.java")
health = read(MESSENGER / "ProxyHealthStore.java")
rotation = read(MESSENGER / "ProxyRotationController.java")
engine = read(MESSENGER / "ProxyRotationEngine.java")
policy_h = read(NATIVE.parent / "mtproxy/MtProxyEndpointPolicy.h")
policy_cpp = read(NATIVE.parent / "mtproxy/MtProxyEndpointPolicy.cpp")
socket = read(NATIVE / "ConnectionSocket.cpp")
analyzer = read(ANALYZER)
runtime_verifier = read(RUNTIME_LOG_VERIFIER)
all_checks = read(ROOT / "Tools/check_mtproxy_all.py")
require("public static boolean hasFreshUsableSuccess" in store, "runtime store must expose fresh usable-success state", failures)
require("public static long usableSuccessRemainingMs" in store, "runtime store must expose remaining usable-success hold", failures)
require("static long usableSuccessRemainingMs" in health, "health store must expose usable-success remaining time", failures)
require(
"return onRuntimeEvent(event)" in method_body(store, "public static Decision onNativeStage")
and "return ProxyEventReducer.reduce(event)" in method_body(store, "public static Decision onRuntimeEvent"),
"ProxyRuntimeStateStore.onNativeStage must delegate through the unified runtime-event reducer",
failures,
)
native_stage = method_body(reducer, "static ProxyRuntimeStateStore.Decision reduce")
helper = method_body(visible, "static boolean shouldHoldLivePhaseByUsableSuccess")
live_hold_idx = native_stage.find("decision=held_live_by_usable_success")
connected_hold_idx = native_stage.find("decision=held_live_by_current_proxy_usable")
visible_write_idx = native_stage.find("if (selectedAccountStage && visibleOwner && verdict.canOverwriteVisible)")
helper_call_idx = native_stage.find("ProxyVisibleStateStore.shouldHoldLivePhaseByUsableSuccess(currentProxy, event)")
require(
helper
and "String phase = ProxyCheckDiagnostics.normalize(event.phase)" in helper
and "ProxyHealthStore.hasFreshUsableSuccess(proxyInfo, event.timestamp)" in helper
and "ProxyPhasePolicy.isLivePhase(phase)" in helper
and "ProxyPhasePolicy.isProxyUsableSuccessPhase(phase)" in helper,
"visible state store must keep usable-success live telemetry hold in a dedicated shouldHoldLivePhaseByUsableSuccess helper",
failures,
)
require(
live_hold_idx >= 0
and visible_write_idx >= 0
and helper_call_idx >= 0
and helper_call_idx < visible_write_idx
and live_hold_idx < visible_write_idx
and 'return new ProxyRuntimeStateStore.Decision("held_live_by_usable_success"' in native_stage,
"fresh usable success must hold later live pre-TCP native stages before they can overwrite visible state",
failures,
)
shadow_failure_idx = native_stage.find("ProxyVisibleStateStore.shouldShadowFailureByUsableSuccess(currentProxy, event)")
shadow_decision_idx = native_stage.find("decision=shadowed_by_usable_success")
require(
"static boolean shouldShadowFailureByUsableSuccess" in visible
and shadow_failure_idx >= 0
and shadow_decision_idx >= 0
and visible_write_idx >= 0
and shadow_failure_idx < visible_write_idx
and shadow_decision_idx < visible_write_idx
and 'return new ProxyRuntimeStateStore.Decision("shadowed_by_usable_success"' in native_stage,
"fresh usable success must shadow later failure phases before any visible write",
failures,
)
fresh_failure_hold_idx = native_stage.find("ProxyVisibleStateStore.shouldHoldVisiblePhaseByFreshFailure(currentProxy, event)")
fresh_failure_decision_idx = native_stage.find('return new ProxyRuntimeStateStore.Decision("held_by_fresh_failure"')
mirror_visible_idx = native_stage.find("ProxyVisibleStateStore.mirrorVisiblePhaseIfAllowed(currentProxy, event")
require(
"static boolean shouldHoldVisiblePhaseByFreshFailure" in visible
and fresh_failure_hold_idx >= 0
and fresh_failure_decision_idx >= 0
and mirror_visible_idx >= 0
and visible_write_idx >= 0
and fresh_failure_hold_idx < mirror_visible_idx
and fresh_failure_decision_idx < mirror_visible_idx,
"fresh failure hold must return held_by_fresh_failure before a retry live phase can become visible_only",
failures,
)
require(
"POST_SUCCESS_DATA_PATH_SHADOWS" in health
and "postSuccessDataPathShadowCount" in health
and "rememberPostSuccessDataPathShadow" in health
and "shouldHoldFailureByUsableSuccess" in health
and "failureUsesPostSuccessShadowBudget" in health
and "ProxyCheckDiagnostics.SHADOWED_SOCKET_FAILURE.equals(normalizedPhase)" in native_stage,
"Java usable-success hold must spend a bounded post-success data-path shadow budget, including native shadowed_socket_failure",
failures,
)
require(
"static String lastUsablePhase" in health
and "ProxyPhasePolicy.isProxyUsableSuccessPhase" in health
and "ProxyHealthStore.lastUsablePhase" in visible
and "static String heldByUsablePhase" in visible,
"usable-success holds must anchor held_by to ProxyHealthStore.lastUsablePhase instead of the mutable visible diagnostic",
failures,
)
phase_policy = read(MESSENGER / "ProxyPhasePolicy.java")
for phase, constant in LIVE_PHASES_HELD_BY_USABLE_SUCCESS:
require(
f"ProxyCheckDiagnostics.{constant}" in phase_policy
and f"case ProxyCheckDiagnostics.{constant}:" in phase_policy
and f"MtProxyPhase(\"{phase}\", PHASE_LIVE" in read(ROOT / "Tools/mtproxy_phase_contract.py"),
f"{phase} must be classified as live telemetry held by fresh usable success",
failures,
)
for phase, constant in USABLE_SUCCESS_PHASES:
require(
f"case ProxyCheckDiagnostics.{constant}:" in phase_policy
and f"MtProxyPhase(\"{phase}\", PHASE_SUCCESS" in read(ROOT / "Tools/mtproxy_phase_contract.py"),
f"{phase} must be classified as usable success rather than live telemetry",
failures,
)
for phase, constant in PUNITIVE_FAILURE_PHASES:
require(
f"case ProxyCheckDiagnostics.{constant}:" in phase_policy
and f"MtProxyPhase(\"{phase}\", PHASE_FAILURE" in read(ROOT / "Tools/mtproxy_phase_contract.py")
and f"MtProxyPhase(\"{phase}\", PHASE_FAILURE" in read(ROOT / "Tools/mtproxy_phase_contract.py").split("MtProxyPhase(\"dropped_after_appdata\"")[0],
f"{phase} must remain a punitive failure handled through backoff/rotation policy, not visible live telemetry",
failures,
)
require(
"public static boolean isCurrentProxyUsable" in store
and connected_hold_idx >= 0
and connected_hold_idx < visible_write_idx
and "ProxyVisibleStateStore.isCurrentProxyUsable(currentProxy, event.timestamp)" in native_stage
and 'return new ProxyRuntimeStateStore.Decision("held_live_by_current_proxy_usable"' in native_stage,
"selected-account connected/updating current proxy must hold later live socket telemetry before visible writes",
failures,
)
mark_start = method_body(visible, "static boolean markConnectionStarting")
runtime_mark_start = method_body(store, "public static void markConnectionStarting(SharedConfig.ProxyInfo proxyInfo, ProxyConnectionEvent.Origin origin)")
connect_start_hold_idx = mark_start.find("decision=held_live_by_usable_success")
current_proxy_hold_idx = mark_start.find("decision=held_live_by_current_proxy_usable")
force_visible_idx = mark_start.find("boolean forceVisibleActivation")
force_clear_idx = mark_start.find("ProxyHealthStore.clearUsableSuccessHold(proxyInfo, now, origin.wireName)")
force_return_idx = mark_start.find("return true;", force_clear_idx)
mark_visible_idx = mark_start.find("ProxyStatusMirror.markConnectionStarting(proxyInfo, now, origin)", connect_start_hold_idx)
require(
connect_start_hold_idx >= 0
and mark_visible_idx >= 0
and connect_start_hold_idx < mark_visible_idx
and "ProxyHealthStore.hasFreshUsableSuccess(proxyInfo, now)" in mark_start
and "return false;" in mark_start[connect_start_hold_idx:mark_visible_idx],
"Java connect_start must be held by fresh usable success before it can overwrite visible state",
failures,
)
require(
"ProxyConnectionEvent.connectStart" in runtime_mark_start
and "onRuntimeEvent" in runtime_mark_start,
"ProxyRuntimeStateStore.markConnectionStarting must publish a runtime event instead of writing visible state directly",
failures,
)
require(
force_visible_idx >= 0
and force_clear_idx >= 0
and force_return_idx >= 0
and force_visible_idx < force_clear_idx < force_return_idx < connect_start_hold_idx
and "origin == ProxyConnectionEvent.Origin.USER_SELECT" in mark_start
and "origin == ProxyConnectionEvent.Origin.SETTINGS_CHANGE" in mark_start,
"Java connect_start must clear usable-success hold only for explicit user/settings activation before the normal hold path",
failures,
)
require(
"isCurrentProxyUsable(proxyInfo, now)" in mark_start
and current_proxy_hold_idx >= 0
and current_proxy_hold_idx < mark_visible_idx,
"Java connect_start must also be held while the selected account is already connected/updating through the current proxy",
failures,
)
require(
"held_connect_start_by_usable_success" not in store + visible
and "held_connect_start_by_current_proxy_usable" not in store + visible,
"Java connect_start hold decisions must use the shared held_live_by_* taxonomy",
failures,
)
status_text = method_body(read(MESSENGER / "ProxyCheckDiagnostics.java"), "public static String statusText")
header_text = method_body(read(MESSENGER / "ProxyCheckDiagnostics.java"), "public static String headerStatusText")
color_key = method_body(read(MESSENGER / "ProxyCheckDiagnostics.java"), "public static int statusColorKey")
connected_check = "currentConnectionIsUsableForStatus(proxyInfo, currentConnectionState)"
diagnostics_text = read(MESSENGER / "ProxyCheckDiagnostics.java")
require(
status_text.find(connected_check) != -1
and status_text.find("hasFreshLivePhase(proxyInfo)") != -1
and status_text.find(connected_check) < status_text.find("hasFreshLivePhase(proxyInfo)"),
"connected current-proxy status text must be gated before unresolved live socket phases",
failures,
)
require(
header_text.find(connected_check) != -1
and header_text.find("hasFreshLivePhase(proxyInfo)") != -1
and header_text.find(connected_check) < header_text.find("hasFreshLivePhase(proxyInfo)"),
"connected current-proxy header text must be gated before unresolved live socket phases",
failures,
)
require(
color_key.find(connected_check) != -1
and color_key.find("hasFreshLivePhase(proxyInfo)") != -1
and color_key.find(connected_check) < color_key.find("hasFreshLivePhase(proxyInfo)"),
"connected current-proxy color must be gated before unresolved live socket phases",
failures,
)
require(
"currentConnectionIsUsableForStatus" in diagnostics_text
and "hasFreshLivePhase(proxyInfo) && isProxyUsableSuccessPhase(proxyInfo.lastCheckDiagnostic)" in diagnostics_text,
"MTProxy connected status must require a fresh data-path success phase",
failures,
)
successful_check = method_body(diagnostics_text, "private static boolean hasFreshSuccessfulProxyCheck")
require(
"proxyInfo.available" in successful_check
and "ProxyCheckScheduler.isFresh(proxyInfo)" in successful_check
and "OK.equals(normalize(proxyInfo.lastCheckDiagnostic))" in successful_check
and "hasFreshSuccessfulProxyCheck(proxyInfo)" in diagnostics_text,
"a fresh successful proxy-path check must also satisfy MTProxy connected status",
failures,
)
rotation_stage = rotation[rotation.find("NotificationCenter.proxyConnectionStageChanged"):]
require(
"ProxyRuntimeStateStore.isCurrentProxyUsable(SharedConfig.currentProxy)" in rotation_stage
and "cancelScheduledSwitch(\"usable_success\")" in rotation_stage
and "cancel usable_success" in rotation_stage,
"rotation controller must cancel pending switches on current-proxy usable success",
failures,
)
complete_attempt = method_body(engine, "SwitchDecision completeScheduledAttempt")
require(
"ProxyRuntimeStateStore.isCurrentProxyUsable(currentProxy)" in complete_attempt
and "SwitchDecision.held" in complete_attempt
and complete_attempt.find("isCurrentProxyUsable") < complete_attempt.find("ProxyConnectionEvent.rotationTimeout"),
"rotation engine must hold scheduled attempts before marking connecting timeout failure",
failures,
)
require(
"rotationTrigger" in rotation
and "ignored_non_rotation_trigger" in rotation
and "ProxyRuntimeStateStore.isCurrentProxyUsable(SharedConfig.currentProxy)" in rotation,
"fallback scheduling must rely on reducer rotationTrigger and still cancel pending switches on current-proxy usable success",
failures,
)
require("MT_PROXY_ENDPOINT_USABLE_SUCCESS_HOLD_MS" in policy_cpp, "native endpoint policy must define a usable-success hold window", failures)
require(
"MT_PROXY_ENDPOINT_POST_SUCCESS_DATA_PATH_SHADOWS" in policy_cpp
and "postSuccessDataPathShadowCount" in policy_cpp
and "failureUsesPostSuccessShadowBudget" in policy_cpp
and "shadowFailureByFreshDataPathSuccess" in policy_h
and "shadowFailureByFreshDataPathSuccess" in socket,
"native endpoint policy must bound post-success data-path shadowing instead of suppressing unlimited sibling socket failures",
failures,
)
require("shadowedByUsableSuccess" in policy_h, "FailureResult must report shadowed usable-success failures", failures)
require("failureCanBeShadowedBySuccess" in policy_cpp, "native policy must restrict which failures can be shadowed", failures)
require('"dropped_early_after_appdata"' in policy_cpp and '"dropped_after_appdata"' in policy_cpp, "native policy must explicitly leave post-data drops unshadowed", failures)
record_failure = method_body(policy_cpp, "MtProxyEndpointPolicy::FailureResult MtProxyEndpointPolicy::recordFailure")
require(
"shadowFailureByFreshDataPathSuccessLocked" in record_failure
and "result.shadowedByUsableSuccess = true" in record_failure
and "return result" in record_failure,
"recordFailure must return a budgeted shadowed result without increasing cooldown counters",
failures,
)
endpoint_recorder = read(ROOT / "TMessagesProj/jni/mtproxy/MtProxyEndpointRecorder.cpp")
failure_body = method_body(endpoint_recorder, "void MtProxyEndpointRecorder::recordFailure")
require(
"endpoint_failure_shadowed_by_success" in failure_body
and "shadowedByUsableSuccess" in failure_body
and "hold_ms" in failure_body,
"native endpoint recorder must log shadowed native failures with a dedicated marker",
failures,
)
require("endpoint_failure_shadowed_by_success" in analyzer, "analyzer must know the native shadow marker", failures)
require("held_by_usable_success" in analyzer, "analyzer must preserve Java usable-success hold decisions", failures)
require("held_live_by_usable_success" in analyzer, "analyzer must explain Java live-stage holds after usable success", failures)
require("held_live_by_current_proxy_usable" in analyzer, "analyzer must explain Java live-stage holds while the current proxy is connected", failures)
require("POST_SUCCESS_BREAKTHROUGH_FAILURE_PHASES" in runtime_verifier and "has_prior_post_success_shadow" in runtime_verifier, "runtime verifier must allow data-path failures to break a fresh usable hold only after a prior bounded shadow", failures)
require('"check_proxy_usable_success_hold.py"' in all_checks, "full guard suite must include usable-success hold guard", failures)
run_analyzer_shadow_check(failures)
run_runtime_log_visible_hold_check(failures)
if failures:
print("Proxy usable-success hold guard failed:")
for failure in failures:
print(f" - {failure}")
return 1
print("Proxy usable-success hold guard passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())