Move MTProxy policy engine from tgnet/ to self-contained jni/mtproxy/ module. Consolidate reconnect hold computation into single MtProxyRetryAuthority owner (was split across Connection, ConnectionSocket, endpoint policy, and probe coordinator, causing hold misalignment bugs). Add MtProxyTerminalDiagnostic module with host tests for pre-I/O verdict preservation (fixes 02.07/30.06 clobber livelocks). Auto-generate phase classification (isPreIoTerminalVerdict, needsReconnectBackoff, isObservationFacadePhase, isLocalSchedulerTimeout) from single Python contract into both C++ and Java to eliminate parallel maintenance. Pass native hold (coordinator terminal hold + endpoint cooldown) via JNI to Java so reconnect timer and health store use THE clock, not re-derived ones. Optimize log flushing: batch debug lines, flush errors immediately. Add module boundary guard, host build system (MSVC), and unit tests for retry logic and terminal diagnostic derivation.
2114 lines
95 KiB
Python
2114 lines
95 KiB
Python
#!/usr/bin/env python3
|
|
"""Summarize MTProxy FakeTLS lifecycle markers from collect_mtproxy_logs.ps1.
|
|
|
|
The analyzer is intentionally conservative: it does not try to prove DPI by
|
|
itself. It groups log markers by ConnectionSocket pointer and shows the exact
|
|
phase where each attempt stopped, so VPN/non-VPN captures can be compared.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
import re
|
|
from collections import Counter, defaultdict
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from mtproxy_phase_contract import evidence_for_phase as mtproxy_evidence_for_phase
|
|
|
|
|
|
CONNECTION_RE = re.compile(r"connection\((0x[0-9a-fA-F]+)")
|
|
ACCOUNT_CONNECT_RE = re.compile(
|
|
r"connection\((0x[0-9a-fA-F]+), account([0-9]+), dc([0-9]+), type ([0-9]+)\) connecting \(([^)]+)\)"
|
|
)
|
|
ACCOUNT_LINE_RE = re.compile(
|
|
r"connection\((0x[0-9a-fA-F]+), account([0-9]+), dc([0-9]+), type ([0-9]+)\) (.*)"
|
|
)
|
|
PROXY_CONNECT_RE = re.compile(r"connecting via proxy ([^ ]+) secret\[([0-9]+)\] secret_kind=([^ ]+)")
|
|
PROFILE_RE = re.compile(r"profile selected=([a-z0-9_]+)(?: id=([0-9]+))?(?: .*hello=([0-9]+))?")
|
|
CONNECT_RE = re.compile(r"connect_start .*profile=([a-z0-9_]+).*address=([^ ]+) port=([0-9]+)")
|
|
KEY_RE = re.compile(r"(?<![A-Za-z0-9_])key=([^ ]+)")
|
|
PRIORITY_RE = re.compile(r"(?<![A-Za-z0-9_])priority=([-0-9]+)")
|
|
CONNECTION_PATTERN_RE = re.compile(r"(?<![A-Za-z0-9_])connection_pattern=([^ ]+)")
|
|
TRANSPORT_STATE_RE = re.compile(r"(?<![A-Za-z0-9_])transport_state=([^ ]+)")
|
|
CLIENT_HELLO_SENT_RE = re.compile(r"client_hello_sent bytes=([0-9]+)")
|
|
DISCONNECT_RE = re.compile(
|
|
r"mtproxy_disconnect reason=([-0-9]+).*?error=([-0-9]+).*?"
|
|
r"proxy_state=([-0-9]+) tls_state=([-0-9]+) bytes_read=([0-9]+)"
|
|
)
|
|
TLS_FRAMES_COMPLETED_RE = re.compile(r"tls_frames_completed=([0-9]+)")
|
|
PROXY_CHECK_RE = re.compile(r"proxy_check_([a-z_]+)")
|
|
PROXY_CHECK_SCHEDULER_RE = re.compile(r"proxy_check_scheduler ([a-z_]+)")
|
|
PROXY_CHECK_START_RE = re.compile(r"proxy_check_start .*ping_id=([0-9]+).*address=([^ ]+)")
|
|
PROXY_CHECK_SOCKET_RE = re.compile(r"proxy_check_socket_connected ping_id=([0-9]+)")
|
|
PROXY_CHECK_RESULT_RE = re.compile(r"proxy_check_finish result=([a-z]+) reason=([^ ]+)")
|
|
PROXY_CHECK_FINISH_RE = re.compile(r"proxy_check_finish result=([a-z]+) reason=([^ ]+).*ping_id=([0-9]+) address=([^ ]+)")
|
|
PROXY_CHECK_DIAGNOSTIC_RE = re.compile(r"proxy_check_finish .*(?<![A-Za-z0-9_])diagnostic=([^ ]+)")
|
|
PROXY_CHECK_START_FAILED_RE = re.compile(r"proxy_check_start_failed reason=([^ ]+)")
|
|
PROXY_CHECK_CLOSE_RE = re.compile(r"proxy_check_connection_closed close_reason=([-0-9]+)")
|
|
PROXY_CHECK_CLOSE_WITH_PING_RE = re.compile(r"proxy_check_connection_closed close_reason=([-0-9]+) ping_id=([0-9]+)")
|
|
PROXY_CHECK_IGNORED_CLOSE_RE = re.compile(r"proxy_check_connection_closed_ignored close_reason=([-0-9]+)")
|
|
PROXY_CONTROL_RE = re.compile(r"proxy_control decision=([a-z_]+)")
|
|
PROXY_ROTATION_RE = re.compile(r"proxy_rotation ([a-z_]+)")
|
|
PROXY_CONNECTION_STAGE_RE = re.compile(r"proxy_connection_stage account=([0-9]+) phase=([^ ]+)")
|
|
ENDPOINT_RE = re.compile(r"(?<![A-Za-z0-9_])endpoint=([^ ]+)")
|
|
SCHEDULER_LISTENERS_RE = re.compile(r"(?<![A-Za-z0-9_])listeners=([0-9]+)")
|
|
SCHEDULER_FORCE_RE = re.compile(r"(?<![A-Za-z0-9_])force=(true|false)")
|
|
SCHEDULER_RESULT_RE = re.compile(r"proxy_check_scheduler finish result=([a-z]+)")
|
|
SCHEDULER_APPLIED_RE = re.compile(
|
|
r"(?<![A-Za-z0-9_])time=([-0-9]+) applied_time=([-0-9]+) raw_time=([-0-9]+)"
|
|
)
|
|
SCHEDULER_PHASE_RE = re.compile(r"(?<![A-Za-z0-9_])phase=([^ ]+)")
|
|
SCHEDULER_DIAGNOSTIC_RE = re.compile(r"(?<![A-Za-z0-9_])diagnostic=([^ ]+)")
|
|
# Accepts logcat ("07-01 20:59:30.000") and the native _net log without zero
|
|
# padding ("7-2 11:48:40.760"); the strict two-digit form made every
|
|
# time-based summary silently no-op on native logs.
|
|
TIME_RE = re.compile(r"^[0-9]{1,2}-[0-9]{1,2} ([0-9]{1,2}):([0-9]{2}):([0-9]{2})\.([0-9]{3})")
|
|
FIELD_RE_TEMPLATE = r"(?<![A-Za-z0-9_]){}=([^ ]+)"
|
|
|
|
FAKETLS_FAILURE_VERDICTS = {
|
|
"connection_not_started",
|
|
"admission_timeout",
|
|
"endpoint_cooldown_timeout",
|
|
"mtproxy_probe_wait_timeout",
|
|
"dns_coalesce_timeout",
|
|
"dns_negative_cache_hit",
|
|
"dns_blocked_zero_address",
|
|
"pre_tcp_gate_admission_overlap",
|
|
"host_resolve_failed",
|
|
"host_resolve_timeout",
|
|
"tcp_connect_gate_timeout",
|
|
"tcp_budget_stolen_by_pre_tcp_wait",
|
|
"dns_budget_stolen_by_pre_tcp_wait",
|
|
"tcp_not_connected",
|
|
"tcp_connection_refused",
|
|
"tcp_connect_timeout",
|
|
"tcp_connected_no_pong",
|
|
"secret_parse_invalid_domain_control_char",
|
|
"secret_parse_invalid_domain",
|
|
# true_client_hello_timeout is kept as a legacy alias for old captures and
|
|
# for strict no-EOF/no-bytes deadlines that predate the split verdict.
|
|
"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",
|
|
"faketls_not_mtproxy_response",
|
|
"faketls_no_server_hello_terminal",
|
|
"faketls_server_closed_terminal",
|
|
"background_handshake_aborted",
|
|
"handshake_profiles_exhausted",
|
|
# unsupported_for_current_client is kept as a legacy alias for old captures.
|
|
"unsupported_for_current_client",
|
|
"mtproxy_packet_sent_no_response",
|
|
"post_handshake_no_appdata",
|
|
"dropped_early_after_appdata",
|
|
"dropped_after_appdata",
|
|
}
|
|
NON_FAILURE_VERDICTS = {
|
|
"ok",
|
|
"handshake_ok_no_appdata_sent",
|
|
"shadowed_by_usable_success",
|
|
"shadowed_socket_failure",
|
|
"ignored_cancelled_generation",
|
|
"reconnect_backoff_suppressed",
|
|
}
|
|
|
|
|
|
def event_marker_matches(text: str, needle: str) -> bool:
|
|
return re.search(r"(?<![A-Za-z0-9_])" + re.escape(needle) + r"(?![A-Za-z0-9_])", text) is not None
|
|
|
|
|
|
def line_field(text: str, name: str) -> str:
|
|
match = re.search(FIELD_RE_TEMPLATE.format(re.escape(name)), text)
|
|
return match.group(1) if match else ""
|
|
|
|
|
|
def line_int_field(text: str, name: str) -> int:
|
|
value = line_field(text, name)
|
|
return int(value) if value.isdigit() else 0
|
|
|
|
|
|
@dataclass
|
|
class Attempt:
|
|
key: str
|
|
first_line: int = 0
|
|
last_line: int = 0
|
|
lines: list[str] = field(default_factory=list)
|
|
events: Counter[str] = field(default_factory=Counter)
|
|
profile: str = ""
|
|
profile_id: str = ""
|
|
hello_bytes: str = ""
|
|
client_hello_bytes: str = ""
|
|
address: str = ""
|
|
port: str = ""
|
|
endpoint: str = ""
|
|
proxy_key: str = ""
|
|
proxy_endpoint: str = ""
|
|
secret_kind: str = ""
|
|
secret_len: str = ""
|
|
account: str = ""
|
|
dc: str = ""
|
|
connection_type: str = ""
|
|
telegram_endpoint: str = ""
|
|
priority: str = ""
|
|
connection_pattern: str = ""
|
|
transport_state: str = ""
|
|
failure_phase: str = ""
|
|
failure_evidence: str = ""
|
|
server_response_bytes: int = 0
|
|
disconnect: str = ""
|
|
disconnect_reason: str = ""
|
|
disconnect_error: str = ""
|
|
tls_frames_completed: int = 0
|
|
first_time: str = ""
|
|
first_seconds: float = 0.0
|
|
event_times: dict[str, float] = field(default_factory=dict)
|
|
|
|
def endpoint_text(self) -> str:
|
|
if self.endpoint:
|
|
return self.endpoint
|
|
if self.proxy_endpoint:
|
|
return self.proxy_endpoint
|
|
if self.address:
|
|
return f"{self.address}:{self.port}"
|
|
return "unknown"
|
|
|
|
def is_faketls(self) -> bool:
|
|
return (
|
|
self.secret_kind == "ee"
|
|
or bool(self.proxy_key)
|
|
or self.events["client_hello_sent"] > 0
|
|
or self.events["server_hello_hmac_ok"] > 0
|
|
)
|
|
|
|
def timing_ms(self, start_event: str, end_event: str) -> str:
|
|
start = self.event_times.get(start_event)
|
|
end = self.event_times.get(end_event)
|
|
if start is None or end is None:
|
|
return ""
|
|
if end < start:
|
|
return ""
|
|
return str(round((end - start) * 1000))
|
|
|
|
def timing_ms_int(self, start_event: str, end_event: str) -> int | None:
|
|
value = self.timing_ms(start_event, end_event)
|
|
if not value:
|
|
return None
|
|
return int(value)
|
|
|
|
def has_pre_tcp_wait_before_socket_connect(self) -> bool:
|
|
pre_tcp_events = (
|
|
"admission_queue",
|
|
"admission_queue_wait",
|
|
"admission_grant_queued",
|
|
"endpoint_cooldown",
|
|
"dns_coalesce_wait",
|
|
"tcp_connect_gate",
|
|
"tcp_connect_gate_wait",
|
|
"host_resolve_start",
|
|
)
|
|
socket_start = self.event_times.get("socket_connect_start")
|
|
if socket_start is None:
|
|
return False
|
|
for event in pre_tcp_events:
|
|
event_time = self.event_times.get(event)
|
|
if event_time is not None and event_time <= socket_start:
|
|
return True
|
|
return False
|
|
|
|
def has_stolen_tcp_budget(self) -> bool:
|
|
tcp_close_ms = self.timing_ms_int("socket_connect_start", "mtproxy_disconnect")
|
|
return (
|
|
tcp_close_ms is not None
|
|
and tcp_close_ms < 250
|
|
and self.disconnect_reason == "2"
|
|
and self.disconnect_error == "0"
|
|
and self.has_pre_tcp_wait_before_socket_connect()
|
|
)
|
|
|
|
def has_stolen_dns_budget(self) -> bool:
|
|
dns_close_ms = self.timing_ms_int("host_resolve_start", "mtproxy_disconnect")
|
|
socket_start = self.event_times.get("socket_connect_start")
|
|
host_resolve_start = self.event_times.get("host_resolve_start")
|
|
return (
|
|
dns_close_ms is not None
|
|
and dns_close_ms < 250
|
|
and socket_start is None
|
|
and host_resolve_start is not None
|
|
and self.disconnect_reason == "2"
|
|
and self.disconnect_error == "0"
|
|
and any(
|
|
self.event_times.get(event) is not None
|
|
and self.event_times[event] < host_resolve_start
|
|
for event in (
|
|
"admission_queue",
|
|
"admission_queue_wait",
|
|
"admission_grant_queued",
|
|
"endpoint_cooldown",
|
|
"dns_coalesce_wait",
|
|
"tcp_connect_gate",
|
|
"tcp_connect_gate_wait",
|
|
)
|
|
)
|
|
)
|
|
|
|
def add(self, line_no: int, text: str) -> None:
|
|
if not self.first_line:
|
|
self.first_line = line_no
|
|
self.first_time = log_time_label(text)
|
|
self.first_seconds = log_time_seconds(text)
|
|
self.last_line = line_no
|
|
self.lines.append(text)
|
|
|
|
account_connect = ACCOUNT_CONNECT_RE.search(text)
|
|
if account_connect:
|
|
self.account = account_connect.group(2)
|
|
self.dc = account_connect.group(3)
|
|
self.connection_type = account_connect.group(4)
|
|
self.telegram_endpoint = account_connect.group(5)
|
|
|
|
account_line = ACCOUNT_LINE_RE.search(text)
|
|
if account_line:
|
|
self.account = account_line.group(2)
|
|
self.dc = account_line.group(3)
|
|
self.connection_type = account_line.group(4)
|
|
account_event = account_line.group(5)
|
|
if account_event.startswith("connected to "):
|
|
self.events["account_connected"] += 1
|
|
if account_event.startswith("send message "):
|
|
self.events["account_send_message"] += 1
|
|
if account_event.startswith("received message "):
|
|
self.events["account_received_message"] += 1
|
|
if "received rpc_result" in account_event:
|
|
self.events["account_rpc_result"] += 1
|
|
if "reset auth key due to -404" in account_event:
|
|
self.events["account_auth_404"] += 1
|
|
if "received invalid packet length" in account_event:
|
|
self.events["account_invalid_packet_length"] += 1
|
|
disconnect_match = re.search(r"disconnected with reason ([-0-9]+)", account_event)
|
|
if disconnect_match:
|
|
self.events[f"account_disconnect_{disconnect_match.group(1)}"] += 1
|
|
|
|
proxy_connect = PROXY_CONNECT_RE.search(text)
|
|
if proxy_connect:
|
|
self.proxy_endpoint = proxy_connect.group(1)
|
|
self.secret_len = proxy_connect.group(2)
|
|
self.secret_kind = proxy_connect.group(3)
|
|
|
|
connect = CONNECT_RE.search(text)
|
|
if connect:
|
|
self.profile = connect.group(1)
|
|
self.address = connect.group(2)
|
|
self.port = connect.group(3)
|
|
self.telegram_endpoint = f"{self.address}:{self.port}"
|
|
|
|
key = KEY_RE.search(text)
|
|
if key:
|
|
self.proxy_key = key.group(1)
|
|
self.endpoint = endpoint_from_admission_key(self.proxy_key)
|
|
priority = PRIORITY_RE.search(text)
|
|
if priority:
|
|
self.priority = priority.group(1)
|
|
|
|
connection_pattern = CONNECTION_PATTERN_RE.search(text)
|
|
if connection_pattern:
|
|
self.connection_pattern = connection_pattern.group(1)
|
|
|
|
transport_state = TRANSPORT_STATE_RE.search(text)
|
|
if transport_state:
|
|
self.transport_state = transport_state.group(1)
|
|
response_bytes = line_int_field(text, "response_bytes")
|
|
if response_bytes:
|
|
self.server_response_bytes = max(self.server_response_bytes, response_bytes)
|
|
if event_marker_matches(text, "mtproxy_tls_after_client_hello"):
|
|
self.server_response_bytes = max(self.server_response_bytes, line_int_field(text, "bytes"))
|
|
line_evidence = line_field(text, "evidence")
|
|
if line_evidence:
|
|
self.failure_evidence = line_evidence
|
|
line_phase = ""
|
|
if event_marker_matches(text, "recipe_exhausted"):
|
|
line_phase = line_field(text, "failed_phase")
|
|
elif event_marker_matches(text, "recipe_failed") or event_marker_matches(text, "close_diagnostic"):
|
|
line_phase = line_field(text, "phase")
|
|
elif event_marker_matches(text, "faketls_budget_exhausted") or event_marker_matches(text, "probe_faketls_budget_backoff"):
|
|
line_phase = line_field(text, "terminal_phase") or line_field(text, "phase")
|
|
if line_phase:
|
|
self.failure_phase = line_phase
|
|
if not self.failure_evidence:
|
|
self.failure_evidence = mtproxy_evidence_for_phase(line_phase, self.server_response_bytes)
|
|
|
|
profile = PROFILE_RE.search(text)
|
|
if profile:
|
|
self.profile = profile.group(1)
|
|
if profile.group(2):
|
|
self.profile_id = profile.group(2)
|
|
if profile.group(3):
|
|
self.hello_bytes = profile.group(3)
|
|
|
|
client_hello = CLIENT_HELLO_SENT_RE.search(text)
|
|
if client_hello:
|
|
self.client_hello_bytes = client_hello.group(1)
|
|
|
|
disconnect = DISCONNECT_RE.search(text)
|
|
if disconnect:
|
|
self.disconnect_reason = disconnect.group(1)
|
|
self.disconnect_error = disconnect.group(2)
|
|
self.events["mtproxy_disconnect"] += 1
|
|
self.event_times.setdefault("mtproxy_disconnect", log_time_seconds(text))
|
|
self.disconnect = (
|
|
f"reason={disconnect.group(1)} error={disconnect.group(2)} "
|
|
f"proxy_state={disconnect.group(3)} tls_state={disconnect.group(4)} "
|
|
f"bytes_read={disconnect.group(5)}"
|
|
)
|
|
tls_frames_completed = TLS_FRAMES_COMPLETED_RE.search(text)
|
|
if tls_frames_completed:
|
|
self.tls_frames_completed = max(self.tls_frames_completed, int(tls_frames_completed.group(1)))
|
|
|
|
if "close_diagnostic_suppressed" in text:
|
|
self.events["close_diagnostic_suppressed"] += 1
|
|
self.event_times.setdefault("close_diagnostic_suppressed", log_time_seconds(text))
|
|
return
|
|
|
|
event_map = {
|
|
"connect_start": "connect_start",
|
|
"host_resolve_start": "host_resolve_start",
|
|
"host_resolve_not_started": "connection_not_started",
|
|
"open_connection_reset_failed": "connection_not_started",
|
|
"host_resolve_failed": "host_resolve_failed",
|
|
"host_resolve_timeout": "host_resolve_timeout",
|
|
"dns_negative_cache_hit": "dns_negative_cache_hit",
|
|
"dns_blocked_zero_address": "dns_blocked_zero_address",
|
|
"connection_not_started": "connection_not_started",
|
|
"admission_timeout": "admission_timeout",
|
|
"endpoint_cooldown_timeout": "endpoint_cooldown_timeout",
|
|
"dns_coalesce_timeout": "dns_coalesce_timeout",
|
|
"tcp_connect_gate_timeout": "tcp_connect_gate_timeout",
|
|
"tcp_not_connected": "tcp_not_connected",
|
|
"tcp_connection_refused": "tcp_connection_refused",
|
|
"tcp_connect_timeout": "tcp_connect_timeout",
|
|
"tcp_connected_no_pong": "tcp_connected_no_pong",
|
|
"secret_domain_sanitized": "secret_domain_sanitized",
|
|
"secret_parse_invalid_domain_control_char": "secret_parse_invalid_domain_control_char",
|
|
"secret_parse_invalid_domain": "secret_parse_invalid_domain",
|
|
"secret_domain_invalid": "secret_domain_invalid",
|
|
"socket_connect_start": "socket_connect_start",
|
|
"socket_connected": "socket_connected",
|
|
"client_hello_send_progress": "client_hello_send_progress",
|
|
"client_hello_fragment_plan": "client_hello_fragment_plan",
|
|
"client_hello_fragment": "client_hello_fragment",
|
|
"client_hello_fingerprint": "client_hello_fingerprint",
|
|
"server_data_before_client_hello_complete": "server_data_before_client_hello_complete",
|
|
"client_hello_sent": "client_hello_sent",
|
|
"true_client_hello_timeout": "true_client_hello_timeout",
|
|
"client_hello_sent_no_server_hello": "client_hello_sent_no_server_hello",
|
|
"mtproxy_tls_after_client_hello": "mtproxy_tls_after_client_hello",
|
|
"tls_alert_after_client_hello": "tls_alert_after_client_hello",
|
|
"short_tls_response_after_client_hello": "short_tls_response_after_client_hello",
|
|
"unrecognized_response_after_client_hello": "unrecognized_response_after_client_hello",
|
|
"unrecognized_tls_response_after_client_hello": "unrecognized_tls_response_after_client_hello",
|
|
"faketls_budget_exhausted": "faketls_budget_exhausted",
|
|
"probe_faketls_budget_backoff": "faketls_budget_exhausted",
|
|
"faketls_not_mtproxy_response": "faketls_not_mtproxy_response",
|
|
"faketls_no_server_hello_terminal": "faketls_no_server_hello_terminal",
|
|
"faketls_server_closed_terminal": "faketls_server_closed_terminal",
|
|
"post_client_hello_response_failed": "post_client_hello_response_failed",
|
|
"handshake_profiles_exhausted": "handshake_profiles_exhausted",
|
|
"unsupported_for_current_client": "unsupported_for_current_client",
|
|
"recipe_exhausted": "recipe_exhausted",
|
|
"recipe_failed": "recipe_failed",
|
|
"endpoint_cooldown": "endpoint_cooldown",
|
|
"tcp_connect_gate": "tcp_connect_gate",
|
|
"tcp_connect_gate_wait": "tcp_connect_gate_wait",
|
|
"dns_coalesce_wait": "dns_coalesce_wait",
|
|
"dns_cache_hit": "dns_cache_hit",
|
|
"dns_cache_store": "dns_cache_store",
|
|
"resolved_sslip": "resolved_sslip",
|
|
"phase_adaptive_recipe": "phase_adaptive_recipe",
|
|
"mtproxy_probe_wait": "mtproxy_probe_wait",
|
|
"mtproxy_probe_wait_timeout": "mtproxy_probe_wait_timeout",
|
|
"probe_start": "probe_start",
|
|
"probe_join": "probe_join",
|
|
"probe_working_recipe": "probe_working_recipe",
|
|
"working_recipe_cached": "working_recipe_cached",
|
|
"probe_profiles_exhausted": "probe_profiles_exhausted",
|
|
"probe_terminal_unsupported": "probe_terminal_unsupported",
|
|
"probe_wait_timer_fire": "probe_wait_timer_fire",
|
|
"probe_owner_complete": "probe_owner_complete",
|
|
"probe_owner_release": "probe_owner_release",
|
|
"endpoint_failure_shadowed_by_success": "endpoint_failure_shadowed_by_success",
|
|
"shadowed_socket_failure": "shadowed_socket_failure",
|
|
"ignored_cancelled_generation": "ignored_cancelled_generation",
|
|
"endpoint_attempt_cancelled": "ignored_cancelled_generation",
|
|
"endpoint_failure_skipped_local": "endpoint_failure_skipped_local",
|
|
"silent_after_client_hello": "silent_after_client_hello",
|
|
"endpoint_failure": "endpoint_failure",
|
|
"endpoint_handshake_ok": "endpoint_handshake_ok",
|
|
"endpoint_data_path_success": "endpoint_data_path_success",
|
|
"endpoint_data_path_success_rejected": "endpoint_data_path_success_rejected",
|
|
"endpoint_success": "endpoint_success",
|
|
"pre_tcp_timeout_diagnostic": "pre_tcp_timeout_diagnostic",
|
|
"pre_tcp_wait_finished": "pre_tcp_wait_finished",
|
|
"pre_tcp_timer_ignored": "pre_tcp_timer_ignored",
|
|
"transport_invariant": "transport_invariant",
|
|
"tcp_connect_timeout": "tcp_connect_timeout",
|
|
"server_hello_hmac_ok": "server_hello_hmac_ok",
|
|
"server_hello_hmac_mismatch": "server_hello_hmac_mismatch",
|
|
"server_hello_hmac_timeout": "server_hello_hmac_timeout",
|
|
"server_hello_timeout_close": "server_hello_timeout_close",
|
|
"background_handshake_aborted": "background_handshake_aborted",
|
|
"close_ignored_already_closed": "close_ignored_already_closed",
|
|
"connection_close_ignored_already_notified": "connection_close_ignored_already_notified",
|
|
"reconnect_backoff_suppressed": "reconnect_backoff_suppressed",
|
|
"admission_disabled": "admission_disabled",
|
|
"admission_grant": "admission_grant",
|
|
"admission_grant_queued": "admission_grant_queued",
|
|
"admission_dequeue": "admission_dequeue",
|
|
"admission_dequeue_global": "admission_dequeue_global",
|
|
"admission_release": "admission_release",
|
|
"admission_release_ignored": "admission_release_ignored",
|
|
"admission_timer_fire": "admission_timer_fire",
|
|
"admission_timer_ignored": "admission_timer_ignored",
|
|
"admission_host_resolve_timer_fire": "admission_host_resolve_timer_fire",
|
|
"endpoint_backoff_timer_fire": "endpoint_backoff_timer_fire",
|
|
"dns_coalesce_timer_fire": "dns_coalesce_timer_fire",
|
|
"tcp_connect_gate_timer_fire": "tcp_connect_gate_timer_fire",
|
|
"tcp_connect_gate_grant": "tcp_connect_gate_grant",
|
|
"tcp_connect_gate_release": "tcp_connect_gate_release",
|
|
"TLS server hello hmac wait": "server_hello_hmac_wait",
|
|
"admission_queue": "admission_queue",
|
|
"admission_queue_wait": "admission_queue_wait",
|
|
"admission_tcp_failure_cooldown": "admission_tcp_failure_cooldown",
|
|
"admission_freeze_cooldown": "admission_freeze_cooldown",
|
|
"admission_failure_cooldown": "admission_failure_cooldown",
|
|
"faketls_server_hello_wait_timeout": "faketls_server_hello_wait_timeout",
|
|
"server_closed_after_client_hello": "server_closed_after_client_hello",
|
|
"admission_freeze_detected": "admission_freeze_detected",
|
|
"admission_freeze_observed": "admission_freeze_observed",
|
|
"admission_hold_after_client_hello_failure": "admission_hold_after_client_hello_failure",
|
|
"profile_rotate": "profile_rotate",
|
|
"on_connected": "on_connected",
|
|
"first_tls_app_sent": "first_tls_app_sent",
|
|
"tls_frame_complete": "tls_frame_complete",
|
|
"first_tls_app_recv": "first_tls_app_recv",
|
|
"mtproxy_tls_appdata_no_response_timeout": "post_handshake_no_appdata",
|
|
"post_handshake_no_appdata": "post_handshake_no_appdata",
|
|
"dropped_early_after_appdata": "dropped_early_after_appdata",
|
|
"dropped_after_appdata": "dropped_after_appdata",
|
|
"first_mtproxy_packet_sent": "first_mtproxy_packet_sent",
|
|
"first_mtproxy_packet_recv": "first_mtproxy_packet_recv",
|
|
"mtproxy_packet_no_response_timeout": "mtproxy_packet_sent_no_response",
|
|
"mtproxy_packet_sent_no_response": "mtproxy_packet_sent_no_response",
|
|
"tls_alert": "tls_alert",
|
|
"recv_eof": "recv_eof",
|
|
"EPOLLHUP": "epoll_hup",
|
|
"EPOLLRDHUP": "epoll_rdhup",
|
|
"socket error": "socket_error",
|
|
"TLS response version mismatch": "tls_response_version_mismatch",
|
|
"TLS response record type mismatch": "tls_response_record_type_mismatch",
|
|
}
|
|
for needle, event in event_map.items():
|
|
if event_marker_matches(text, needle):
|
|
self.events[event] += 1
|
|
self.event_times.setdefault(event, log_time_seconds(text))
|
|
|
|
def verdict(self) -> str:
|
|
has = self.events.__contains__
|
|
if has("endpoint_failure_shadowed_by_success"):
|
|
return "shadowed_by_usable_success"
|
|
if has("on_connected") and not has("socket_connected"):
|
|
return "connected_without_socket_connected_marker"
|
|
if has("host_resolve_failed"):
|
|
return "host_resolve_failed"
|
|
if self.has_stolen_dns_budget():
|
|
return "dns_budget_stolen_by_pre_tcp_wait"
|
|
if has("host_resolve_timeout"):
|
|
return "host_resolve_timeout"
|
|
if has("endpoint_cooldown_timeout"):
|
|
return "endpoint_cooldown_timeout"
|
|
if has("dns_coalesce_timeout"):
|
|
return "dns_coalesce_timeout"
|
|
if has("connection_not_started"):
|
|
return "connection_not_started"
|
|
if has("admission_timeout"):
|
|
return "admission_timeout"
|
|
if has("tcp_connect_gate_timeout"):
|
|
return "tcp_connect_gate_timeout"
|
|
if has("tcp_connect_gate_grant") and has("admission_queue") and not has("socket_connect_start"):
|
|
return "pre_tcp_gate_admission_overlap"
|
|
if not has("socket_connect_start"):
|
|
if has("mtproxy_probe_wait_timeout"):
|
|
return "mtproxy_probe_wait_timeout"
|
|
if has("host_resolve_start"):
|
|
return "host_resolve_timeout"
|
|
if has("endpoint_cooldown"):
|
|
return "endpoint_cooldown_timeout"
|
|
if has("dns_coalesce_wait"):
|
|
return "dns_coalesce_timeout"
|
|
if has("tcp_connect_gate"):
|
|
return "tcp_connect_gate_timeout"
|
|
if has("admission_queue"):
|
|
return "admission_timeout"
|
|
return "connection_not_started"
|
|
if not has("socket_connected"):
|
|
if self.has_stolen_tcp_budget():
|
|
return "tcp_budget_stolen_by_pre_tcp_wait"
|
|
if has("tcp_connection_refused"):
|
|
return "tcp_connection_refused"
|
|
if has("tcp_connect_timeout"):
|
|
return "tcp_connect_timeout"
|
|
return "tcp_not_connected"
|
|
if has("tcp_connected_no_pong"):
|
|
return "tcp_connected_no_pong"
|
|
if has("secret_parse_invalid_domain_control_char"):
|
|
return "secret_parse_invalid_domain_control_char"
|
|
if has("secret_parse_invalid_domain"):
|
|
return "secret_parse_invalid_domain"
|
|
if not has("client_hello_sent"):
|
|
return "tcp_connected_no_pong"
|
|
if not has("server_hello_hmac_ok"):
|
|
if has("background_handshake_aborted"):
|
|
return "background_handshake_aborted"
|
|
if has("faketls_not_mtproxy_response"):
|
|
return "faketls_not_mtproxy_response"
|
|
if has("faketls_no_server_hello_terminal"):
|
|
return "faketls_no_server_hello_terminal"
|
|
if has("faketls_server_closed_terminal"):
|
|
return "faketls_server_closed_terminal"
|
|
if has("handshake_profiles_exhausted"):
|
|
return "handshake_profiles_exhausted"
|
|
if has("unsupported_for_current_client"):
|
|
return "handshake_profiles_exhausted"
|
|
if has("server_closed_after_client_hello"):
|
|
return "server_closed_after_client_hello"
|
|
if has("tls_alert_after_client_hello"):
|
|
return "tls_alert_after_client_hello"
|
|
if has("short_tls_response_after_client_hello"):
|
|
return "short_tls_response_after_client_hello"
|
|
if has("unrecognized_response_after_client_hello") or has("unrecognized_tls_response_after_client_hello"):
|
|
return "unrecognized_response_after_client_hello"
|
|
if has("server_hello_hmac_mismatch") or has("server_hello_hmac_timeout") or has("server_hello_hmac_wait"):
|
|
return "server_hello_hmac_mismatch"
|
|
if has("true_client_hello_timeout"):
|
|
return "true_client_hello_timeout"
|
|
if has("faketls_server_hello_wait_timeout") or has("server_hello_timeout_close"):
|
|
return "faketls_server_hello_wait_timeout"
|
|
if has("client_hello_sent_no_server_hello") or has("admission_freeze_detected"):
|
|
return "client_hello_sent_no_server_hello"
|
|
if has("recv_eof"):
|
|
return "server_closed_after_client_hello"
|
|
return "faketls_server_hello_wait_timeout"
|
|
if not has("on_connected"):
|
|
return "post_handshake_no_appdata"
|
|
if has("post_handshake_no_appdata"):
|
|
return "post_handshake_no_appdata"
|
|
if has("first_tls_app_sent") and not has("first_tls_app_recv"):
|
|
return "post_handshake_no_appdata"
|
|
if has("dropped_early_after_appdata"):
|
|
return "dropped_early_after_appdata"
|
|
if has("dropped_after_appdata"):
|
|
return "dropped_after_appdata"
|
|
if has("first_tls_app_recv"):
|
|
return "ok"
|
|
return "handshake_ok_no_appdata_sent"
|
|
|
|
def evidence(self) -> str:
|
|
if self.failure_evidence:
|
|
return self.failure_evidence
|
|
phase = self.failure_phase or self.verdict()
|
|
return mtproxy_evidence_for_phase(phase, self.server_response_bytes)
|
|
|
|
def completed_tls_frames(self) -> int:
|
|
return max(self.tls_frames_completed, self.events["tls_frame_complete"])
|
|
|
|
def compact(self) -> str:
|
|
parts = [
|
|
self.first_time or f"line {self.first_line}",
|
|
self.endpoint_text(),
|
|
f"profile={profile_text(self)}",
|
|
f"phase={self.verdict()}",
|
|
]
|
|
evidence = self.evidence()
|
|
if evidence and evidence != "none":
|
|
parts.append(f"evidence={evidence}")
|
|
if self.hello_bytes:
|
|
parts.append(f"hello={self.hello_bytes}")
|
|
if self.connection_type:
|
|
parts.append(f"type={self.connection_type}")
|
|
if self.priority:
|
|
parts.append(f"priority={self.priority}")
|
|
if self.connection_pattern:
|
|
parts.append(f"pattern={self.connection_pattern}")
|
|
if (hmac_ms := self.timing_ms("client_hello_sent", "server_hello_hmac_ok")):
|
|
parts.append(f"hmac_ms={hmac_ms}")
|
|
if (tcp_close_ms := self.timing_ms("socket_connect_start", "mtproxy_disconnect")):
|
|
parts.append(f"tcp_close_ms={tcp_close_ms}")
|
|
if (dns_close_ms := self.timing_ms("host_resolve_start", "mtproxy_disconnect")):
|
|
parts.append(f"dns_close_ms={dns_close_ms}")
|
|
if self.disconnect_reason:
|
|
parts.append(f"close={self.disconnect_reason}/{self.disconnect_error}")
|
|
return " ".join(parts)
|
|
|
|
|
|
def marker_text(line: str) -> tuple[int, str]:
|
|
# collect_mtproxy_logs.ps1 writes: path:line_number: original log line
|
|
match = re.match(r"^.*?:([0-9]+):\s*(.*)$", line.rstrip("\n"))
|
|
if match:
|
|
prefix = line[: match.start(1) - 1]
|
|
if "/" not in prefix and "\\" not in prefix and not prefix.endswith((".txt", ".log")):
|
|
return 0, line.rstrip("\n")
|
|
return int(match.group(1)), match.group(2)
|
|
return 0, line.rstrip("\n")
|
|
|
|
|
|
def log_time_seconds(text: str) -> float:
|
|
match = TIME_RE.match(text)
|
|
if not match:
|
|
return 0.0
|
|
hours, minutes, seconds, millis = [int(part) for part in match.groups()]
|
|
return hours * 3600 + minutes * 60 + seconds + millis / 1000.0
|
|
|
|
|
|
def log_time_label(text: str) -> str:
|
|
match = TIME_RE.match(text)
|
|
if not match:
|
|
return ""
|
|
return text[:match.end()]
|
|
|
|
|
|
def endpoint_from_admission_key(proxy_key: str) -> str:
|
|
prefix, separator, tail = proxy_key.rpartition(":")
|
|
if separator and prefix and not tail.isdigit():
|
|
return prefix
|
|
return proxy_key
|
|
|
|
|
|
def profile_text(attempt: Attempt) -> str:
|
|
if attempt.profile:
|
|
return attempt.profile
|
|
if attempt.events["client_hello_sent"]:
|
|
return "unknown_profile"
|
|
return "no_profile_before_clienthello"
|
|
|
|
|
|
def is_connect_start(text: str) -> bool:
|
|
return "mtproxy_startup connect_start " in text
|
|
|
|
|
|
def is_socket_connect_start(text: str) -> bool:
|
|
return "mtproxy_startup socket_connect_start" in text
|
|
|
|
|
|
def is_proxy_connect(text: str) -> bool:
|
|
return "connecting via proxy " in text
|
|
|
|
|
|
def load_attempts(path: Path) -> tuple[list[Attempt], list[str]]:
|
|
attempts: dict[str, Attempt] = {}
|
|
global_lines: list[str] = []
|
|
sequence_by_key: defaultdict[str, int] = defaultdict(int)
|
|
active_key_by_pointer: dict[str, str] = {}
|
|
pending_account_by_pointer: dict[str, tuple[str, str, str, str]] = {}
|
|
|
|
def apply_pending_account(pointer: str, attempt: Attempt) -> None:
|
|
pending = pending_account_by_pointer.get(pointer)
|
|
if not pending:
|
|
return
|
|
attempt.account, attempt.dc, attempt.connection_type, attempt.telegram_endpoint = pending
|
|
|
|
def new_attempt(pointer: str) -> Attempt:
|
|
if sequence_by_key[pointer] == 0:
|
|
key = pointer
|
|
else:
|
|
key = f"{pointer}#{sequence_by_key[pointer]}"
|
|
sequence_by_key[pointer] += 1
|
|
attempt = Attempt(key=key)
|
|
attempts[key] = attempt
|
|
active_key_by_pointer[pointer] = key
|
|
apply_pending_account(pointer, attempt)
|
|
return attempt
|
|
|
|
for raw in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
if raw.strip() == "No MTProxy markers found.":
|
|
continue
|
|
line_no, text = marker_text(raw)
|
|
connection = CONNECTION_RE.search(text)
|
|
if not connection:
|
|
global_lines.append(text)
|
|
continue
|
|
|
|
pointer = connection.group(1)
|
|
current_key = active_key_by_pointer.get(pointer)
|
|
account_connect = ACCOUNT_CONNECT_RE.search(text)
|
|
if account_connect:
|
|
pending_account_by_pointer[pointer] = (
|
|
account_connect.group(2),
|
|
account_connect.group(3),
|
|
account_connect.group(4),
|
|
account_connect.group(5),
|
|
)
|
|
if current_key is None:
|
|
global_lines.append(text)
|
|
continue
|
|
|
|
if is_proxy_connect(text):
|
|
attempt = new_attempt(pointer)
|
|
elif is_connect_start(text):
|
|
if current_key is not None and attempts[current_key].events["connect_start"] == 0:
|
|
attempt = attempts[current_key]
|
|
else:
|
|
attempt = new_attempt(pointer)
|
|
elif is_socket_connect_start(text):
|
|
if current_key is None:
|
|
attempt = new_attempt(pointer)
|
|
else:
|
|
current_attempt = attempts[current_key]
|
|
if current_attempt.events["socket_connect_start"]:
|
|
attempt = new_attempt(pointer)
|
|
else:
|
|
attempt = current_attempt
|
|
elif current_key is None:
|
|
attempt = new_attempt(pointer)
|
|
else:
|
|
attempt = attempts[current_key]
|
|
attempt.add(line_no, text)
|
|
if "mtproxy_disconnect" in text:
|
|
active_key_by_pointer.pop(pointer, None)
|
|
|
|
return sorted(attempts.values(), key=lambda item: (item.first_line, item.key)), global_lines
|
|
|
|
|
|
def proxy_check_endpoint_phase_counts(lines: list[str]) -> defaultdict[str, Counter[str]]:
|
|
endpoint_phases: defaultdict[str, Counter[str]] = defaultdict(Counter)
|
|
proxy_checks: dict[str, dict[str, str | bool]] = {}
|
|
|
|
for text in lines:
|
|
if not PROXY_CHECK_RE.search(text):
|
|
continue
|
|
|
|
start = PROXY_CHECK_START_RE.search(text)
|
|
if start:
|
|
proxy_checks[start.group(1)] = {
|
|
"endpoint": start.group(2),
|
|
"socket_connected": False,
|
|
"close_reason": "",
|
|
}
|
|
|
|
socket = PROXY_CHECK_SOCKET_RE.search(text)
|
|
if socket:
|
|
proxy_checks.setdefault(socket.group(1), {"endpoint": "unknown", "socket_connected": False, "close_reason": ""})[
|
|
"socket_connected"
|
|
] = True
|
|
|
|
close_with_ping = PROXY_CHECK_CLOSE_WITH_PING_RE.search(text)
|
|
if close_with_ping:
|
|
proxy_checks.setdefault(close_with_ping.group(2), {"endpoint": "unknown", "socket_connected": False, "close_reason": ""})[
|
|
"close_reason"
|
|
] = close_with_ping.group(1)
|
|
|
|
finish = PROXY_CHECK_FINISH_RE.search(text)
|
|
if not finish:
|
|
continue
|
|
|
|
result_text = finish.group(1)
|
|
ping_id = finish.group(3)
|
|
endpoint_text = finish.group(4)
|
|
state = proxy_checks.setdefault(
|
|
ping_id,
|
|
{"endpoint": endpoint_text, "socket_connected": False, "close_reason": ""},
|
|
)
|
|
state["endpoint"] = endpoint_text
|
|
close_reason = state.get("close_reason") or "none"
|
|
if result_text == "ok":
|
|
phase = "ok"
|
|
elif (diagnostic := PROXY_CHECK_DIAGNOSTIC_RE.search(text)):
|
|
phase = diagnostic.group(1)
|
|
elif state.get("socket_connected"):
|
|
phase = "tcp_connected_no_pong"
|
|
else:
|
|
phase = "tcp_not_connected"
|
|
|
|
stats = endpoint_phases[endpoint_text]
|
|
stats["total"] += 1
|
|
stats[phase] += 1
|
|
if close_reason != "none":
|
|
stats[f"close_reason_{close_reason}"] += 1
|
|
|
|
return endpoint_phases
|
|
|
|
|
|
def proxy_check_phase_counts(lines: list[str]) -> Counter[str]:
|
|
phases: Counter[str] = Counter()
|
|
for stats in proxy_check_endpoint_phase_counts(lines).values():
|
|
for phase, count in stats.items():
|
|
if phase != "total" and not phase.startswith("close_reason_"):
|
|
phases[phase] += count
|
|
return phases
|
|
|
|
|
|
def scheduler_endpoint_stats(lines: list[str]) -> defaultdict[str, Counter[str]]:
|
|
by_endpoint: defaultdict[str, Counter[str]] = defaultdict(Counter)
|
|
|
|
for text in lines:
|
|
if "proxy_check_scheduler " not in text:
|
|
continue
|
|
endpoint = ENDPOINT_RE.search(text)
|
|
if not endpoint:
|
|
continue
|
|
endpoint_text = endpoint.group(1)
|
|
stats = by_endpoint[endpoint_text]
|
|
stats["total_events"] += 1
|
|
|
|
scheduler = PROXY_CHECK_SCHEDULER_RE.search(text)
|
|
event = scheduler.group(1) if scheduler else "unknown"
|
|
stats[event] += 1
|
|
|
|
result = SCHEDULER_RESULT_RE.search(text)
|
|
if result:
|
|
stats[f"finish_{result.group(1)}"] += 1
|
|
|
|
finish_phase = SCHEDULER_PHASE_RE.search(text) if event == "finish" else None
|
|
if finish_phase:
|
|
stats[f"finish_phase_{finish_phase.group(1)}"] += 1
|
|
for diagnostic in SCHEDULER_DIAGNOSTIC_RE.findall(text):
|
|
stats[f"diagnostic_{diagnostic}"] += 1
|
|
if not finish_phase:
|
|
state_phase = SCHEDULER_PHASE_RE.search(text)
|
|
if state_phase:
|
|
stats[f"diagnostic_{state_phase.group(1)}"] += 1
|
|
|
|
return by_endpoint
|
|
|
|
|
|
def print_proxy_check_summary(lines: list[str]) -> None:
|
|
native_events: Counter[str] = Counter()
|
|
native_results: Counter[str] = Counter()
|
|
native_endpoint_outcomes: Counter[str] = Counter()
|
|
native_start_failures: Counter[str] = Counter()
|
|
native_close_reasons: Counter[str] = Counter()
|
|
native_ignored_close_reasons: Counter[str] = Counter()
|
|
scheduler_events: Counter[str] = Counter()
|
|
scheduler_endpoints: Counter[str] = Counter()
|
|
scheduler_coalescing: Counter[str] = Counter()
|
|
scheduler_listener_peaks: dict[str, int] = {}
|
|
scheduler_force: Counter[str] = Counter()
|
|
scheduler_results: Counter[str] = Counter()
|
|
scheduler_preserved_connected: Counter[str] = Counter()
|
|
scheduler_applied_split: Counter[str] = Counter()
|
|
rotation_events: Counter[str] = Counter()
|
|
proxy_checks: dict[str, dict[str, str | bool]] = {}
|
|
|
|
for text in lines:
|
|
rotation = PROXY_ROTATION_RE.search(text)
|
|
if rotation:
|
|
rotation_events[rotation.group(1)] += 1
|
|
|
|
if "proxy_check_scheduler " in text:
|
|
scheduler = PROXY_CHECK_SCHEDULER_RE.search(text)
|
|
event = ""
|
|
if scheduler:
|
|
event = scheduler.group(1)
|
|
scheduler_events[event] += 1
|
|
endpoint = ENDPOINT_RE.search(text)
|
|
if endpoint:
|
|
endpoint_text = endpoint.group(1)
|
|
scheduler_endpoints[endpoint_text] += 1
|
|
listeners = SCHEDULER_LISTENERS_RE.search(text)
|
|
if listeners:
|
|
scheduler_listener_peaks[endpoint_text] = max(
|
|
scheduler_listener_peaks.get(endpoint_text, 0),
|
|
int(listeners.group(1)),
|
|
)
|
|
if event in {"attach_pending", "enqueue_now", "cancel_owner"}:
|
|
scheduler_coalescing[f"{event} endpoint={endpoint_text}"] += 1
|
|
force = SCHEDULER_FORCE_RE.search(text)
|
|
if force:
|
|
scheduler_force[force.group(1)] += 1
|
|
result = SCHEDULER_RESULT_RE.search(text)
|
|
if result:
|
|
scheduler_results[result.group(1)] += 1
|
|
applied = SCHEDULER_APPLIED_RE.search(text)
|
|
if applied and (applied.group(1) != applied.group(2) or applied.group(1) != applied.group(3)):
|
|
scheduler_applied_split[f"callback={applied.group(1)} applied={applied.group(2)} raw={applied.group(3)}"] += 1
|
|
if event == "finish_keep_connected":
|
|
endpoint = ENDPOINT_RE.search(text)
|
|
scheduler_preserved_connected[endpoint.group(1) if endpoint else "unknown"] += 1
|
|
continue
|
|
|
|
native = PROXY_CHECK_RE.search(text)
|
|
if native:
|
|
native_events[native.group(1)] += 1
|
|
start = PROXY_CHECK_START_RE.search(text)
|
|
if start:
|
|
proxy_checks[start.group(1)] = {
|
|
"endpoint": start.group(2),
|
|
"socket_connected": False,
|
|
"close_reason": "",
|
|
}
|
|
socket = PROXY_CHECK_SOCKET_RE.search(text)
|
|
if socket:
|
|
proxy_checks.setdefault(socket.group(1), {"endpoint": "unknown", "socket_connected": False, "close_reason": ""})[
|
|
"socket_connected"
|
|
] = True
|
|
close_with_ping = PROXY_CHECK_CLOSE_WITH_PING_RE.search(text)
|
|
if close_with_ping:
|
|
proxy_checks.setdefault(close_with_ping.group(2), {"endpoint": "unknown", "socket_connected": False, "close_reason": ""})[
|
|
"close_reason"
|
|
] = close_with_ping.group(1)
|
|
start_failed = PROXY_CHECK_START_FAILED_RE.search(text)
|
|
if start_failed:
|
|
native_start_failures[start_failed.group(1)] += 1
|
|
result = PROXY_CHECK_RESULT_RE.search(text)
|
|
if result:
|
|
native_results[f"{result.group(1)}:{result.group(2)}"] += 1
|
|
finish = PROXY_CHECK_FINISH_RE.search(text)
|
|
if finish:
|
|
result_text = finish.group(1)
|
|
reason_text = finish.group(2)
|
|
ping_id = finish.group(3)
|
|
endpoint_text = finish.group(4)
|
|
state = proxy_checks.setdefault(
|
|
ping_id,
|
|
{"endpoint": endpoint_text, "socket_connected": False, "close_reason": ""},
|
|
)
|
|
state["endpoint"] = endpoint_text
|
|
close_reason = state.get("close_reason") or "none"
|
|
if result_text == "ok":
|
|
outcome = f"{endpoint_text} ok:{reason_text}"
|
|
elif (diagnostic := PROXY_CHECK_DIAGNOSTIC_RE.search(text)):
|
|
outcome = f"{endpoint_text} fail:{diagnostic.group(1)} close_reason={close_reason}"
|
|
elif state.get("socket_connected"):
|
|
outcome = f"{endpoint_text} fail:tcp_connected_no_pong close_reason={close_reason}"
|
|
else:
|
|
outcome = f"{endpoint_text} fail:tcp_not_connected close_reason={close_reason}"
|
|
native_endpoint_outcomes[outcome] += 1
|
|
close = PROXY_CHECK_CLOSE_RE.search(text)
|
|
if close:
|
|
native_close_reasons[close.group(1)] += 1
|
|
ignored_close = PROXY_CHECK_IGNORED_CLOSE_RE.search(text)
|
|
if ignored_close:
|
|
native_ignored_close_reasons[ignored_close.group(1)] += 1
|
|
|
|
if not native_events and not scheduler_events and not rotation_events:
|
|
return
|
|
|
|
print()
|
|
print("Proxy-check lifecycle:")
|
|
if rotation_events:
|
|
print(" Rotation events:")
|
|
for event, count in rotation_events.most_common():
|
|
print(f" {event}: {count}")
|
|
if scheduler_events:
|
|
print(" Java scheduler events:")
|
|
for event, count in scheduler_events.most_common():
|
|
print(f" {event}: {count}")
|
|
if scheduler_coalescing:
|
|
print(" Scheduler coalescing:")
|
|
for item, count in scheduler_coalescing.most_common(10):
|
|
print(f" {item}: {count}")
|
|
if scheduler_listener_peaks:
|
|
print(" Scheduler listener peaks:")
|
|
for endpoint, count in sorted(scheduler_listener_peaks.items(), key=lambda item: (-item[1], item[0]))[:10]:
|
|
print(f" {endpoint}: {count}")
|
|
if scheduler_force:
|
|
print(" Scheduler force flags:")
|
|
for value, count in scheduler_force.most_common():
|
|
print(f" {value}: {count}")
|
|
if scheduler_results:
|
|
print(" Scheduler finish results:")
|
|
for result, count in scheduler_results.most_common():
|
|
print(f" {result}: {count}")
|
|
if scheduler_preserved_connected:
|
|
print(" Scheduler preserved connected state:")
|
|
for endpoint, count in scheduler_preserved_connected.most_common(10):
|
|
print(f" {endpoint}: {count}")
|
|
if scheduler_applied_split:
|
|
print(" Scheduler applied/callback split:")
|
|
for item, count in scheduler_applied_split.most_common(10):
|
|
print(f" {item}: {count}")
|
|
if native_events:
|
|
print(" Native events:")
|
|
for event, count in native_events.most_common():
|
|
print(f" {event}: {count}")
|
|
if native_results:
|
|
print(" Native finish results:")
|
|
for result, count in native_results.most_common():
|
|
print(f" {result}: {count}")
|
|
if native_endpoint_outcomes:
|
|
print(" Native endpoint outcomes:")
|
|
for result, count in native_endpoint_outcomes.most_common():
|
|
print(f" {result}: {count}")
|
|
if native_start_failures:
|
|
print(" Native start failures:")
|
|
for reason, count in native_start_failures.most_common():
|
|
print(f" {reason}: {count}")
|
|
if native_close_reasons:
|
|
print(" Native close reasons:")
|
|
for reason, count in native_close_reasons.most_common():
|
|
print(f" {reason}: {count}")
|
|
if native_ignored_close_reasons:
|
|
print(" Native ignored close reasons:")
|
|
for reason, count in native_ignored_close_reasons.most_common():
|
|
print(f" {reason}: {count}")
|
|
if scheduler_endpoints:
|
|
print(" Scheduler endpoints:")
|
|
for endpoint, count in scheduler_endpoints.most_common(10):
|
|
print(f" {endpoint}: {count}")
|
|
|
|
|
|
def print_java_live_stage_summary(lines: list[str]) -> None:
|
|
stages: Counter[str] = Counter()
|
|
accounts: Counter[str] = Counter()
|
|
for text in lines:
|
|
stage = PROXY_CONNECTION_STAGE_RE.search(text)
|
|
if not stage:
|
|
continue
|
|
accounts[stage.group(1)] += 1
|
|
stages[stage.group(2)] += 1
|
|
|
|
if not stages:
|
|
return
|
|
|
|
print()
|
|
print("Java live connection stages:")
|
|
print(" Phases:")
|
|
for phase, count in stages.most_common():
|
|
print(f" {phase}: {count}")
|
|
print(" Accounts:")
|
|
for account, count in accounts.most_common():
|
|
print(f" account{account}: {count}")
|
|
|
|
|
|
def print_proxy_control_summary(lines: list[str]) -> None:
|
|
decisions: Counter[str] = Counter()
|
|
for text in lines:
|
|
control = PROXY_CONTROL_RE.search(text)
|
|
if control:
|
|
decisions[control.group(1)] += 1
|
|
|
|
if not decisions:
|
|
return
|
|
|
|
print()
|
|
print("Java proxy control decisions:")
|
|
for decision, count in decisions.most_common():
|
|
print(f" {decision}: {count}")
|
|
|
|
|
|
def print_faketls_endpoint_summary(attempts: list[Attempt]) -> None:
|
|
attempts = faketls_attempts(attempts)
|
|
endpoint_verdicts: Counter[str] = Counter()
|
|
endpoint_profile_verdicts: Counter[str] = Counter()
|
|
for attempt in attempts:
|
|
endpoint = attempt.endpoint_text()
|
|
endpoint_verdicts[f"{endpoint} {attempt.verdict()}"] += 1
|
|
if attempt.profile or attempt.secret_kind == "ee":
|
|
endpoint_profile_verdicts[f"{endpoint} {profile_text(attempt)} {attempt.verdict()}"] += 1
|
|
|
|
if not endpoint_verdicts:
|
|
return
|
|
|
|
print()
|
|
print("FakeTLS endpoint phases:")
|
|
for item, count in endpoint_verdicts.most_common(30):
|
|
print(f" {item}: {count}")
|
|
|
|
if endpoint_profile_verdicts:
|
|
print()
|
|
print("FakeTLS endpoint/profile phases:")
|
|
for item, count in endpoint_profile_verdicts.most_common(30):
|
|
print(f" {item}: {count}")
|
|
|
|
|
|
def print_plain_mtproxy_summary(attempts: list[Attempt]) -> None:
|
|
plain_attempts = [
|
|
attempt
|
|
for attempt in attempts
|
|
if attempt.secret_kind and attempt.secret_kind != "ee" and not attempt.events["client_hello_sent"]
|
|
]
|
|
if not plain_attempts:
|
|
return
|
|
|
|
by_flow: defaultdict[tuple[str, str, str, str, str], Counter[str]] = defaultdict(Counter)
|
|
for attempt in plain_attempts:
|
|
key = (
|
|
attempt.endpoint_text(),
|
|
attempt.secret_kind,
|
|
attempt.account or "unknown",
|
|
attempt.dc or "unknown",
|
|
attempt.connection_type or "unknown",
|
|
)
|
|
stats = by_flow[key]
|
|
stats["socket_connected"] += attempt.events["socket_connected"]
|
|
stats["connected"] += 1 if attempt.events["on_connected"] or attempt.events["account_connected"] else 0
|
|
stats["first_packet_sent"] += attempt.events["first_mtproxy_packet_sent"]
|
|
stats["first_packet_recv"] += attempt.events["first_mtproxy_packet_recv"]
|
|
stats["packet_sent_no_response"] += attempt.events["mtproxy_packet_sent_no_response"]
|
|
stats["send"] += attempt.events["account_send_message"]
|
|
stats["recv"] += attempt.events["account_received_message"]
|
|
stats["rpc_result"] += attempt.events["account_rpc_result"]
|
|
stats["recv_eof"] += attempt.events["recv_eof"]
|
|
stats["socket_error"] += attempt.events["socket_error"]
|
|
stats["invalid_packet_length"] += attempt.events["account_invalid_packet_length"]
|
|
stats["auth_404"] += attempt.events["account_auth_404"]
|
|
for event, count in attempt.events.items():
|
|
if event.startswith("account_disconnect_"):
|
|
stats[event.replace("account_", "")] += count
|
|
|
|
if not by_flow:
|
|
return
|
|
|
|
print()
|
|
print("Plain MTProxy lifecycle:")
|
|
for (endpoint, secret_kind, account, dc, connection_type), stats in sorted(
|
|
by_flow.items(),
|
|
key=lambda item: (
|
|
-item[1]["send"],
|
|
item[0][0],
|
|
item[0][2],
|
|
item[0][3],
|
|
int(item[0][4]) if item[0][4].isdigit() else 0,
|
|
),
|
|
)[:40]:
|
|
parts = [
|
|
f"{endpoint} {secret_kind} account{account} dc{dc} type{connection_type}:",
|
|
f"socket_connected={stats['socket_connected']}",
|
|
f"connected={stats['connected']}",
|
|
f"first_packet_sent={stats['first_packet_sent']}",
|
|
f"first_packet_recv={stats['first_packet_recv']}",
|
|
f"packet_sent_no_response={stats['packet_sent_no_response']}",
|
|
f"send={stats['send']}",
|
|
f"recv={stats['recv']}",
|
|
f"rpc_result={stats['rpc_result']}",
|
|
]
|
|
if stats["recv_eof"]:
|
|
parts.append(f"recv_eof={stats['recv_eof']}")
|
|
if stats["socket_error"]:
|
|
parts.append(f"socket_error={stats['socket_error']}")
|
|
if stats["invalid_packet_length"]:
|
|
parts.append(f"invalid_packet_length={stats['invalid_packet_length']}")
|
|
if stats["auth_404"]:
|
|
parts.append(f"auth_404={stats['auth_404']}")
|
|
for event, count in sorted(stats.items()):
|
|
if event.startswith("disconnect_") and count:
|
|
parts.append(f"{event}={count}")
|
|
print(" " + " ".join(parts))
|
|
|
|
|
|
def print_layer_recommendations(attempts: list[Attempt], all_lines: list[str]) -> None:
|
|
proxy_check_phases = proxy_check_phase_counts(all_lines)
|
|
if not attempts and not proxy_check_phases:
|
|
return
|
|
|
|
verdicts = Counter(attempt.verdict() for attempt in attempts)
|
|
faketls = faketls_attempts(attempts)
|
|
faketls_verdicts = Counter(attempt.verdict() for attempt in faketls)
|
|
plain_attempts = [
|
|
attempt
|
|
for attempt in attempts
|
|
if attempt.secret_kind and attempt.secret_kind != "ee" and not attempt.events["client_hello_sent"]
|
|
]
|
|
plain_no_response = sum(attempt.events["mtproxy_packet_sent_no_response"] for attempt in plain_attempts)
|
|
tls_frames_completed = sum(attempt.completed_tls_frames() for attempt in faketls)
|
|
|
|
print()
|
|
print("Layer recommendations:")
|
|
print(
|
|
" "
|
|
f"dns_endpoint_stability host_resolve_failed={verdicts['host_resolve_failed']} "
|
|
f"host_resolve_timeout={verdicts['host_resolve_timeout']} "
|
|
f"connection_not_started={verdicts['connection_not_started']} "
|
|
f"admission_timeout={verdicts['admission_timeout']} "
|
|
f"endpoint_cooldown_timeout={verdicts['endpoint_cooldown_timeout']} "
|
|
f"dns_coalesce_timeout={verdicts['dns_coalesce_timeout']} "
|
|
f"tcp_connect_gate_timeout={verdicts['tcp_connect_gate_timeout']} "
|
|
f"tcp_not_connected={verdicts['tcp_not_connected']} "
|
|
f"proxy_check_tcp_not_connected={proxy_check_phases['tcp_not_connected']} "
|
|
f"proxy_check_tcp_connected_no_pong={proxy_check_phases['tcp_connected_no_pong']} "
|
|
"action=dns_cache_endpoint_circuit_breaker not_ja4_or_drs"
|
|
)
|
|
print(
|
|
" "
|
|
f"faketls_handshake_recipe faketls_server_hello_wait_timeout={faketls_verdicts['faketls_server_hello_wait_timeout']} "
|
|
f"server_closed_after_client_hello={faketls_verdicts['server_closed_after_client_hello']} "
|
|
f"true_client_hello_timeout={faketls_verdicts['true_client_hello_timeout']} "
|
|
f"client_hello_sent_no_server_hello={faketls_verdicts['client_hello_sent_no_server_hello']} "
|
|
f"tls_alert_after_client_hello={faketls_verdicts['tls_alert_after_client_hello']} "
|
|
f"short_tls_response_after_client_hello={faketls_verdicts['short_tls_response_after_client_hello']} "
|
|
f"unrecognized_tls_response_after_client_hello={faketls_verdicts['unrecognized_tls_response_after_client_hello']} "
|
|
f"server_hello_hmac_mismatch={faketls_verdicts['server_hello_hmac_mismatch']} "
|
|
f"tcp_open_no_server_hello_terminal={faketls_verdicts['faketls_no_server_hello_terminal']} "
|
|
f"tcp_open_server_closed_terminal={faketls_verdicts['faketls_server_closed_terminal']} "
|
|
f"tcp_open_bad_mtproxy_response={faketls_verdicts['faketls_not_mtproxy_response']} "
|
|
f"handshake_profiles_exhausted={faketls_verdicts['handshake_profiles_exhausted']} "
|
|
"action=bounded_faketls_budget_then_endpoint_backoff"
|
|
)
|
|
print(
|
|
" "
|
|
f"plain_dd_endpoint_backoff mtproxy_packet_sent_no_response={plain_no_response} "
|
|
"action=endpoint_backoff_fallback dd_no_ja4"
|
|
)
|
|
print(
|
|
" "
|
|
f"faketls_data_path post_handshake_no_appdata={faketls_verdicts['post_handshake_no_appdata']} "
|
|
f"dropped_early_after_appdata={faketls_verdicts['dropped_early_after_appdata']} "
|
|
f"dropped_after_appdata={faketls_verdicts['dropped_after_appdata']} "
|
|
f"tls_frames_completed={tls_frames_completed} "
|
|
"action=inspect_record_sizing_ipt_lifecycle"
|
|
)
|
|
|
|
|
|
def stage2_status(value: bool) -> str:
|
|
return "seen" if value else "missing"
|
|
|
|
|
|
def runtime_profile_exhaustion_recovered(attempts: list[Attempt], all_lines: list[str]) -> bool:
|
|
exhaustion_line = 0
|
|
exhaustion_endpoint = ""
|
|
for attempt in attempts:
|
|
if not (attempt.events["recipe_exhausted"] or attempt.events["handshake_profiles_exhausted"]):
|
|
continue
|
|
exhaustion_line = attempt.last_line
|
|
exhaustion_endpoint = attempt.endpoint_text()
|
|
break
|
|
if not exhaustion_line:
|
|
for index, line in enumerate(all_lines, start=1):
|
|
if "handshake_profiles_exhausted" in line:
|
|
exhaustion_line = index
|
|
exhaustion_endpoint = line_field(line, "endpoint") or endpoint_from_admission_key(line_field(line, "key"))
|
|
break
|
|
if not exhaustion_line:
|
|
return False
|
|
for attempt in attempts:
|
|
if attempt.first_line <= exhaustion_line:
|
|
continue
|
|
if exhaustion_endpoint and not attempt.endpoint_text().startswith(exhaustion_endpoint.split(":ee:", 1)[0]):
|
|
continue
|
|
if attempt.events["connect_start"] or attempt.events["client_hello_sent"]:
|
|
return True
|
|
return any(
|
|
"decision=backoff" in line and "phase=handshake_profiles_exhausted" in line
|
|
for line in all_lines
|
|
)
|
|
|
|
|
|
def print_runtime_proof_summary(attempts: list[Attempt], all_lines: list[str]) -> None:
|
|
faketls = faketls_attempts(attempts)
|
|
proof = {
|
|
"source_contract_ok": "requires_source_guards",
|
|
"runtime_client_hello_seen": stage2_status(any(attempt.events["client_hello_sent"] for attempt in faketls)),
|
|
"runtime_server_hello_hmac_ok_seen": stage2_status(any(attempt.events["server_hello_hmac_ok"] for attempt in faketls)),
|
|
"runtime_first_tls_app_recv_seen": stage2_status(any(attempt.events["first_tls_app_recv"] for attempt in faketls)),
|
|
"runtime_profile_exhaustion_recovered": stage2_status(runtime_profile_exhaustion_recovered(faketls, all_lines)),
|
|
"runtime_post_handshake_no_appdata_seen": stage2_status(any(attempt.verdict() == "post_handshake_no_appdata" for attempt in faketls)),
|
|
}
|
|
missing = [name for name, status in proof.items() if status == "missing"]
|
|
print()
|
|
print("Stage 2 runtime proof:")
|
|
for name, status in proof.items():
|
|
print(f" {name}={status}")
|
|
print(f" missing={','.join(missing) if missing else 'none'}")
|
|
|
|
|
|
def print_faketls_profile_summary(attempts: list[Attempt]) -> None:
|
|
profile_verdicts: Counter[str] = Counter()
|
|
profile_hmac_ms: defaultdict[str, list[int]] = defaultdict(list)
|
|
for attempt in attempts:
|
|
if not attempt.is_faketls() or (attempt.secret_kind and attempt.secret_kind != "ee"):
|
|
continue
|
|
profile = profile_text(attempt)
|
|
profile_verdicts[f"{profile} {attempt.verdict()}"] += 1
|
|
hmac_ms = attempt.timing_ms("client_hello_sent", "server_hello_hmac_ok")
|
|
if hmac_ms:
|
|
profile_hmac_ms[profile].append(int(hmac_ms))
|
|
|
|
if not profile_verdicts:
|
|
return
|
|
|
|
print()
|
|
print("FakeTLS profile phases:")
|
|
for item, count in profile_verdicts.most_common():
|
|
print(f" {item}: {count}")
|
|
|
|
if profile_hmac_ms:
|
|
print()
|
|
print("FakeTLS profile HMAC latency:")
|
|
for profile, values in sorted(profile_hmac_ms.items()):
|
|
values.sort()
|
|
mid = values[len(values) // 2]
|
|
print(f" {profile}: n={len(values)} min={values[0]}ms p50={mid}ms max={values[-1]}ms")
|
|
|
|
|
|
def faketls_attempts(attempts: list[Attempt]) -> list[Attempt]:
|
|
return [
|
|
attempt
|
|
for attempt in attempts
|
|
if attempt.is_faketls() and (not attempt.secret_kind or attempt.secret_kind == "ee")
|
|
]
|
|
|
|
|
|
def percentile(values: list[int], percent: int) -> str:
|
|
if not values:
|
|
return ""
|
|
values = sorted(values)
|
|
index = round((len(values) - 1) * percent / 100)
|
|
return str(values[index])
|
|
|
|
|
|
def ok_percent(ok: int, total: int) -> str:
|
|
if total <= 0:
|
|
return "0%"
|
|
return f"{round(ok * 100 / total)}%"
|
|
|
|
|
|
def max_attempts_in_window(attempts: list[Attempt], seconds: float) -> int:
|
|
times = sorted(attempt.first_seconds for attempt in attempts if attempt.first_seconds > 0)
|
|
if not times:
|
|
return 0
|
|
best = 1
|
|
left = 0
|
|
for right, value in enumerate(times):
|
|
while value - times[left] > seconds:
|
|
left += 1
|
|
best = max(best, right - left + 1)
|
|
return best
|
|
|
|
|
|
def top_failures(verdicts: Counter[str]) -> str:
|
|
failures = [
|
|
f"{verdict}={count}"
|
|
for verdict, count in verdicts.most_common()
|
|
if verdict not in NON_FAILURE_VERDICTS
|
|
]
|
|
return ", ".join(failures[:3]) if failures else "none"
|
|
|
|
|
|
def failure_count(verdicts: Counter[str]) -> int:
|
|
return sum(count for verdict, count in verdicts.items() if verdict not in NON_FAILURE_VERDICTS)
|
|
|
|
|
|
def print_faketls_reliability_summary(attempts: list[Attempt]) -> None:
|
|
attempts = faketls_attempts(attempts)
|
|
if not attempts:
|
|
return
|
|
|
|
by_profile: defaultdict[str, list[Attempt]] = defaultdict(list)
|
|
by_endpoint_profile: defaultdict[tuple[str, str], list[Attempt]] = defaultdict(list)
|
|
by_endpoint: defaultdict[str, list[Attempt]] = defaultdict(list)
|
|
by_connection_pattern: defaultdict[str, list[Attempt]] = defaultdict(list)
|
|
for attempt in attempts:
|
|
profile = profile_text(attempt)
|
|
endpoint = attempt.endpoint_text()
|
|
by_profile[profile].append(attempt)
|
|
by_endpoint_profile[(endpoint, profile)].append(attempt)
|
|
by_endpoint[endpoint].append(attempt)
|
|
by_connection_pattern[attempt.connection_pattern or "unknown"].append(attempt)
|
|
|
|
print()
|
|
print("FakeTLS reliability:")
|
|
print(" By profile:")
|
|
for profile, items in sorted(by_profile.items(), key=lambda item: (-len(item[1]), item[0])):
|
|
verdicts = Counter(item.verdict() for item in items)
|
|
hmac_values = [
|
|
int(value)
|
|
for item in items
|
|
if (value := item.timing_ms("client_hello_sent", "server_hello_hmac_ok"))
|
|
]
|
|
print(
|
|
" "
|
|
f"{profile}: total={len(items)} ok={verdicts['ok']} ok_rate={ok_percent(verdicts['ok'], len(items))} "
|
|
f"idle_handshake={verdicts['handshake_ok_no_appdata_sent']} "
|
|
f"true_client_hello_timeout={verdicts['true_client_hello_timeout']} "
|
|
f"pre_server_hello_legacy={verdicts['client_hello_sent_no_server_hello']} "
|
|
f"tls_alert={verdicts['tls_alert_after_client_hello']} "
|
|
f"short_tls={verdicts['short_tls_response_after_client_hello']} "
|
|
f"unrecognized_tls={verdicts['unrecognized_tls_response_after_client_hello']} "
|
|
f"no_server_hello_terminal={verdicts['faketls_no_server_hello_terminal']} "
|
|
f"server_closed_terminal={verdicts['faketls_server_closed_terminal']} "
|
|
f"bad_mtproxy_response={verdicts['faketls_not_mtproxy_response']} "
|
|
f"post_handshake={verdicts['post_handshake_no_appdata']} "
|
|
f"early_drop={verdicts['dropped_early_after_appdata']} "
|
|
f"tls_frames={sum(item.completed_tls_frames() for item in items)} "
|
|
f"hmac_fail={verdicts['server_hello_hmac_mismatch']} "
|
|
f"hmac_p50={percentile(hmac_values, 50) or '-'}ms"
|
|
)
|
|
|
|
if by_connection_pattern:
|
|
print(" By connection pattern:")
|
|
for pattern, items in sorted(by_connection_pattern.items(), key=lambda item: (-len(item[1]), item[0])):
|
|
verdicts = Counter(item.verdict() for item in items)
|
|
print(
|
|
" "
|
|
f"{pattern}: total={len(items)} ok={verdicts['ok']} "
|
|
f"ok_rate={ok_percent(verdicts['ok'], len(items))} "
|
|
f"max_1s={max_attempts_in_window(items, 1.0)} "
|
|
f"max_5s={max_attempts_in_window(items, 5.0)} "
|
|
f"{top_failures(verdicts)}"
|
|
)
|
|
|
|
suspicious = []
|
|
for (endpoint, profile), items in by_endpoint_profile.items():
|
|
verdicts = Counter(item.verdict() for item in items)
|
|
failures = failure_count(verdicts)
|
|
if len(items) < 2 or failures == 0:
|
|
continue
|
|
suspicious.append((failures, len(items), endpoint, profile, verdicts))
|
|
suspicious.sort(key=lambda item: (-item[0], -item[1], item[2], item[3]))
|
|
if suspicious:
|
|
print(" Top failing endpoint/profile clusters:")
|
|
for failures, total, endpoint, profile, verdicts in suspicious[:20]:
|
|
print(
|
|
" "
|
|
f"{endpoint} {profile}: total={total} ok={verdicts['ok']} "
|
|
f"ok_rate={ok_percent(verdicts['ok'], total)} failures={failures} "
|
|
f"{top_failures(verdicts)}"
|
|
)
|
|
|
|
burst_rows = []
|
|
for endpoint, items in by_endpoint.items():
|
|
one_second = max_attempts_in_window(items, 1.0)
|
|
five_seconds = max_attempts_in_window(items, 5.0)
|
|
if one_second >= 2 or five_seconds >= 3:
|
|
verdicts = Counter(item.verdict() for item in items)
|
|
profiles = Counter(profile_text(item) for item in items)
|
|
patterns = Counter((item.connection_pattern or "unknown") for item in items)
|
|
burst_rows.append((five_seconds, one_second, len(items), endpoint, verdicts, profiles, patterns))
|
|
burst_rows.sort(key=lambda item: (-item[0], -item[1], -item[2], item[3]))
|
|
if burst_rows:
|
|
print(" Endpoint handshake bursts:")
|
|
for five_seconds, one_second, total, endpoint, verdicts, profiles, patterns in burst_rows[:20]:
|
|
profile_mix = ", ".join(f"{profile}={count}" for profile, count in profiles.most_common(3))
|
|
pattern_mix = ", ".join(f"{pattern}={count}" for pattern, count in patterns.most_common(3))
|
|
print(
|
|
" "
|
|
f"{endpoint}: total={total} max_1s={one_second} max_5s={five_seconds} "
|
|
f"ok={verdicts['ok']} failures={failure_count(verdicts)} profiles={profile_mix} patterns={pattern_mix}"
|
|
)
|
|
|
|
|
|
def print_faketls_failure_timeline(attempts: list[Attempt]) -> None:
|
|
interesting = [
|
|
attempt
|
|
for attempt in attempts
|
|
if attempt.is_faketls()
|
|
and (not attempt.secret_kind or attempt.secret_kind == "ee")
|
|
and attempt.verdict()
|
|
in {
|
|
"true_client_hello_timeout",
|
|
"client_hello_sent_no_server_hello",
|
|
"faketls_no_server_hello_terminal",
|
|
"server_closed_after_client_hello",
|
|
"faketls_server_closed_terminal",
|
|
"tls_alert_after_client_hello",
|
|
"short_tls_response_after_client_hello",
|
|
"unrecognized_tls_response_after_client_hello",
|
|
"server_hello_hmac_mismatch",
|
|
"faketls_not_mtproxy_response",
|
|
"handshake_profiles_exhausted",
|
|
"post_handshake_no_appdata",
|
|
}
|
|
]
|
|
if not interesting:
|
|
return
|
|
|
|
print()
|
|
print("FakeTLS failure timeline:")
|
|
for attempt in interesting[:80]:
|
|
print(f" {attempt.compact()}")
|
|
|
|
|
|
def write_csv_reports(attempts: list[Attempt], global_lines: list[str], out_dir: Path) -> None:
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
all_attempts = attempts
|
|
all_lines = list(global_lines)
|
|
for attempt in all_attempts:
|
|
all_lines.extend(attempt.lines)
|
|
attempts = faketls_attempts(attempts)
|
|
|
|
attempts_path = out_dir / "mtproxy_attempts.csv"
|
|
with attempts_path.open("w", encoding="utf-8", newline="") as handle:
|
|
writer = csv.DictWriter(
|
|
handle,
|
|
fieldnames=[
|
|
"time",
|
|
"line_start",
|
|
"line_end",
|
|
"connection",
|
|
"endpoint",
|
|
"profile",
|
|
"profile_id",
|
|
"connection_pattern",
|
|
"secret_kind",
|
|
"hello_bytes",
|
|
"client_hello_bytes",
|
|
"verdict",
|
|
"evidence",
|
|
"connection_type",
|
|
"priority",
|
|
"tcp_ms",
|
|
"tcp_close_ms",
|
|
"dns_close_ms",
|
|
"hmac_ms",
|
|
"app_recv_ms",
|
|
"tls_frames_completed",
|
|
"disconnect_reason",
|
|
"disconnect_error",
|
|
"events",
|
|
],
|
|
)
|
|
writer.writeheader()
|
|
for attempt in attempts:
|
|
writer.writerow(
|
|
{
|
|
"time": attempt.first_time,
|
|
"line_start": attempt.first_line,
|
|
"line_end": attempt.last_line,
|
|
"connection": attempt.key,
|
|
"endpoint": attempt.endpoint_text(),
|
|
"profile": profile_text(attempt),
|
|
"profile_id": attempt.profile_id,
|
|
"connection_pattern": attempt.connection_pattern,
|
|
"secret_kind": attempt.secret_kind,
|
|
"hello_bytes": attempt.hello_bytes,
|
|
"client_hello_bytes": attempt.client_hello_bytes,
|
|
"verdict": attempt.verdict(),
|
|
"evidence": attempt.evidence(),
|
|
"connection_type": attempt.connection_type,
|
|
"priority": attempt.priority,
|
|
"tcp_ms": attempt.timing_ms("socket_connect_start", "socket_connected"),
|
|
"tcp_close_ms": attempt.timing_ms("socket_connect_start", "mtproxy_disconnect"),
|
|
"dns_close_ms": attempt.timing_ms("host_resolve_start", "mtproxy_disconnect"),
|
|
"hmac_ms": attempt.timing_ms("client_hello_sent", "server_hello_hmac_ok"),
|
|
"app_recv_ms": attempt.timing_ms("first_tls_app_sent", "first_tls_app_recv"),
|
|
"tls_frames_completed": attempt.completed_tls_frames(),
|
|
"disconnect_reason": attempt.disconnect_reason,
|
|
"disconnect_error": attempt.disconnect_error,
|
|
"events": ",".join(sorted(attempt.events)),
|
|
}
|
|
)
|
|
|
|
by_endpoint_profile: defaultdict[tuple[str, str], list[Attempt]] = defaultdict(list)
|
|
for attempt in attempts:
|
|
by_endpoint_profile[(attempt.endpoint_text(), profile_text(attempt))].append(attempt)
|
|
|
|
stats_path = out_dir / "mtproxy_endpoint_profile_stats.csv"
|
|
verdict_columns = sorted({attempt.verdict() for attempt in attempts} | FAKETLS_FAILURE_VERDICTS | {"ok"})
|
|
with stats_path.open("w", encoding="utf-8", newline="") as handle:
|
|
fieldnames = [
|
|
"endpoint",
|
|
"profile",
|
|
"total",
|
|
"ok",
|
|
"ok_percent",
|
|
"pre_server_hello",
|
|
"pre_server_hello_legacy",
|
|
"post_handshake",
|
|
"early_drop",
|
|
"tls_frames_completed",
|
|
"hmac_fail",
|
|
"hmac_min_ms",
|
|
"hmac_p50_ms",
|
|
"hmac_max_ms",
|
|
"max_1s",
|
|
"max_5s",
|
|
"top_failures",
|
|
*verdict_columns,
|
|
]
|
|
writer = csv.DictWriter(handle, fieldnames=fieldnames)
|
|
writer.writeheader()
|
|
for (endpoint, profile), items in sorted(
|
|
by_endpoint_profile.items(),
|
|
key=lambda item: (-len(item[1]), item[0][0], item[0][1]),
|
|
):
|
|
verdicts = Counter(item.verdict() for item in items)
|
|
hmac_values = [
|
|
int(value)
|
|
for item in items
|
|
if (value := item.timing_ms("client_hello_sent", "server_hello_hmac_ok"))
|
|
]
|
|
row = {
|
|
"endpoint": endpoint,
|
|
"profile": profile,
|
|
"total": len(items),
|
|
"ok": verdicts["ok"],
|
|
"ok_percent": ok_percent(verdicts["ok"], len(items)),
|
|
"pre_server_hello": (
|
|
verdicts["faketls_server_hello_wait_timeout"]
|
|
+ verdicts["faketls_no_server_hello_terminal"]
|
|
+ verdicts["server_closed_after_client_hello"]
|
|
+ verdicts["faketls_server_closed_terminal"]
|
|
+ verdicts["true_client_hello_timeout"]
|
|
+ verdicts["client_hello_sent_no_server_hello"]
|
|
),
|
|
"faketls_server_hello_wait_timeout": verdicts["faketls_server_hello_wait_timeout"],
|
|
"server_closed_after_client_hello": verdicts["server_closed_after_client_hello"],
|
|
"true_client_hello_timeout": verdicts["true_client_hello_timeout"],
|
|
"pre_server_hello_legacy": verdicts["client_hello_sent_no_server_hello"],
|
|
"tls_alert_after_client_hello": verdicts["tls_alert_after_client_hello"],
|
|
"short_tls_response_after_client_hello": verdicts["short_tls_response_after_client_hello"],
|
|
"unrecognized_tls_response_after_client_hello": verdicts["unrecognized_tls_response_after_client_hello"],
|
|
"faketls_no_server_hello_terminal": verdicts["faketls_no_server_hello_terminal"],
|
|
"faketls_server_closed_terminal": verdicts["faketls_server_closed_terminal"],
|
|
"faketls_not_mtproxy_response": verdicts["faketls_not_mtproxy_response"],
|
|
"post_handshake": verdicts["post_handshake_no_appdata"],
|
|
"early_drop": verdicts["dropped_early_after_appdata"],
|
|
"tls_frames_completed": sum(item.completed_tls_frames() for item in items),
|
|
"hmac_fail": verdicts["server_hello_hmac_mismatch"] + verdicts["faketls_not_mtproxy_response"],
|
|
"hmac_min_ms": percentile(hmac_values, 0),
|
|
"hmac_p50_ms": percentile(hmac_values, 50),
|
|
"hmac_max_ms": percentile(hmac_values, 100),
|
|
"max_1s": max_attempts_in_window(items, 1.0),
|
|
"max_5s": max_attempts_in_window(items, 5.0),
|
|
"top_failures": top_failures(verdicts),
|
|
}
|
|
for verdict in verdict_columns:
|
|
row[verdict] = verdicts[verdict]
|
|
writer.writerow(row)
|
|
|
|
plain_rows: defaultdict[tuple[str, str, str, str, str], Counter[str]] = defaultdict(Counter)
|
|
for attempt in all_attempts:
|
|
if not attempt.secret_kind or attempt.secret_kind == "ee" or attempt.events["client_hello_sent"]:
|
|
continue
|
|
key = (
|
|
attempt.endpoint_text(),
|
|
attempt.secret_kind,
|
|
attempt.account or "unknown",
|
|
attempt.dc or "unknown",
|
|
attempt.connection_type or "unknown",
|
|
)
|
|
stats = plain_rows[key]
|
|
stats["socket_connected"] += attempt.events["socket_connected"]
|
|
stats["connected"] += 1 if attempt.events["on_connected"] or attempt.events["account_connected"] else 0
|
|
stats["first_packet_sent"] += attempt.events["first_mtproxy_packet_sent"]
|
|
stats["first_packet_recv"] += attempt.events["first_mtproxy_packet_recv"]
|
|
stats["packet_sent_no_response"] += attempt.events["mtproxy_packet_sent_no_response"]
|
|
stats["send"] += attempt.events["account_send_message"]
|
|
stats["recv"] += attempt.events["account_received_message"]
|
|
stats["rpc_result"] += attempt.events["account_rpc_result"]
|
|
stats["recv_eof"] += attempt.events["recv_eof"]
|
|
stats["socket_error"] += attempt.events["socket_error"]
|
|
stats["invalid_packet_length"] += attempt.events["account_invalid_packet_length"]
|
|
stats["auth_404"] += attempt.events["account_auth_404"]
|
|
for event, count in attempt.events.items():
|
|
if event.startswith("account_disconnect_"):
|
|
stats[event.replace("account_", "")] += count
|
|
|
|
plain_path = out_dir / "mtproxy_plain_account_stats.csv"
|
|
with plain_path.open("w", encoding="utf-8", newline="") as handle:
|
|
fieldnames = [
|
|
"endpoint",
|
|
"secret_kind",
|
|
"account",
|
|
"dc",
|
|
"connection_type",
|
|
"socket_connected",
|
|
"connected",
|
|
"first_packet_sent",
|
|
"first_packet_recv",
|
|
"packet_sent_no_response",
|
|
"send",
|
|
"recv",
|
|
"rpc_result",
|
|
"recv_eof",
|
|
"socket_error",
|
|
"invalid_packet_length",
|
|
"auth_404",
|
|
"disconnect_0",
|
|
"disconnect_1",
|
|
"disconnect_2",
|
|
]
|
|
writer = csv.DictWriter(handle, fieldnames=fieldnames)
|
|
writer.writeheader()
|
|
for (endpoint, secret_kind, account, dc, connection_type), stats in sorted(
|
|
plain_rows.items(),
|
|
key=lambda item: (-item[1]["send"], item[0]),
|
|
):
|
|
writer.writerow(
|
|
{
|
|
"endpoint": endpoint,
|
|
"secret_kind": secret_kind,
|
|
"account": account,
|
|
"dc": dc,
|
|
"connection_type": connection_type,
|
|
"socket_connected": stats["socket_connected"],
|
|
"connected": stats["connected"],
|
|
"first_packet_sent": stats["first_packet_sent"],
|
|
"first_packet_recv": stats["first_packet_recv"],
|
|
"packet_sent_no_response": stats["packet_sent_no_response"],
|
|
"send": stats["send"],
|
|
"recv": stats["recv"],
|
|
"rpc_result": stats["rpc_result"],
|
|
"recv_eof": stats["recv_eof"],
|
|
"socket_error": stats["socket_error"],
|
|
"invalid_packet_length": stats["invalid_packet_length"],
|
|
"auth_404": stats["auth_404"],
|
|
"disconnect_0": stats["disconnect_0"],
|
|
"disconnect_1": stats["disconnect_1"],
|
|
"disconnect_2": stats["disconnect_2"],
|
|
}
|
|
)
|
|
|
|
proxy_check_stats = proxy_check_endpoint_phase_counts(all_lines)
|
|
proxy_check_path = out_dir / "mtproxy_proxy_check_stats.csv"
|
|
proxy_check_phase_columns = sorted(
|
|
{
|
|
phase
|
|
for stats in proxy_check_stats.values()
|
|
for phase in stats
|
|
if phase != "total" and not phase.startswith("close_reason_")
|
|
}
|
|
| {
|
|
"ok",
|
|
"connection_not_started",
|
|
"admission_timeout",
|
|
"endpoint_cooldown_timeout",
|
|
"dns_coalesce_timeout",
|
|
"tcp_connect_gate_timeout",
|
|
"tcp_not_connected",
|
|
"tcp_connected_no_pong",
|
|
"host_resolve_failed",
|
|
"host_resolve_timeout",
|
|
"network_block_suspected",
|
|
"mtproxy_packet_sent_no_response",
|
|
"unknown_fail",
|
|
}
|
|
)
|
|
proxy_check_close_columns = sorted(
|
|
{
|
|
phase
|
|
for stats in proxy_check_stats.values()
|
|
for phase in stats
|
|
if phase.startswith("close_reason_")
|
|
}
|
|
)
|
|
with proxy_check_path.open("w", encoding="utf-8", newline="") as handle:
|
|
fieldnames = [
|
|
"endpoint",
|
|
"total",
|
|
"ok_percent",
|
|
*proxy_check_phase_columns,
|
|
*proxy_check_close_columns,
|
|
]
|
|
writer = csv.DictWriter(handle, fieldnames=fieldnames)
|
|
writer.writeheader()
|
|
for endpoint, stats in sorted(
|
|
proxy_check_stats.items(),
|
|
key=lambda item: (-item[1]["total"], item[0]),
|
|
):
|
|
total = stats["total"]
|
|
row = {
|
|
"endpoint": endpoint,
|
|
"total": total,
|
|
"ok_percent": ok_percent(stats["ok"], total),
|
|
}
|
|
for column in proxy_check_phase_columns:
|
|
row[column] = stats[column]
|
|
for column in proxy_check_close_columns:
|
|
row[column] = stats[column]
|
|
writer.writerow(row)
|
|
|
|
scheduler_stats = scheduler_endpoint_stats(all_lines)
|
|
scheduler_path = out_dir / "mtproxy_scheduler_stats.csv"
|
|
scheduler_event_columns = sorted(
|
|
{
|
|
event
|
|
for stats in scheduler_stats.values()
|
|
for event in stats
|
|
if event != "total_events"
|
|
and not event.startswith("finish_phase_")
|
|
and not event.startswith("diagnostic_")
|
|
}
|
|
| {
|
|
"enqueue",
|
|
"enqueue_now",
|
|
"attach_pending",
|
|
"start",
|
|
"finish",
|
|
"finish_ok",
|
|
"finish_fail",
|
|
"backoff",
|
|
"skip_backoff",
|
|
"skip_fresh",
|
|
"finish_keep_connected",
|
|
"cancel_owner",
|
|
"live_failure_dedup",
|
|
}
|
|
)
|
|
scheduler_finish_phase_columns = sorted(
|
|
{
|
|
event
|
|
for stats in scheduler_stats.values()
|
|
for event in stats
|
|
if event.startswith("finish_phase_")
|
|
}
|
|
)
|
|
scheduler_diagnostic_columns = sorted(
|
|
{
|
|
event
|
|
for stats in scheduler_stats.values()
|
|
for event in stats
|
|
if event.startswith("diagnostic_")
|
|
}
|
|
)
|
|
with scheduler_path.open("w", encoding="utf-8", newline="") as handle:
|
|
fieldnames = [
|
|
"endpoint",
|
|
"total_events",
|
|
*scheduler_event_columns,
|
|
*scheduler_finish_phase_columns,
|
|
*scheduler_diagnostic_columns,
|
|
]
|
|
writer = csv.DictWriter(handle, fieldnames=fieldnames)
|
|
writer.writeheader()
|
|
for endpoint, stats in sorted(
|
|
scheduler_stats.items(),
|
|
key=lambda item: (-item[1]["total_events"], item[0]),
|
|
):
|
|
row = {
|
|
"endpoint": endpoint,
|
|
"total_events": stats["total_events"],
|
|
}
|
|
for column in scheduler_event_columns:
|
|
row[column] = stats[column]
|
|
for column in scheduler_finish_phase_columns:
|
|
row[column] = stats[column]
|
|
for column in scheduler_diagnostic_columns:
|
|
row[column] = stats[column]
|
|
writer.writerow(row)
|
|
|
|
|
|
ANOMALY_RECONNECT_STORM_PER_SECOND = 10
|
|
ANOMALY_LOG_FLOOD_THRESHOLD = 500
|
|
PRE_IO_TERMINAL_DECISION_MARKERS = (
|
|
"probe_faketls_budget_backoff",
|
|
"probe_profiles_exhausted",
|
|
)
|
|
|
|
|
|
def print_anomaly_summary(all_lines: list[str]) -> None:
|
|
"""Loud summary of pathological patterns the flat counters hide:
|
|
reconnect storms (one connection re-dialing many times per second),
|
|
pre-I/O terminal verdicts clobbered to connection_not_started at close
|
|
(the backoff-disabling bug class), and log floods."""
|
|
dial_times: defaultdict[str, list[float]] = defaultdict(list)
|
|
events: defaultdict[str, list[tuple[float, str]]] = defaultdict(list)
|
|
flood: Counter[str] = Counter()
|
|
for text in all_lines:
|
|
seconds = log_time_seconds(text)
|
|
connection = CONNECTION_RE.search(text)
|
|
pointer = connection.group(1) if connection else ""
|
|
if pointer and is_proxy_connect(text) and seconds > 0:
|
|
dial_times[pointer].append(seconds)
|
|
if pointer:
|
|
if any(marker in text for marker in PRE_IO_TERMINAL_DECISION_MARKERS):
|
|
events[pointer].append((seconds, "terminal"))
|
|
elif is_proxy_connect(text):
|
|
events[pointer].append((seconds, "connect"))
|
|
elif " close_diagnostic phase=connection_not_started" in text:
|
|
events[pointer].append((seconds, "clobbered_close"))
|
|
signature = TIME_RE.sub("", text)
|
|
signature = CONNECTION_RE.sub("connection(", signature)
|
|
flood[signature.strip()] += 1
|
|
|
|
anomalies: list[str] = []
|
|
|
|
for pointer, times in dial_times.items():
|
|
times.sort()
|
|
peak = 0
|
|
peak_at = 0.0
|
|
start = 0
|
|
for index, moment in enumerate(times):
|
|
while moment - times[start] > 1.0:
|
|
start += 1
|
|
window = index - start + 1
|
|
if window > peak:
|
|
peak = window
|
|
peak_at = moment
|
|
if peak > ANOMALY_RECONNECT_STORM_PER_SECOND:
|
|
anomalies.append(
|
|
f"reconnect_storm connection={pointer} peak={peak}/s "
|
|
f"around t+{peak_at:.3f}s total_dials={len(times)}"
|
|
)
|
|
|
|
clobbered = 0
|
|
clobber_example = ""
|
|
for pointer, pointer_events in events.items():
|
|
pointer_events.sort(key=lambda item: item[0])
|
|
pending_terminal = False
|
|
for _, kind in pointer_events:
|
|
if kind == "terminal":
|
|
pending_terminal = True
|
|
elif kind == "connect":
|
|
pending_terminal = False
|
|
elif kind == "clobbered_close" and pending_terminal:
|
|
pending_terminal = False
|
|
clobbered += 1
|
|
if not clobber_example:
|
|
clobber_example = pointer
|
|
if clobbered:
|
|
anomalies.append(
|
|
f"pre_io_terminal_clobber count={clobbered} example_connection={clobber_example} "
|
|
"(terminal verdict re-derived to connection_not_started at close -> no cooldown, "
|
|
"no reconnect backoff; see deriveMtProxyTerminalDiagnostic preserve list)"
|
|
)
|
|
|
|
for signature, count in flood.most_common(3):
|
|
if count < ANOMALY_LOG_FLOOD_THRESHOLD:
|
|
break
|
|
anomalies.append(f"log_flood count={count} marker=\"{signature[:120]}\"")
|
|
|
|
print()
|
|
print("Anomalies:")
|
|
if anomalies:
|
|
for anomaly in anomalies:
|
|
print(f" {anomaly}")
|
|
else:
|
|
print(" none detected")
|
|
|
|
|
|
def print_report(attempts: list[Attempt], global_lines: list[str]) -> None:
|
|
print("MTProxy FakeTLS diagnostic summary")
|
|
print("===================================")
|
|
if not attempts and not global_lines:
|
|
print("No MTProxy markers found.")
|
|
print("Most likely causes: APK was built without LOGS_ENABLED, wrong package was captured, or the MTProxy path was not exercised.")
|
|
return
|
|
|
|
verdicts = Counter(attempt.verdict() for attempt in attempts)
|
|
profiles = Counter(profile_text(attempt) for attempt in attempts)
|
|
print(f"Attempts: {len(attempts)}")
|
|
print(f"FakeTLS attempts: {sum(1 for attempt in attempts if attempt.is_faketls())}")
|
|
print("Verdicts:")
|
|
for verdict, count in verdicts.most_common():
|
|
print(f" {verdict}: {count}")
|
|
print("Profiles:")
|
|
for profile, count in profiles.most_common():
|
|
print(f" {profile}: {count}")
|
|
|
|
if global_lines:
|
|
print(f"Global/non-connection markers: {len(global_lines)}")
|
|
|
|
print_faketls_reliability_summary(attempts)
|
|
print_faketls_profile_summary(attempts)
|
|
print_faketls_endpoint_summary(attempts)
|
|
print_faketls_failure_timeline(attempts)
|
|
print_plain_mtproxy_summary(attempts)
|
|
|
|
all_lines = list(global_lines)
|
|
for attempt in attempts:
|
|
all_lines.extend(attempt.lines)
|
|
print_anomaly_summary(all_lines)
|
|
print_runtime_proof_summary(attempts, all_lines)
|
|
print_layer_recommendations(attempts, all_lines)
|
|
print_java_live_stage_summary(all_lines)
|
|
print_proxy_control_summary(all_lines)
|
|
print_proxy_check_summary(all_lines)
|
|
|
|
print()
|
|
print("Per-attempt details:")
|
|
for attempt in attempts:
|
|
endpoint = f" {attempt.endpoint_text()}"
|
|
flags = ",".join(sorted(attempt.events)) or "no_known_phase"
|
|
extra = []
|
|
if attempt.hello_bytes:
|
|
extra.append(f"hello={attempt.hello_bytes}")
|
|
if attempt.connection_type:
|
|
extra.append(f"type={attempt.connection_type}")
|
|
if attempt.priority:
|
|
extra.append(f"priority={attempt.priority}")
|
|
if attempt.telegram_endpoint:
|
|
extra.append(f"telegram={attempt.telegram_endpoint}")
|
|
suffix = f" {' '.join(extra)}" if extra else ""
|
|
evidence = attempt.evidence()
|
|
evidence_suffix = f" evidence={evidence}" if evidence and evidence != "none" else ""
|
|
print(f"- {attempt.key}{endpoint} profile={profile_text(attempt)} verdict={attempt.verdict()}{evidence_suffix}{suffix}")
|
|
print(f" lines={attempt.first_line}-{attempt.last_line} events={flags}")
|
|
if attempt.disconnect:
|
|
print(f" disconnect={attempt.disconnect}")
|
|
|
|
print()
|
|
print("How to read the verdicts:")
|
|
print("- connection_not_started: this socket closed before a real TCP connect attempt started.")
|
|
print("- admission_timeout: this socket waited in the MTProxy admission scheduler until timeout; TCP did not start.")
|
|
print("- pre_tcp_gate_admission_overlap: historical source bug where TCP gate was held while admission was still queued.")
|
|
print("- endpoint_cooldown_timeout: this socket waited in local endpoint cooldown until timeout; TCP did not start.")
|
|
print("- dns_coalesce_timeout: this socket waited for local DNS coalescing until timeout; DNS did not start.")
|
|
print("- tcp_connect_gate_timeout: this socket waited behind another active TCP connect until timeout; TCP did not start.")
|
|
print("- tcp_not_connected: TCP connect was attempted, but the socket never reached socket_connected.")
|
|
print("- host_resolve_failed: proxy hostname did not resolve; compare DNS/VPN and sslip.io fast-path before blaming JA4.")
|
|
print("- host_resolve_timeout: proxy DNS lookup started, but no DNS callback arrived before close.")
|
|
print("- dns_budget_stolen_by_pre_tcp_wait: DNS resolve was closed almost immediately after host_resolve_start following a long pre-TCP wait.")
|
|
print("- endpoint_cooldown: client delayed the next connect for this endpoint after a recent phase-specific failure.")
|
|
print("- tcp_connect_gate: client delayed a duplicate active TCP connect attempt for the same MTProxy endpoint.")
|
|
print("- dns_coalesce_wait: client delayed a duplicate cold DNS resolve for the same proxy host:port.")
|
|
print("- dns_cache_hit/dns_cache_store: client used or updated the last-good IP for a domain proxy.")
|
|
print("- phase_adaptive_recipe: client changed the next FakeTLS startup recipe after a phase-specific failure.")
|
|
print("- recipe_failed: current FakeTLS recipe failed and the next attempt should move along the recipe ladder, not mark the endpoint bad yet.")
|
|
print("- shadowed_by_usable_success: a late sibling startup failure was ignored because this endpoint recently delivered app-data.")
|
|
print("- shadowed_socket_failure: native suppressed a sibling socket failure because the endpoint recently delivered app-data.")
|
|
print("- silent_after_client_hello: zero bytes after ClientHello; treated as a transient transport failure (endpoint cooldown paces the retry with the same recipe), not recipe evidence.")
|
|
print("- ignored_cancelled_generation: late native callbacks were ignored after terminal endpoint cancellation advanced the generation.")
|
|
print("- reconnect_backoff_suppressed: native closed a socket without adding endpoint reconnect backoff.")
|
|
print("- telemetry_only: Java kept per-connection DNS telemetry out of the visible proxy status.")
|
|
print("- visible_delayed_dns: Java showed DNS only after the DNS telemetry stayed unresolved past the debounce window.")
|
|
print("- held_by_usable_success: Java control-plane kept the current proxy after fresh app-data success.")
|
|
print("- held_live_by_usable_success: Java control-plane kept proven usable status instead of showing newer sibling live telemetry, including local reconnect start.")
|
|
print("- held_live_by_current_proxy_usable: Java control-plane kept connected current-proxy status instead of showing newer sibling socket telemetry or local reconnect start.")
|
|
print("- held_by_fresh_failure: Java kept the concrete recent failure visible instead of replacing it with an early retry live phase.")
|
|
print("- post_success_shadow_budget: one bounded sibling data-path failure after app-data success was forgiven; a repeated data-path failure should break through to backoff/rotation.")
|
|
print("- connected_without_socket_connected_marker: Telegram reached on_connected, but this log slice has no socket_connected marker; do not treat it as a TCP failure.")
|
|
print("- client_hello_sent_no_server_hello: compare VPN vs non-VPN; with VPN failure points to server/client compatibility, without VPN it can be DPI blackhole.")
|
|
print("- tls_alert_after_client_hello: TCP and ClientHello completed; probable TLS alert / non-ServerHello record after ClientHello. Inspect mtproxy_tls_after_client_hello hex/record_len/alert fields before blaming the proxy.")
|
|
print("- short_tls_response_after_client_hello: bytes arrived after ClientHello, but not enough for a parseable ServerHello flight.")
|
|
print("- unrecognized_tls_response_after_client_hello: bytes arrived after ClientHello, but the FakeTLS parser did not recognize the server response.")
|
|
print("- faketls_no_server_hello_terminal: TCP opened and ClientHello was sent repeatedly, but no FakeTLS ServerHello arrived before the endpoint budget closed.")
|
|
print("- faketls_server_closed_terminal: TCP opened and ClientHello was sent repeatedly, but the peer closed after ClientHello with no response bytes.")
|
|
print("- faketls_not_mtproxy_response: TCP opened, but repeated server bytes did not validate as a MTProxy/FakeTLS ServerHello; suspect wrong secret/SNI, ordinary HTTPS, WAF, or non-MTProxy endpoint.")
|
|
print("- handshake_profiles_exhausted: every allowed FakeTLS handshake recipe failed; treat as recovery/backoff, not proof that the proxy is unsupported.")
|
|
print("- unsupported_for_current_client: legacy alias from older captures; read it as handshake_profiles_exhausted.")
|
|
print("- server_hello_hmac_mismatch: likely ClientHello/profile/server response mismatch, not plain packet loss.")
|
|
print("- mtproxy_packet_sent_no_response: plain dd TCP opened and the first MTProxy packet was sent, but no server reply arrived.")
|
|
print("- handshake_ok_no_appdata_sent: HMAC passed and on_connected fired, but this socket closed before app-data was sent; usually idle/restart noise, not a proxy failure.")
|
|
print("- post_handshake_no_appdata: HMAC passed; inspect TLS app-data write/read path and first MTProto packets.")
|
|
print("- dropped_early_after_appdata: first data arrived, then the session died quickly; inspect post-handshake lifecycle/backoff, not JA4.")
|
|
print("- dropped_after_appdata: startup worked; look at later MTProto keepalive, server close, or external throttling.")
|
|
print("- proxy_check fail:tcp_not_connected: TCP/connect/DNS/server availability layer; compare with VPN and external probe.")
|
|
print("- proxy_check fail:tcp_connected_no_pong: TCP opened, but MTProxy ping did not complete; can be dead proxy, server overload, or path filtering.")
|
|
print()
|
|
print("Runtime proof command:")
|
|
print(" /mnt/d/bin/platform-tools/adb.exe logcat -c")
|
|
print(" # reproduce MTProxy startup or failure on device")
|
|
print(" RUN_ID=$(date +%Y%m%d-%H%M%S)")
|
|
print(" mkdir -p mtproxy-logs-live/$RUN_ID")
|
|
print(" /mnt/d/bin/platform-tools/adb.exe logcat -d > mtproxy-logs-live/$RUN_ID/logcat.txt")
|
|
print(" python3 Tools/analyze_mtproxy_markers.py mtproxy-logs-live/$RUN_ID/logcat.txt > mtproxy-logs-live/$RUN_ID/mtproxy_analysis.txt")
|
|
print(" python3 Tools/verify_mtproxy_runtime_logs.py mtproxy-logs-live/$RUN_ID/logcat.txt > mtproxy-logs-live/$RUN_ID/mtproxy_runtime_contract.txt")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("markers", type=Path, help="Path to mtproxy_markers.txt or raw logcat.txt")
|
|
parser.add_argument(
|
|
"--out-dir",
|
|
type=Path,
|
|
help="Optional directory for CSV reports: mtproxy_attempts.csv, mtproxy_endpoint_profile_stats.csv, mtproxy_plain_account_stats.csv, mtproxy_proxy_check_stats.csv, mtproxy_scheduler_stats.csv",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
if not args.markers.exists():
|
|
raise SystemExit(f"markers file not found: {args.markers}")
|
|
|
|
attempts, global_lines = load_attempts(args.markers)
|
|
print_report(attempts, global_lines)
|
|
if args.out_dir:
|
|
write_csv_reports(attempts, global_lines, args.out_dir)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|