ZaStoGram_desktop/Telegram/SourceFiles/mtproto/proxy/resolving_connection.cpp
loop-uh de04277eb8
All checks were successful
Desktop source guards / guards (push) Successful in 8s
Гнать фронт наперегонки с релеем, пока релей не ответил
По логу ПК 02.10 первый запуск тратил 1+2+4 с бюджетов на прямые релеи,
до которых не доходил даже TCP, и только после их подавления пробовал
фронты Cloudflare. В VM релей, отвечавший отказом сразу, вообще не
подавлялся: две минуты попыток с растущей паузой, фронты не открывались.

- Пока релей датацентра не ответил в этой сети или сбоит, сессия через
  500 мс открывает рядом с ним тестовое соединение через фронт. Работающий
  релей успевает раньше, и фронт не открывается вовсе.
- Релей сохраняет более высокий приоритет: подключившемуся фронту даётся
  обычное ожидание лучшего соединения, бюджет на подключение не меньше
  4,5 с, чтобы фронт успел пройти TLS и апгрейд.
- Релей, проигравший гонку без TCP, считается недоступным, как при
  истёкшем бюджете. Через несколько раундов он подавляется, и фронты
  набираются сразу, без гонки и ожидания.
- Отказ релея на уровне TCP (сброс, reject, нет такого хоста) учитывается
  так же, как таймаут.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:27:00 +03:00

1135 lines
32 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "mtproto/proxy/resolving_connection.h"
#include "mtproto/proxy/capabilities.h"
#include "mtproto/proxy/diagnostics.h"
#include "mtproto/proxy/dns_resolver_cache.h"
#include "mtproto/proxy/mtproxy/endpoint_identity.h"
#include "mtproto/proxy/proxy_endpoint_context.h"
#include "mtproto/proxy/proxy_services.h"
#include "mtproto/runtime/runtime_environment.h"
#include "mtproto/transport/details/mtproto_abstract_socket.h"
#include <algorithm>
#include <tuple>
namespace MTP {
namespace details {
namespace {
constexpr auto kRouteAttemptTimeout = crl::time(4000);
constexpr auto kRouteRaceDelay = crl::time(300);
constexpr auto kMaxParallelRouteAttempts = 2;
constexpr auto kColdServerHelloTimeout = crl::time(5000);
constexpr auto kFullConnectTimeoutSafetyMargin = crl::time(500);
constexpr auto kFullConnectTimeout = crl::time(12600);
// When the running attempt is the last route available there is nothing
// to race it against - killing it at the short timeout only burns a
// handshake and reconnects. Give TCP time to retransmit SYN instead: a
// proxy that throttles new connects often accepts on a later try. This
// also covers the post-handshake wait for the proxy to relay telegram
// data (its own dial to the DC), which racing cannot speed up.
constexpr auto kOnlyRouteAttemptTimeout = crl::time(8000);
// The route attempt timer knows nothing about handshakes by itself, so
// when it fires the failure must be attributed from the phase the child
// socket actually reached - reporting a generic tcp_connect_timeout for
// a connection that completed TCP (or even the whole TLS handshake)
// corrupts diagnostics.
[[nodiscard]] MtProxy::FailureReason RouteTimeoutReason(
HandshakePhase phase) {
switch (phase) {
case HandshakePhase::None:
return MtProxy::FailureReason::TcpConnectTimeout;
case HandshakePhase::TcpConnected:
return MtProxy::FailureReason::TcpConnectedNoClientHelloWrite;
case HandshakePhase::ClientHelloSent:
return MtProxy::FailureReason::ClientHelloSentNoServerHello;
case HandshakePhase::ServerHelloOk:
return MtProxy::FailureReason::ServerHelloOkNoAppData;
case HandshakePhase::FirstDataReceived:
return MtProxy::FailureReason::ServerHelloOkNoMtprotoData;
}
return MtProxy::FailureReason::TcpConnectTimeout;
}
[[nodiscard]] HandshakePhase ChildHandshakePhase(
AbstractConnection *child) {
return child ? child->handshakePhase() : HandshakePhase::None;
}
[[nodiscard]] MtProxy::FailureReason ChildFailureReason(
AbstractConnection *child,
int errorCode) {
if (child) {
const auto failure = child->proxyTransportFailure();
if (failure.reason != ProxyMtproxyTerminalReason::None) {
return MtProxy::FromProxyMtproxyTerminalReason(failure.reason);
}
}
const auto reason = MtProxy::FailureReasonFromErrorCode(errorCode);
return (reason == MtProxy::FailureReason::None)
? MtProxy::FailureReason::TcpConnectTimeout
: reason;
}
[[nodiscard]] MtProxy::EndpointId MtproxyEndpointIdForRoute(
const ProxyData &proxy,
int ipIndex) {
auto stealth = ProxyStealthOptions();
stealth.transport = ProxyTransport::Tcp;
const auto address = (ipIndex >= 0 && ipIndex < proxy.resolvedIPs.size())
? proxy.resolvedIPs[ipIndex]
: QString();
return MtProxy::EndpointIdFromProxy(
proxy,
stealth,
address,
int(proxy.port));
}
[[nodiscard]] ProxyFailureAttribution DefaultFailureAttribution(
const ProxyTransportFailure &failure) {
if (failure.closeOrigin == ProxyCloseOrigin::PeerClosed) {
return ProxyFailureAttribution::Peer;
} else if (failure.error == ProxyConnectionError::Network
|| failure.error == ProxyConnectionError::ConnectionRefused
|| failure.error == ProxyConnectionError::HostNotFound) {
return ProxyFailureAttribution::Network;
}
const auto reason = MtProxy::FromProxyMtproxyTerminalReason(failure.reason);
if (reason == MtProxy::FailureReason::TlsAlertAfterClientHello) {
return ProxyFailureAttribution::Client;
} else if (reason == MtProxy::FailureReason::ServerHelloHmacMismatch
|| reason == MtProxy::FailureReason::ServerHelloForeignTls
|| reason == MtProxy::FailureReason::ProxyProtocolBadResponse) {
return ProxyFailureAttribution::Peer;
}
return (reason == MtProxy::FailureReason::None)
? ProxyFailureAttribution::None
: ProxyFailureAttribution::Unclear;
}
[[nodiscard]] int FailureReasonSpecificity(
ProxyMtproxyTerminalReason reason) {
switch (reason) {
case ProxyMtproxyTerminalReason::None:
return 0;
case ProxyMtproxyTerminalReason::DnsFailed:
return 10;
case ProxyMtproxyTerminalReason::TcpConnectTimeout:
return 20;
case ProxyMtproxyTerminalReason::TcpConnectedNoClientHelloWrite:
return 30;
case ProxyMtproxyTerminalReason::ClientHelloSentNoServerHello:
return 40;
case ProxyMtproxyTerminalReason::TlsAlertAfterClientHello:
return 50;
case ProxyMtproxyTerminalReason::ServerHelloHmacMismatch:
return 60;
case ProxyMtproxyTerminalReason::ServerHelloForeignTls:
return 62;
case ProxyMtproxyTerminalReason::ProxyProtocolBadResponse:
return 65;
case ProxyMtproxyTerminalReason::ServerHelloOkNoAppData:
return 70;
case ProxyMtproxyTerminalReason::ServerHelloOkNoMtprotoData:
case ProxyMtproxyTerminalReason::ConnectedNoMtprotoData:
return 80;
case ProxyMtproxyTerminalReason::AppDataRemoteClosed:
return 90;
case ProxyMtproxyTerminalReason::MtpReceiveTimeoutAfterData:
return 100;
}
return 0;
}
[[nodiscard]] int FailureAttributionSpecificity(
ProxyFailureAttribution attribution) {
switch (attribution) {
case ProxyFailureAttribution::None:
return 0;
case ProxyFailureAttribution::Unclear:
return 1;
case ProxyFailureAttribution::Local:
return 2;
case ProxyFailureAttribution::Network:
return 3;
case ProxyFailureAttribution::Client:
case ProxyFailureAttribution::Peer:
return 4;
}
return 0;
}
[[nodiscard]] int FailureErrorSpecificity(ProxyConnectionError error) {
switch (error) {
case ProxyConnectionError::None:
case ProxyConnectionError::Unknown:
return 0;
case ProxyConnectionError::Timeout:
return 1;
case ProxyConnectionError::HostNotFound:
case ProxyConnectionError::Network:
return 2;
case ProxyConnectionError::ConnectionRefused:
case ProxyConnectionError::RemoteClosed:
return 3;
case ProxyConnectionError::Authentication:
case ProxyConnectionError::ProxyProtocol:
case ProxyConnectionError::BadResponse:
return 4;
}
return 0;
}
[[nodiscard]] std::tuple<int, int, int> FailureSpecificity(
const ProxyTransportFailure &failure) {
return {
FailureReasonSpecificity(failure.reason),
FailureAttributionSpecificity(failure.attribution),
FailureErrorSpecificity(failure.error),
};
}
[[nodiscard]] ProxyTransportFailure TypedRouteFailure(
AbstractConnection *child,
MtProxy::FailureReason fallbackReason,
ProxyConnectionError fallbackError,
ProxyCloseOrigin fallbackOrigin) {
auto result = child
? child->proxyTransportFailure()
: ProxyTransportFailure();
if (result.reason == ProxyMtproxyTerminalReason::None) {
result.reason = MtProxy::ToProxyMtproxyTerminalReason(
fallbackReason);
}
if (result.error == ProxyConnectionError::None) {
result.error = fallbackError;
}
if (result.closeOrigin == ProxyCloseOrigin::None) {
result.closeOrigin = fallbackOrigin;
}
if (result.attribution == ProxyFailureAttribution::None) {
result.attribution = DefaultFailureAttribution(result);
}
return result;
}
void MergeExhaustedFailure(
ProxyTransportFailure &result,
ProxyTransportFailure failure) {
if (FailureSpecificity(failure) > FailureSpecificity(result)) {
result = std::move(failure);
}
}
enum class RouteOutcomeKind {
RaceLost,
Failed,
};
void RecordRouteOutcome(
not_null<RuntimeEnvironment*> runtime,
const ProxyData &proxy,
int ipIndex,
RouteOutcomeKind kind,
MtProxy::FailureReason reason) {
if (proxy.type != ProxyData::Type::Mtproto
|| kind == RouteOutcomeKind::RaceLost) {
return;
}
const auto endpoint = MtproxyEndpointIdForRoute(proxy, ipIndex);
// Route memory only: "this address of this proxy did not answer". It
// never feeds back into the handshake shape.
runtime->proxyServices().capabilities().noteMtproxyFailure(
MtProxy::CapabilityProxyKey(endpoint.canonical),
MtProxy::RouteKey(endpoint.route),
MtProxy::ToLegacyDiagnostic(reason));
}
void ReportRouteEvent(
not_null<RuntimeEnvironment*> runtime,
const ProxyData &proxy,
int ipIndex,
ProxyDiagnosticsPhase phase,
ProxyConnectionAttempt attempt,
MtProxy::FailureReason reason = MtProxy::FailureReason::None,
std::optional<crl::time> dnsMs = std::nullopt,
std::optional<ProxyCloseOrigin> closeOrigin = std::nullopt,
ProxyConnectionError connectionError = ProxyConnectionError::None) {
if (proxy.type != ProxyData::Type::Mtproto) {
return;
}
const auto endpoint = MtproxyEndpointIdForRoute(proxy, ipIndex);
const auto routeKey = MtProxy::RouteKey(endpoint.route);
const auto routeText = ProxyDiagnosticsEndpointText(
endpoint.route.address,
endpoint.route.port);
const auto failurePhase = MtProxy::ToLegacyDiagnostic(reason);
const auto routeRaceLost = closeOrigin
&& (*closeOrigin == ProxyCloseOrigin::RouteRaceLost)
&& (reason == MtProxy::FailureReason::None);
ReportProxyEvent(runtime, {
.phase = phase,
.error = (connectionError == ProxyConnectionError::None)
? MtProxy::ToProxyConnectionError(reason)
: connectionError,
.mtproxyReason = MtProxy::ToProxyMtproxyTerminalReason(reason),
.attempt = std::move(attempt),
.severity = routeRaceLost
? ProxyDiagnosticsSeverity::Info
: (phase == ProxyDiagnosticsPhase::RouteFailed)
? ProxyDiagnosticsSeverity::Warning
: ProxyDiagnosticsSeverity::Info,
.proxy = proxy,
.transport = ProxyDiagnosticsTransportName(
proxy,
ProxyTransport::Tcp),
.message = routeRaceLost
? u"mtproxy route race lost"_q
: (phase == ProxyDiagnosticsPhase::RouteFailed)
? u"mtproxy route failed"_q
: u"mtproxy route selected"_q,
.canonical = ProxyDiagnosticsEndpointText(
endpoint.canonical.originalHost,
endpoint.canonical.port),
.route = routeText.isEmpty() ? routeKey : routeText,
.proxyKeyHash = ProxyDiagnosticsKeyHash(
MtProxy::EndpointKey(endpoint.canonical)),
.phaseAtFailure = (phase == ProxyDiagnosticsPhase::RouteFailed
&& reason != MtProxy::FailureReason::None)
? (failurePhase.isEmpty()
? u"tcp_not_connected"_q
: failurePhase)
: QString(),
.closeOrigin = closeOrigin,
.dnsMs = dnsMs,
});
}
} // namespace
ResolvingConnection::ResolvingConnection(
not_null<RuntimeEnvironment*> runtime,
QThread *thread,
const ProxyData &proxy,
ConnectionPointer &&child)
: AbstractConnection(runtime, thread, proxy)
, _child(std::move(child))
, _timeoutTimer([=] { handleRouteAttemptTimeout(); })
, _routeRaceTimer([=] { startNextRouteAttempt(); }) {
}
ConnectionPointer ResolvingConnection::clone(const ProxyData &proxy) {
Unexpected("ResolvingConnection::clone call.");
}
void ResolvingConnection::startResolving() {
if (_resolvingStartedAt || !_child) {
return;
}
_resolvingStartedAt = crl::now();
_resolvingDeadline = _resolvingStartedAt + kRouteAttemptTimeout;
refreshAttemptTimeout();
ReportProxyEvent(_runtime, {
.phase = ProxyDiagnosticsPhase::Resolving,
.attempt = _mtproxyAttempt,
.proxy = _proxy,
.message = u"resolving proxy host"_q,
});
const auto host = _proxy.host;
_runtime->proxyServices().dnsResolver().request(
this,
host,
[=](QString host, QStringList ips, qint64 expireAt) {
domainResolved(host, ips, expireAt);
});
}
void ResolvingConnection::addRouteAttempt(int ipIndex) {
if (!_child
|| ipIndex < 0
|| ipIndex >= int(_proxy.resolvedIPs.size())) {
return;
}
auto attempt = RouteAttempt();
attempt.ipIndex = ipIndex;
attempt.routeAttemptId = ++_lastRouteAttemptId;
attempt.child = _child->clone(ToDirectIpProxy(_proxy, ipIndex));
const auto raw = attempt.child.get();
attempt.phase = ChildHandshakePhase(raw);
attempt.phaseEnteredAt = crl::now();
connect(
raw,
&AbstractConnection::receivedData,
this,
[=] { handleReceivedData(raw); });
connect(
raw,
&AbstractConnection::receivedSome,
this,
&ResolvingConnection::receivedSome);
connect(
raw,
&AbstractConnection::handshakeProgress,
this,
[=] {
refreshAttemptTimeout();
handshakeProgress();
});
connect(
raw,
&AbstractConnection::error,
this,
[=](int errorCode) { handleError(raw, errorCode); });
connect(raw,
&AbstractConnection::connected,
this,
[=] { handleConnected(raw); });
connect(raw,
&AbstractConnection::disconnected,
this,
[=] { handleDisconnected(raw); });
_routeAttempts.push_back(std::move(attempt));
auto &stored = _routeAttempts.back();
stored.wrapperDeadline = stored.phaseEnteredAt
+ routePhaseBudget(stored.phase);
if (_protocolDcId) {
auto routeConnectionAttempt = _mtproxyAttempt;
routeConnectionAttempt.routeAttemptId = stored.routeAttemptId;
stored.child->connectToServer(
_address,
_port,
_protocolSecret,
_protocolDcId,
_protocolForFiles,
{
.mtproxyAttempt = routeConnectionAttempt,
.mtproxyPlan = _mtproxyPlan,
.mtproxyAttemptStartedAt = _mtproxyAttemptStartedAt,
.wssFront = _wssFront,
});
CONNECTION_LOG_INFO("Resolving connected a new child: "
+ stored.child->debugId());
}
ReportRouteEvent(
_runtime,
_proxy,
ipIndex,
ProxyDiagnosticsPhase::RouteSelected,
[&] {
auto result = _mtproxyAttempt;
result.routeAttemptId = _lastRouteAttemptId;
return result;
}(),
MtProxy::FailureReason::None,
_dnsDuration,
std::nullopt);
refreshAttemptTimeout();
}
std::vector<int> ResolvingConnection::routeOrder() const {
auto result = std::vector<int>();
result.reserve(_proxy.resolvedIPs.size());
const auto append = [&](int index) {
if (std::find(begin(result), end(result), index) == end(result)) {
result.push_back(index);
}
};
if (_proxy.type != ProxyData::Type::Mtproto) {
for (auto index = 0; index != int(_proxy.resolvedIPs.size()); ++index) {
append(index);
}
return result;
}
const auto capability = _runtime->proxyServices().capabilities().lookup(
_proxy);
const auto keyOf = [&](int index) {
return MtProxy::RouteKey(MtProxy::RouteEndpointFromAddress(
_proxy.resolvedIPs[index],
int(_proxy.port),
ProxyTransport::Tcp,
_proxy.originalHost.isEmpty()
? _proxy.host
: _proxy.originalHost));
};
const auto listed = [](const std::vector<QString> &list,
const QString &key) {
return std::find(begin(list), end(list), key) != end(list);
};
// Addresses that answered last time first, then untried ones, then the
// ones that already failed - a blackholed IP of a multi-homed proxy
// should not keep being the first thing we dial.
for (auto index = 0; index != int(_proxy.resolvedIPs.size()); ++index) {
if (listed(capability.goodRoutes, keyOf(index))) {
append(index);
}
}
for (auto index = 0; index != int(_proxy.resolvedIPs.size()); ++index) {
const auto key = keyOf(index);
if (!listed(capability.goodRoutes, key)
&& !listed(capability.badRoutes, key)) {
append(index);
}
}
for (auto index = 0; index != int(_proxy.resolvedIPs.size()); ++index) {
append(index);
}
return result;
}
int ResolvingConnection::activeRouteAttempts() const {
return int(_routeAttempts.size());
}
ResolvingConnection::RouteAttempt *ResolvingConnection::findRouteAttempt(
AbstractConnection *child) {
const auto i = std::find_if(
begin(_routeAttempts),
end(_routeAttempts),
[&](const RouteAttempt &attempt) {
return attempt.child.get() == child;
});
return (i == end(_routeAttempts)) ? nullptr : &*i;
}
void ResolvingConnection::removeRouteAttempt(AbstractConnection *child) {
const auto i = std::find_if(
begin(_routeAttempts),
end(_routeAttempts),
[&](const RouteAttempt &attempt) {
return attempt.child.get() == child;
});
if (i != end(_routeAttempts)) {
_routeAttempts.erase(i);
}
refreshAttemptTimeout();
}
void ResolvingConnection::startRouteAttempts() {
if (_connected || _protocolDcId == 0 || _proxy.resolvedIPs.empty()) {
return;
}
if (_routeOrder.empty()) {
_routeOrder = routeOrder();
_nextRoutePosition = 0;
}
if (_routeAttempts.empty()) {
startNextRouteAttempt();
} else {
scheduleRouteRace();
}
}
void ResolvingConnection::startNextRouteAttempt() {
_routeRaceTimer.cancel();
if (_connected
|| _protocolDcId == 0
|| activeRouteAttempts() >= kMaxParallelRouteAttempts) {
return;
}
if (_routeOrder.empty()) {
_routeOrder = routeOrder();
_nextRoutePosition = 0;
}
while (_nextRoutePosition < int(_routeOrder.size())) {
const auto ipIndex = _routeOrder[_nextRoutePosition++];
const auto alreadyStarted = std::find_if(
begin(_routeAttempts),
end(_routeAttempts),
[&](const RouteAttempt &attempt) {
return attempt.ipIndex == ipIndex;
}) != end(_routeAttempts);
if (!alreadyStarted) {
addRouteAttempt(ipIndex);
break;
}
}
scheduleRouteRace();
}
void ResolvingConnection::scheduleRouteRace() {
if (_connected
|| _routeRaceTimer.isActive()
|| _nextRoutePosition >= int(_routeOrder.size())
|| activeRouteAttempts() >= kMaxParallelRouteAttempts) {
return;
}
_routeRaceTimer.callOnce(kRouteRaceDelay);
}
crl::time ResolvingConnection::routePhaseBudget(
HandshakePhase phase) const {
if (phase == HandshakePhase::ClientHelloSent) {
return 0;
} else if (phase == HandshakePhase::ServerHelloOk
|| phase == HandshakePhase::FirstDataReceived) {
return kOnlyRouteAttemptTimeout;
}
const auto onlyRoute = (_routeAttempts.size() == 1)
&& (_nextRoutePosition >= int(_routeOrder.size()));
return onlyRoute ? kOnlyRouteAttemptTimeout : kRouteAttemptTimeout;
}
void ResolvingConnection::refreshAttemptTimeout() {
if (_connected || _terminal) {
_timeoutTimer.cancel();
return;
}
const auto now = crl::now();
auto earliest = _resolvingDeadline;
for (auto &attempt : _routeAttempts) {
const auto phase = ChildHandshakePhase(attempt.child.get());
if (phase != attempt.phase) {
attempt.phase = phase;
attempt.phaseEnteredAt = now;
const auto budget = routePhaseBudget(phase);
attempt.wrapperDeadline = budget
? (attempt.phaseEnteredAt + budget)
: 0;
}
if (attempt.wrapperDeadline
&& (!earliest || attempt.wrapperDeadline < earliest)) {
earliest = attempt.wrapperDeadline;
}
}
if (!earliest) {
_timeoutTimer.cancel();
return;
}
_timeoutTimer.callOnce(std::max(crl::time(0), earliest - now));
}
void ResolvingConnection::handleRouteAttemptTimeout() {
if (_connected || _terminal) {
return;
}
const auto now = crl::now();
if (_resolvingDeadline && _resolvingDeadline <= now) {
_resolvingDeadline = 0;
_lastFailure.reason = ProxyMtproxyTerminalReason::DnsFailed;
_lastFailure.error = ProxyConnectionError::Timeout;
_lastFailure.closeOrigin = ProxyCloseOrigin::LocalTimeout;
_lastFailure.parserStage = u"dns"_q;
_lastFailure.dnsMs = now - _resolvingStartedAt;
_lastFailure.attribution = ProxyFailureAttribution::Network;
emitError(kErrorCodeOther);
return;
}
refreshAttemptTimeout();
const auto victim = std::min_element(
begin(_routeAttempts),
end(_routeAttempts),
[&](const RouteAttempt &a, const RouteAttempt &b) {
const auto aDue = a.wrapperDeadline
&& a.wrapperDeadline <= now;
const auto bDue = b.wrapperDeadline
&& b.wrapperDeadline <= now;
return (aDue != bDue)
? aDue
: (a.wrapperDeadline < b.wrapperDeadline);
});
if (victim == end(_routeAttempts)
|| !victim->wrapperDeadline
|| victim->wrapperDeadline > now
|| victim->phase == HandshakePhase::ClientHelloSent) {
refreshAttemptTimeout();
return;
}
const auto ipIndex = victim->ipIndex;
auto routeAttempt = _mtproxyAttempt;
routeAttempt.routeAttemptId = victim->routeAttemptId;
const auto fallbackReason = RouteTimeoutReason(
victim->phase);
const auto child = victim->child.get();
if (child) {
child->timedOut();
}
const auto failure = TypedRouteFailure(
child,
fallbackReason,
ProxyConnectionError::Timeout,
ProxyCloseOrigin::LocalTimeout);
const auto reason = MtProxy::FromProxyMtproxyTerminalReason(
failure.reason);
RecordRouteOutcome(
_runtime,
_proxy,
ipIndex,
RouteOutcomeKind::Failed,
reason);
ReportRouteEvent(
_runtime,
_proxy,
ipIndex,
ProxyDiagnosticsPhase::RouteFailed,
routeAttempt,
reason,
std::nullopt,
(failure.closeOrigin == ProxyCloseOrigin::None)
? std::optional<ProxyCloseOrigin>()
: std::make_optional(failure.closeOrigin),
failure.error);
MergeExhaustedFailure(_lastFailure, failure);
_routeAttempts.erase(victim);
if (_routeAttempts.empty() && _nextRoutePosition >= int(_routeOrder.size())) {
emitError(kErrorCodeOther);
return;
}
startNextRouteAttempt();
refreshAttemptTimeout();
}
void ResolvingConnection::domainResolved(
const QString &host,
const QStringList &ips,
qint64 expireAt) {
if (_terminal) {
return;
}
if (_resolvingStartedAt) {
_dnsDuration = crl::now() - _resolvingStartedAt;
}
if (host != _proxy.host || !_child) {
return;
}
_resolvingDeadline = 0;
_proxy.resolvedExpireAt = expireAt;
if (ips.empty()) {
if (_proxy.type == ProxyData::Type::Mtproto) {
_lastFailure.reason = ProxyMtproxyTerminalReason::DnsFailed;
_lastFailure.error = ProxyConnectionError::HostNotFound;
_lastFailure.closeOrigin = ProxyCloseOrigin::NetworkError;
_lastFailure.parserStage = u"dns"_q;
_lastFailure.dnsMs = _dnsDuration;
_lastFailure.attribution = ProxyFailureAttribution::Network;
}
ReportProxyEvent(_runtime, {
.phase = ProxyDiagnosticsPhase::Failed,
.error = ProxyConnectionError::HostNotFound,
.mtproxyReason = (_proxy.type == ProxyData::Type::Mtproto)
? ProxyMtproxyTerminalReason::DnsFailed
: ProxyMtproxyTerminalReason::None,
.attempt = _mtproxyAttempt,
.terminalUntil = expireAt,
.proxy = _proxy,
.message = u"proxy host not found"_q,
});
emitError(kErrorCodeOther);
return;
}
ReportProxyEvent(_runtime, {
.phase = ProxyDiagnosticsPhase::Resolving,
.attempt = _mtproxyAttempt,
.proxy = _proxy,
.message = u"proxy host resolved (%1 addresses)"_q.arg(
ips.size()),
});
_proxy.resolvedIPs.clear();
_proxy.resolvedIPs.reserve(ips.size());
for (const auto &ip : ips) {
_proxy.resolvedIPs.push_back(ip);
}
_routeOrder = routeOrder();
_nextRoutePosition = 0;
startRouteAttempts();
}
void ResolvingConnection::emitError(int errorCode) {
if (_terminal) {
return;
}
_terminal = true;
_ipIndex = -1;
_resolvingDeadline = 0;
_routeRaceTimer.cancel();
_timeoutTimer.cancel();
_routeAttempts.clear();
_child = nullptr;
error(errorCode);
}
void ResolvingConnection::handleError(
AbstractConnection *child,
int errorCode) {
if (_terminal) {
return;
}
if (_connected && _child.get() == child) {
_lastFailure = child->proxyTransportFailure();
emitError(errorCode);
return;
} else if (_connected) {
return;
}
const auto fallbackReason = ChildFailureReason(child, errorCode);
if (const auto attempt = findRouteAttempt(child)) {
const auto failure = TypedRouteFailure(
child,
fallbackReason,
SocketProxyConnectionError(errorCode),
ProxyCloseOrigin::NetworkError);
const auto typedReason = MtProxy::FromProxyMtproxyTerminalReason(
failure.reason);
const auto reason = (typedReason == MtProxy::FailureReason::None)
? fallbackReason
: typedReason;
RecordRouteOutcome(
_runtime,
_proxy,
attempt->ipIndex,
RouteOutcomeKind::Failed,
reason);
auto routeConnectionAttempt = _mtproxyAttempt;
routeConnectionAttempt.routeAttemptId = attempt->routeAttemptId;
ReportRouteEvent(
_runtime,
_proxy,
attempt->ipIndex,
ProxyDiagnosticsPhase::RouteFailed,
routeConnectionAttempt,
reason,
std::nullopt,
(failure.closeOrigin == ProxyCloseOrigin::None)
? std::optional<ProxyCloseOrigin>()
: std::make_optional(failure.closeOrigin),
failure.error);
MergeExhaustedFailure(_lastFailure, failure);
}
removeRouteAttempt(child);
if (_routeAttempts.empty() && _nextRoutePosition >= int(_routeOrder.size())) {
emitError(errorCode);
} else if (_routeAttempts.empty()) {
startNextRouteAttempt();
} else {
scheduleRouteRace();
}
}
void ResolvingConnection::handleDisconnected(AbstractConnection *child) {
if (_terminal) {
return;
}
if (_connected && _child.get() == child) {
disconnected();
} else if (!_connected) {
handleError(child, kErrorCodeOther);
}
}
void ResolvingConnection::handleReceivedData(AbstractConnection *child) {
if (_connected && _child.get() != child) {
return;
}
const auto attempt = findRouteAttempt(child);
const auto source = _connected ? _child.get() : (attempt
? attempt->child.get()
: nullptr);
if (!source) {
return;
}
auto &my = received();
auto &his = source->received();
for (auto &item : his) {
my.push_back(std::move(item));
}
his.clear();
receivedData();
}
void ResolvingConnection::promoteRouteAttempt(AbstractConnection *child) {
auto winner = ConnectionPointer();
auto winnerIpIndex = -1;
for (auto &attempt : _routeAttempts) {
if (attempt.child.get() == child) {
winner = std::move(attempt.child);
winnerIpIndex = attempt.ipIndex;
} else if (attempt.child) {
auto routeAttempt = _mtproxyAttempt;
routeAttempt.routeAttemptId = attempt.routeAttemptId;
RecordRouteOutcome(
_runtime,
_proxy,
attempt.ipIndex,
RouteOutcomeKind::RaceLost,
MtProxy::FailureReason::None);
ReportRouteEvent(
_runtime,
_proxy,
attempt.ipIndex,
ProxyDiagnosticsPhase::RouteFailed,
routeAttempt,
MtProxy::FailureReason::None,
std::nullopt,
ProxyCloseOrigin::RouteRaceLost);
attempt.child->disconnectFromServer();
}
}
_routeAttempts.clear();
if (!winner) {
return;
}
_child = std::move(winner);
_ipIndex = winnerIpIndex;
_runtime->proxyEndpointContext().updateTraceAttempt(
_child->proxyConnectionAttempt());
}
void ResolvingConnection::handleConnected(AbstractConnection *child) {
if (_connected || _terminal) {
return;
}
_connected = true;
promoteRouteAttempt(child);
if (!_child) {
_connected = false;
return;
}
_timeoutTimer.cancel();
_routeRaceTimer.cancel();
if (_ipIndex >= 0 && !IsProxyCheck(_mtproxyAttempt.use)) {
const auto host = _proxy.host;
const auto good = _proxy.resolvedIPs[_ipIndex];
const auto runtime = _runtime;
InvokeQueued(runtime, [=] {
if (runtime->proxyResolver().setGoodDomain) {
runtime->proxyResolver().setGoodDomain(host, good);
}
});
if (_proxy.type == ProxyData::Type::Mtproto) {
// This address of this proxy answered a Telegram reply, so it
// goes to the front of the order next time. Route memory only -
// it never influences the handshake shape.
const auto endpoint = MtproxyEndpointIdForRoute(
_proxy,
_ipIndex);
const auto routeKey = MtProxy::RouteKey(endpoint.route);
_runtime->proxyServices().capabilities().noteMtproxySuccess(
MtProxy::CapabilityProxyKey(endpoint.canonical),
routeKey,
routeKey,
_mtproxyPlan.effectiveTlsProfile,
_mtproxyPlan.stealth,
_mtproxyPlan.recipeLevel,
true);
}
}
connected();
}
crl::time ResolvingConnection::pingTime() const {
Expects(_child != nullptr);
return _child->pingTime();
}
crl::time ResolvingConnection::fullConnectTimeout() const {
const auto resolvingRaceTimeout = kRouteAttemptTimeout
+ kRouteRaceDelay * kMaxParallelRouteAttempts;
if (_proxy.type != ProxyData::Type::Mtproto) {
return std::min(
kFullConnectTimeout,
resolvingRaceTimeout + kOnlyRouteAttemptTimeout);
}
const auto serverHelloTimeout = (_mtproxyPlan.serverHelloTimeout > 0)
? _mtproxyPlan.serverHelloTimeout
: kColdServerHelloTimeout;
const auto phasedTimeout = resolvingRaceTimeout
+ kOnlyRouteAttemptTimeout
+ serverHelloTimeout
+ kOnlyRouteAttemptTimeout
+ kFullConnectTimeoutSafetyMargin;
return std::min(kFullConnectTimeout, phasedTimeout);
}
void ResolvingConnection::sendData(
mtpBuffer &&buffer,
SendDataContext context) {
Expects(_child != nullptr);
_child->sendData(std::move(buffer), context);
}
void ResolvingConnection::disconnectFromServer() {
_address = QString();
_port = 0;
_protocolSecret = bytes::vector();
_protocolDcId = 0;
_resolvingDeadline = 0;
_routeRaceTimer.cancel();
_timeoutTimer.cancel();
for (auto &attempt : _routeAttempts) {
if (attempt.child) {
attempt.child->disconnectFromServer();
}
}
_routeAttempts.clear();
if (!_child) {
return;
}
_child->disconnectFromServer();
}
void ResolvingConnection::connectToServer(
const QString &address,
int port,
const bytes::vector &protocolSecret,
int16 protocolDcId,
bool protocolForFiles,
ConnectionStartContext context) {
_mtproxyAttempt = context.mtproxyAttempt;
_mtproxyPlan = context.mtproxyPlan;
_mtproxyAttemptStartedAt = context.mtproxyAttemptStartedAt;
_wssFront = context.wssFront;
_terminal = false;
_lastFailure = {};
if (!_child) {
if (_proxy.type == ProxyData::Type::Mtproto) {
_lastFailure.reason = ProxyMtproxyTerminalReason::DnsFailed;
_lastFailure.error = ProxyConnectionError::HostNotFound;
_lastFailure.closeOrigin = ProxyCloseOrigin::NetworkError;
_lastFailure.parserStage = u"dns"_q;
_lastFailure.dnsMs = _dnsDuration;
_lastFailure.attribution = ProxyFailureAttribution::Network;
}
ReportProxyEvent(_runtime, {
.phase = ProxyDiagnosticsPhase::Failed,
.error = ProxyConnectionError::HostNotFound,
.mtproxyReason = (_proxy.type == ProxyData::Type::Mtproto)
? ProxyMtproxyTerminalReason::DnsFailed
: ProxyMtproxyTerminalReason::None,
.attempt = _mtproxyAttempt,
.terminalUntil = _proxy.resolvedExpireAt,
.proxy = _proxy,
.message = u"proxy host not found"_q,
});
InvokeQueued(this, [=] { emitError(kErrorCodeOther); });
return;
}
_address = address;
_port = port;
_protocolSecret = protocolSecret;
_protocolDcId = protocolDcId;
_protocolForFiles = protocolForFiles;
if (_proxy.resolvedIPs.empty()
|| _proxy.resolvedExpireAt < crl::now()) {
startResolving();
} else {
startRouteAttempts();
}
}
bool ResolvingConnection::isConnected() const {
return _child ? _child->isConnected() : false;
}
HandshakePhase ResolvingConnection::handshakePhase() const {
if (_connected && _child) {
return _child->handshakePhase();
}
auto result = HandshakePhase::None;
for (const auto &attempt : _routeAttempts) {
result = std::max(
result,
ChildHandshakePhase(attempt.child.get()));
}
return result;
}
ProxyConnectionAttempt ResolvingConnection::proxyConnectionAttempt() const {
if (_child) {
const auto result = _child->proxyConnectionAttempt();
if (result.traceId) {
return result;
}
}
if (!_routeAttempts.empty()) {
const auto result = _routeAttempts.front().child
->proxyConnectionAttempt();
if (result.traceId) {
return result;
}
}
return _mtproxyAttempt;
}
ProxyTransportFailure ResolvingConnection::proxyTransportFailure() const {
const auto withDns = [&](ProxyTransportFailure result) {
if (!result.dnsMs) {
result.dnsMs = _dnsDuration;
}
return result;
};
if (_connected && _child) {
return withDns(_child->proxyTransportFailure());
}
if (!_routeAttempts.empty()) {
const auto best = std::max_element(
begin(_routeAttempts),
end(_routeAttempts),
[](const RouteAttempt &a, const RouteAttempt &b) {
return int(ChildHandshakePhase(a.child.get()))
< int(ChildHandshakePhase(b.child.get()));
});
return withDns(best->child->proxyTransportFailure());
}
return withDns(_lastFailure);
}
void ResolvingConnection::timedOut() {
if (_terminal) {
return;
}
auto reported = false;
for (const auto &attempt : _routeAttempts) {
if (attempt.child) {
attempt.child->timedOut();
reported = true;
}
}
// _child is the template the routes are cloned from: it never dials, so
// its own report is always "tcp connect timed out" no matter how far the
// routes got. Letting it speak over them puts that reason on the proxy
// status the user sees. It is only the truth while no route exists yet.
if (!reported && _child) {
_child->timedOut();
}
}
void ResolvingConnection::markProxyMtprotoPayloadReceived() {
if (_child) {
_child->markProxyMtprotoPayloadReceived();
}
}
int32 ResolvingConnection::debugState() const {
if (!_connected && !_routeAttempts.empty()) {
return _routeAttempts.front().child->debugState();
}
return _child ? _child->debugState() : -1;
}
QString ResolvingConnection::transport() const {
if (!_connected && !_routeAttempts.empty()) {
return _routeAttempts.front().child->transport();
}
return _child ? _child->transport() : QString();
}
QString ResolvingConnection::tag() const {
if (!_connected && !_routeAttempts.empty()) {
return _routeAttempts.front().child->tag();
}
return _child ? _child->tag() : QString();
}
} // namespace details
} // namespace MTP