Every session dialled a proxy on its own with nothing in between, so a cold start or a proxy switch opened one socket per session per account in the same millisecond. A public mtproxy answers about two parallel handshakes and silently drops the rest, which the client read back as client_hello_sent_no_server_hello or connected_no_mtproto_data across the whole batch - including on a dd-secret proxy that sends no ClientHello at all and therefore cannot be fingerprint-blocked. Replace the machinery that was supposed to prevent this with a rate limiter that actually does. ProxyDialLease keeps at most two unproven handshakes in flight per proxy server, spaces the rest apart and stretches the spacing after a run of attempts that proved nothing, so a blackholed proxy is no longer redialled by every session on its own eight second timer. Everything that reacted to failure by changing its own behaviour is gone: EndpointAdmissionArbiter, EndpointLivePool, ConnectionBroker, session_proxy_adapter, SessionProxyPort, HandshakeGate, open_scheduler, endpoint_health, adaptive_policy and ProxyRotationManager. A fingerprint filter is deterministic, so rotating emulated ClientHello profiles only hands the other side more of them, and escalated recipes (fragmentation, pacing) make the flow less browser-like rather than more - while the signal that drove the escalation could not tell a DPI box from a proxy refusing extra connections. The ClientHello templates themselves are untouched and the profile is now whatever the user configured, fixed. Also: ServerHello budget 2.5s -> 5s, mtproxy status reduces through the same ProxyConnectionStatus path as every other proxy type, and route memory is wired back up so an address that answered is dialled first and one that failed is dialled last. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
389 lines
15 KiB
Python
389 lines
15 KiB
Python
from pathlib import Path
|
|
from session_private_sources import read_session_private_sources
|
|
|
|
|
|
SOURCE_DIR = Path(__file__).resolve().parents[1]
|
|
ROOT = SOURCE_DIR.parents[1]
|
|
CMAKE = ROOT / "Telegram" / "CMakeLists.txt"
|
|
MTPROTO_DIR = SOURCE_DIR / "mtproto"
|
|
PROXY_DIR = MTPROTO_DIR / "proxy"
|
|
PROXY_SERVICES_H = PROXY_DIR / "proxy_services.h"
|
|
PROXY_SERVICES_CPP = PROXY_DIR / "proxy_services.cpp"
|
|
RUNTIME_H = MTPROTO_DIR / "runtime" / "runtime_environment.h"
|
|
RUNTIME_CPP = MTPROTO_DIR / "runtime" / "runtime_environment.cpp"
|
|
CONNECTION_STATUS_H = MTPROTO_DIR / "runtime" / "connection_status.h"
|
|
CONNECTION_STATUS_CPP = MTPROTO_DIR / "runtime" / "connection_status.cpp"
|
|
DIAGNOSTICS_H = PROXY_DIR / "diagnostics.h"
|
|
DIAGNOSTICS_CPP = PROXY_DIR / "diagnostics.cpp"
|
|
CONTROL_H = PROXY_DIR / "control_plane.h"
|
|
CONTROL_CPP = PROXY_DIR / "control_plane.cpp"
|
|
DNS_H = PROXY_DIR / "dns_resolver_cache.h"
|
|
CHECK_H = PROXY_DIR / "check.h"
|
|
ABSTRACT_CONNECTION_H = MTPROTO_DIR / "transport" / "connection_abstract.h"
|
|
ABSTRACT_CONNECTION_CPP = MTPROTO_DIR / "transport" / "connection_abstract.cpp"
|
|
INSTANCE_CPP = MTPROTO_DIR / "instance" / "mtp_instance.cpp"
|
|
INSTANCE_H = MTPROTO_DIR / "instance" / "mtp_instance.h"
|
|
RPC_ERROR_H = MTPROTO_DIR / "instance" / "rpc_error_handler.h"
|
|
RPC_ERROR_CPP = MTPROTO_DIR / "instance" / "rpc_error_handler.cpp"
|
|
SENDER_CPP = MTPROTO_DIR / "instance" / "sender.cpp"
|
|
SENDER_H = MTPROTO_DIR / "instance" / "sender.h"
|
|
SESSION_CPP = MTPROTO_DIR / "session" / "session.cpp"
|
|
SESSION_H = MTPROTO_DIR / "session" / "session.h"
|
|
SESSION_DELEGATE_H = MTPROTO_DIR / "session" / "session_delegate.h"
|
|
SESSION_PRIVATE_CPP = MTPROTO_DIR / "session" / "private" / "session_private.cpp"
|
|
SESSION_PRIVATE_H = MTPROTO_DIR / "session" / "private" / "session_private.h"
|
|
|
|
|
|
def read(path):
|
|
assert path.exists(), f"missing expected source file: {path}"
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
def function_body(source, signature):
|
|
start = source.index(signature)
|
|
brace = source.index("{", start)
|
|
depth = 0
|
|
for index in range(brace, len(source)):
|
|
char = source[index]
|
|
if char == "{":
|
|
depth += 1
|
|
elif char == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return source[brace + 1:index]
|
|
raise AssertionError(f"function body not found: {signature}")
|
|
|
|
|
|
def mtproto_sources():
|
|
for path in (
|
|
list(PROXY_DIR.rglob("*.h"))
|
|
+ list(PROXY_DIR.rglob("*.cpp"))
|
|
+ [
|
|
MTPROTO_DIR / "transport" / "connection_abstract.h",
|
|
MTPROTO_DIR / "transport" / "connection_abstract.cpp",
|
|
MTPROTO_DIR / "transport" / "connection_tcp.h",
|
|
MTPROTO_DIR / "transport" / "connection_tcp.cpp",
|
|
MTPROTO_DIR / "transport" / "connection_http.h",
|
|
MTPROTO_DIR / "transport" / "connection_http.cpp",
|
|
MTPROTO_DIR / "transport" / "mtproto_abstract_socket.h",
|
|
MTPROTO_DIR / "transport" / "mtproto_abstract_socket.cpp",
|
|
]):
|
|
if path.exists():
|
|
yield path
|
|
|
|
|
|
def test_runtime_environment_is_the_app_gateway():
|
|
cmake = read(CMAKE)
|
|
header = read(RUNTIME_H)
|
|
source = read(RUNTIME_CPP)
|
|
services_header = read(PROXY_SERVICES_H)
|
|
status_header = read(CONNECTION_STATUS_H)
|
|
status_source = read(CONNECTION_STATUS_CPP)
|
|
instance = read(INSTANCE_CPP)
|
|
|
|
assert "mtproto/runtime/runtime_environment.cpp" in cmake
|
|
assert "mtproto/runtime/runtime_environment.h" in cmake
|
|
assert "mtproto/runtime/connection_status.cpp" in cmake
|
|
assert "mtproto/runtime/connection_status.h" in cmake
|
|
assert "mtproto/proxy/proxy_services.cpp" in cmake
|
|
assert "mtproto/proxy/proxy_services.h" in cmake
|
|
assert "struct RuntimeProxySettings" in header
|
|
assert "struct RuntimeDeviceSettings" in header
|
|
assert "struct RuntimeLanguageGateway" in header
|
|
assert "struct RuntimeStorageGateway" in header
|
|
assert "struct RuntimeAppGateway" in header
|
|
assert "struct RuntimeDiagnosticsGateway" in header
|
|
assert "struct RuntimeInstanceServices" in header
|
|
assert "struct RuntimeProxyResolver" in header
|
|
assert "class RuntimeEnvironment final" in header
|
|
assert "class ProxyServices;" in header
|
|
assert "[[nodiscard]] ProxyServices &proxyServices() const;" in header
|
|
assert "class ProxyServices final" in services_header
|
|
assert "ProxyControlPlane &control();" in services_header
|
|
assert "details::DnsResolverCache &dnsResolver();" in services_header
|
|
assert "void bindInstance(RuntimeInstanceServices services);" in header
|
|
assert "void unbindInstance(ConnectionStatus *status);" in header
|
|
assert "RuntimeEnvironmentDescriptor" in header
|
|
assert "DefaultRuntimeEnvironment()" in header
|
|
assert "DefaultRuntimeEnvironment()" in source
|
|
assert "class ConnectionStatus final" in status_header
|
|
assert "ConnectionStatus::setProxyStatus(" in status_source
|
|
assert '#include "mtproto/auth/mtproto_auth_key.h"' in instance
|
|
assert "fields.runtimeEnvironment" in instance
|
|
|
|
|
|
def test_runtime_environment_has_no_public_mutable_service_locator_fields():
|
|
header = read(RUNTIME_H)
|
|
source = read(RUNTIME_CPP)
|
|
instance = read(INSTANCE_CPP)
|
|
|
|
runtime_body = function_body(header, "class RuntimeEnvironment final")
|
|
|
|
assert "public:\n\tRuntimeProxySettings proxy;" not in header
|
|
assert "ConnectionStatus *connectionStatus = nullptr" not in runtime_body
|
|
assert "Fn<" not in runtime_body
|
|
assert "runtime->connectionStatus =" not in instance
|
|
assert "runtime->mainDcId =" not in instance
|
|
assert "runtime->dcOptionsLookup =" not in instance
|
|
assert "runtime->resolveProxyDomain =" not in instance
|
|
assert "runtime->setGoodProxyDomain =" not in instance
|
|
assert "runtime->proxyDomainResolved =" not in instance
|
|
assert "runtime->syncHttpUnixtime =" not in instance
|
|
assert "InstallDefaultHandlers" not in source
|
|
|
|
|
|
def test_lower_mtproto_layers_do_not_include_app_facade():
|
|
banned_tokens = (
|
|
'#include "mtproto/instance/mtp_instance.h"',
|
|
'#include "core/',
|
|
'#include "main/',
|
|
'#include "settings.h"',
|
|
"Core::App(",
|
|
"Core::App().",
|
|
"Local::",
|
|
"Lang::",
|
|
"Logs::writeMtproxy",
|
|
)
|
|
allowed = {
|
|
RUNTIME_CPP,
|
|
RUNTIME_H,
|
|
}
|
|
|
|
for path in mtproto_sources():
|
|
if path in allowed:
|
|
continue
|
|
text = read(path)
|
|
for token in banned_tokens:
|
|
assert token not in text, (
|
|
f"{path.relative_to(ROOT)} still depends on app layer via "
|
|
f"{token}")
|
|
|
|
|
|
def test_proxy_reporting_and_control_plane_do_not_accept_instance():
|
|
diagnostics_h = read(DIAGNOSTICS_H)
|
|
diagnostics_cpp = read(DIAGNOSTICS_CPP)
|
|
control_h = read(CONTROL_H)
|
|
control_cpp = read(CONTROL_CPP)
|
|
runtime_cpp = read(RUNTIME_CPP)
|
|
|
|
assert "void ReportProxyEvent(ProxyEventReport report);" not in diagnostics_h
|
|
assert "not_null<Instance*>" not in diagnostics_h
|
|
assert "not_null<Instance*>" not in diagnostics_cpp
|
|
assert "class Instance;" not in diagnostics_h
|
|
assert "not_null<RuntimeEnvironment*> runtime" in diagnostics_h
|
|
assert "runtime->diagnostics().reportProxyEvent" in diagnostics_cpp
|
|
assert "proxyServices().control().submitFact(report)" in runtime_cpp
|
|
assert "void submitFact(const ProxyEventReport &report);" in control_h
|
|
assert "not_null<Instance*>" not in control_h
|
|
assert "not_null<Instance*>" not in control_cpp
|
|
assert "_runtime->instance().connectionStatus->setProxyStatus" in control_cpp
|
|
|
|
|
|
def test_instance_and_session_use_runtime_gateway_for_app_facade():
|
|
runtime = read(RUNTIME_CPP)
|
|
instance_h = read(INSTANCE_H)
|
|
checked_sources = (
|
|
INSTANCE_CPP,
|
|
SESSION_CPP,
|
|
SESSION_PRIVATE_CPP,
|
|
)
|
|
banned_tokens = (
|
|
'#include "core/application.h"',
|
|
'#include "core/core_settings.h"',
|
|
'#include "main/',
|
|
'#include "storage/localstorage.h"',
|
|
'#include "lang/',
|
|
"Core::App(",
|
|
"Core::App().",
|
|
"Local::",
|
|
"Lang::",
|
|
)
|
|
|
|
assert "Core::App().settings().proxy()" in runtime
|
|
assert "Lang::CurrentCloudManager()" in runtime
|
|
assert "Local::writeSettings()" in runtime
|
|
assert "ConnectionStatus &connectionStatus() const;" in instance_h
|
|
for removed in (
|
|
"proxyConnectionStatus",
|
|
"connectionNoticeValue",
|
|
"setConnectionNotice",
|
|
"pingTimeValue",
|
|
"setSessionPingTime"):
|
|
assert removed not in instance_h
|
|
|
|
for path in checked_sources:
|
|
text = read(path)
|
|
for token in banned_tokens:
|
|
assert token not in text, (
|
|
f"{path.relative_to(ROOT)} still depends on app layer via "
|
|
f"{token}")
|
|
|
|
|
|
def test_on_error_default_is_split_into_helpers():
|
|
cmake = read(CMAKE)
|
|
instance = read(INSTANCE_CPP)
|
|
rpc_h = read(RPC_ERROR_H)
|
|
rpc_cpp = read(RPC_ERROR_CPP)
|
|
body = function_body(
|
|
instance,
|
|
"bool Instance::Private::onErrorDefault(")
|
|
|
|
assert "mtproto/instance/rpc_error_handler.cpp" in cmake
|
|
assert "mtproto/instance/rpc_error_handler.h" in cmake
|
|
assert "struct DefaultRpcErrorAction" in rpc_h
|
|
assert "DefaultRpcErrorAction ClassifyDefaultRpcError(" in rpc_h
|
|
assert "QRegularExpression" not in instance
|
|
assert "QRegularExpression" in rpc_cpp
|
|
assert "ClassifyDefaultRpcError(" in body
|
|
assert len(body.splitlines()) <= 45
|
|
for helper in (
|
|
"handleMigrationError(",
|
|
"handleMsgWaitError(",
|
|
"handleRetryError(",
|
|
"handleUnauthorizedError(",
|
|
"handleConnectionInitError("):
|
|
assert helper in body
|
|
|
|
|
|
def test_session_callbacks_are_hidden_behind_delegate():
|
|
cmake = read(CMAKE)
|
|
instance = read(INSTANCE_CPP)
|
|
instance_h = read(INSTANCE_H)
|
|
session_h = read(SESSION_H)
|
|
session = read(SESSION_CPP)
|
|
session_private_h = read(SESSION_PRIVATE_H)
|
|
session_private = read_session_private_sources()
|
|
delegate = read(SESSION_DELEGATE_H)
|
|
public_instance = function_body(instance_h, "class Instance : public QObject")
|
|
|
|
assert "mtproto/session/session_delegate.h" in cmake
|
|
assert "class SessionDelegate" in delegate
|
|
assert "public details::SessionDelegate" in instance
|
|
assert "not_null<SessionDelegate*> delegate" in session_h
|
|
assert "const not_null<SessionDelegate*> _delegate;" in session_h
|
|
assert "const not_null<SessionDelegate*> _delegate;" in session_private_h
|
|
assert (
|
|
"std::make_unique<Session>(\n\t\t\t_instance,\n\t\t\tthis,"
|
|
in instance)
|
|
assert "new SessionPrivate(\n\t\t_instance,\n\t\t_delegate," in session
|
|
|
|
for hidden in (
|
|
"resolveProxyDomain(",
|
|
"setGoodProxyDomain(",
|
|
"systemLangCode(",
|
|
"cloudLangCode(",
|
|
"langPackName(",
|
|
"dcPersistentKeyChanged(",
|
|
"dcTemporaryKeyChanged(",
|
|
"proxyMigrationSucceeded(",
|
|
"onStateChange(",
|
|
"onSessionReset(",
|
|
"hasCallback(",
|
|
"processCallback(",
|
|
"processUpdate(",
|
|
"rpcErrorOccured(",
|
|
"keyWasPossiblyDestroyed(",
|
|
"keyDestroyedOnServer(",
|
|
"badConfigurationError(",
|
|
"restartedByTimeout("):
|
|
assert hidden not in public_instance
|
|
|
|
assert "_instance->" not in session
|
|
assert "_instance->" not in session_private
|
|
assert "delegate->processCallback(" in session
|
|
assert "_delegate->hasCallback(" in session_private
|
|
|
|
|
|
def test_transport_session_leaks_use_neutral_metadata():
|
|
abstract_h = read(ABSTRACT_CONNECTION_H)
|
|
abstract_cpp = read(ABSTRACT_CONNECTION_CPP)
|
|
session = read_session_private_sources()
|
|
|
|
assert "enum class TransportServiceRequest" in abstract_h
|
|
assert "HttpWait" in abstract_h
|
|
assert "serviceRequestNeeded(" in abstract_h
|
|
assert "usingHttpWait" not in abstract_h
|
|
assert "needHttpWait" not in abstract_h
|
|
assert "setSentEncryptedWithKeyId" not in abstract_h
|
|
assert "sentEncryptedWithKeyId" not in abstract_h
|
|
assert "struct SendDataContext" in abstract_h
|
|
assert "uint64 keyId = 0;" in abstract_h
|
|
assert "sendData(mtpBuffer &&buffer, SendDataContext context)" in abstract_h
|
|
assert "struct ConnectionStartContext" in abstract_h
|
|
assert "ProxyConnectionAttempt mtproxyAttempt;" in abstract_h
|
|
assert "ConnectionStartContext context = {}" in abstract_h
|
|
assert "setMtproxyAttempt" not in abstract_h
|
|
assert "setMtproxyAttempt" not in abstract_cpp
|
|
assert "setMtproxyAttempt(" not in session
|
|
assert "TransportServiceRequest::HttpWait" in session
|
|
assert ".keyId = _sessionState.keyId" in session
|
|
assert "usingHttpWait" not in session
|
|
assert "needHttpWait" not in session
|
|
assert "sentEncryptedWithKeyId" not in abstract_cpp
|
|
|
|
|
|
def test_sender_header_does_not_pull_instance_facade():
|
|
cmake = read(CMAKE)
|
|
instance_h = read(INSTANCE_H)
|
|
sender_h = read(SENDER_H)
|
|
sender_cpp = read(SENDER_CPP)
|
|
|
|
assert "mtproto/instance/sender.cpp" in cmake
|
|
assert '#include "mtproto/instance/mtp_instance.h"' not in sender_h
|
|
assert '#include "mtproto/proxy/data.h"' not in instance_h
|
|
assert "class Instance;" in sender_h
|
|
assert "Sender::sendSerializedRequest(" in sender_cpp
|
|
assert '#include "mtproto/instance/mtp_instance.h"' in sender_cpp
|
|
|
|
|
|
def test_sources_that_call_instance_main_dc_include_instance_header():
|
|
instance_calls = (
|
|
".instance().killSession(",
|
|
".instance().mainDcId(",
|
|
".instance().mainDcIdValue(",
|
|
".instance().nonPremiumDelayedRequests(",
|
|
".instance().requestConfig(",
|
|
".instance().setMainDcId(",
|
|
".instance().setUserPhone(",
|
|
".instance().state(",
|
|
".instance().stopSession(",
|
|
".mtp().config(",
|
|
".mtp().configValues(",
|
|
".mtp().dcstate(",
|
|
".mtp().environment(",
|
|
".mtp().isTestMode(",
|
|
".mtp().mainDcId(",
|
|
".mtp().mainDcIdValue(",
|
|
".mtp().requestConfig(",
|
|
".mtp().runtimeEnvironment(",
|
|
"->mtp().config(",
|
|
"->mtp().configValues(",
|
|
"->mtp().dcstate(",
|
|
"->mtp().environment(",
|
|
"->mtp().isTestMode(",
|
|
"->mtp().mainDcId(",
|
|
"->mtp().mainDcIdValue(",
|
|
"->mtp().runtimeEnvironment(",
|
|
)
|
|
offenders = []
|
|
for path in SOURCE_DIR.rglob("*.cpp"):
|
|
source = read(path)
|
|
if not any(call in source for call in instance_calls):
|
|
continue
|
|
if '#include "mtproto/instance/mtp_instance.h"' not in source:
|
|
offenders.append(path.relative_to(SOURCE_DIR).as_posix())
|
|
|
|
assert offenders == []
|
|
|
|
|
|
if __name__ == "__main__":
|
|
test_runtime_environment_is_the_app_gateway()
|
|
test_runtime_environment_has_no_public_mutable_service_locator_fields()
|
|
test_lower_mtproto_layers_do_not_include_app_facade()
|
|
test_proxy_reporting_and_control_plane_do_not_accept_instance()
|
|
test_instance_and_session_use_runtime_gateway_for_app_facade()
|
|
test_on_error_default_is_split_into_helpers()
|
|
test_session_callbacks_are_hidden_behind_delegate()
|
|
test_sources_that_call_instance_main_dc_include_instance_header()
|
|
test_transport_session_leaks_use_neutral_metadata()
|
|
test_sender_header_does_not_pull_instance_facade()
|