ZaStoGram_desktop/Telegram/SourceFiles/mtproto/session/private/connection.cpp
loop-uh 8015cb4f9d
All checks were successful
Desktop source guards / guards (push) Successful in 6s
Не выпадать из DC1 на проверку мёртвого релея и не замерзать в туннеле
По логу dev-27 (25.09, 18:40–18:45):

- После двух минут подавления релей kws1, к которому TCP не проходит ни
  разу, проверялся заново бюджетами 1+2+4+8 с, и DC1 каждые ~2 минуты
  выпадал на 15 с. Повторное подавление без единого ответа между ними
  удваивается: 2, 4, 8, 16, до 30 минут.
- Основная сессия DC1 через туннель замерзала после 11–15 КБ и ждала
  8 с таймаута приёма каждые 15–50 с. Ротация на границе пакета теперь
  есть и у основных сессий, после 8 КБ; у файловых — по-прежнему 4 КБ.
- Сокет туннеля медиа был готов только через 3,7 с и убивался бюджетом
  4 с; минимальный бюджет подключения через туннель — 6 с.
2026-09-25 18:46:04 +03:00

891 lines
30 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/session/private/session_private.h"
#include "mtproto/session/private/timings.h"
#include "core/version.h"
#include "mtproto/dc_id.h"
#include "mtproto/auth/mtproto_bound_key_creator.h"
#include "mtproto/details/mtproto_dcenter.h"
#include "mtproto/instance/mtp_instance.h"
#include "mtproto/protocol/mtproto_dump_to_text.h"
#include "mtproto/details/mtproto_rsa_public_key.h"
#include "mtproto/proxy/diagnostics.h"
#include "mtproto/proxy/dial_pacer.h"
#include "mtproto/proxy/mtproxy/handshake_plan.h"
#include "mtproto/proxy/transport_policy.h"
#include "mtproto/proxy/wss/socket.h"
#include "mtproto/runtime/runtime_environment.h"
#include "mtproto/session/options.h"
#include "mtproto/session/session.h"
#include "mtproto/protocol/mtproto_response.h"
#include "mtproto/config/mtproto_dc_options.h"
#include "mtproto/transport/details/mtproto_abstract_socket.h"
#include "mtproto/transport/connection_abstract.h"
#include "base/options.h"
#include "base/random.h"
#include "base/qthelp_url.h"
#include "base/openssl_help.h"
#include "base/unixtime.h"
#include "base/platform/base_platform_info.h"
#include <ksandbox.h>
#include <zlib.h>
namespace MTP {
namespace details {
namespace {
constexpr auto kWaitForBetterTimeout = crl::time(2000);
constexpr auto kMaxConnectedTimeout = crl::time(8000);
// The Cloudflare tunnel needs ~0.7 s for TCP, TLS and the upgrade and answers
// the first MTProto packet at ~1.1 s, sometimes only at ~4.2 s (desktop logs
// 25.09): the 1 s first wait
// killed nearly every tunnel attempt, and with a connection reopened after
// each file piece that was most of them.
constexpr auto kTunnelMinConnectedTimeout = crl::time(6000);
constexpr auto kMtproxyMinReceiveTimeout = crl::time(8000);
constexpr auto kMaxReceiveTimeout = crl::time(64000);
constexpr auto kProxyReconnectMinTimeout = 1800;
constexpr auto kProxyReconnectMaxTimeout = 8000;
constexpr auto kWaitForProxyTimeout = 2000;
constexpr auto kMarkConnectionOldTimeout = crl::time(192000);
constexpr auto kRequestConfigTimeout = 8 * crl::time(1000);
constexpr auto kSilentTimeoutsToAssumeKeyDestroyed = 2;
// A WEB proxy stream is a relay stream to an MTProxy, and a -404 on it is
// as likely to come from that hop being reset as from the server having
// lost our temporary key. Recreating a key is expensive, so the first -404
// only reconnects; a second one before any reply decrypts is believed.
constexpr auto kWebKeyNotFoundStrikesToAssumeKeyDestroyed = 2;
base::options::toggle OptionPreferIPv6({
.id = kOptionPreferIPv6,
.name = "Prefer IPv6",
.description = "Prefer IPv6 if it is available. Require \"Try connecting through IPv6\" to be enabled",
});
} // namespace
ProxyConnectionUse SessionTransport::classifyEndpointUse() const {
return isUploadDcId(_owner->_shiftedDcId)
? ProxyConnectionUse::Upload
: (isMediaClusterDcId(_owner->_shiftedDcId)
|| _owner->_realDcType == DcType::Cdn)
? ProxyConnectionUse::Media
: (_owner->_role == SessionRole::PrimaryMain)
? ProxyConnectionUse::Main
: (_owner->_role == SessionRole::Maintenance)
? ProxyConnectionUse::Maintenance
: ProxyConnectionUse::Auxiliary;
}
bool SessionTransport::appendTestConnection(
DcOptions::Variants::Protocol protocol,
const QString &ip,
int port,
const bytes::vector &protocolSecret,
bool protocolForFiles) {
QWriteLocker lock(&_owner->_stateMutex);
const auto proxy = _owner->_sessionState.options->proxy;
const auto stealth = _owner->_sessionState.options->stealth;
const auto endpoint = ip.isEmpty()
? (proxy.host + ':' + QString::number(proxy.port))
: (ip + ':' + QString::number(port));
const auto priority = (qthelp::is_ipv6(ip) ? (OptionPreferIPv6.value() ? 2 : 0) : 1)
+ (protocol == DcOptions::Variants::Tcp ? 1 : 0)
+ (protocolSecret.empty() ? 0 : 1);
const auto mtproxy = (proxy.type == ProxyData::Type::Mtproto);
const auto mtproxyUse = classifyEndpointUse();
const auto protocolDcId = _owner->getProtocolDcId();
auto attempt = ProxyConnectionAttempt{
.proxyGeneration = _state.proxyGeneration,
.use = mtproxyUse,
};
// Without a plan the socket falls back to its own emergency defaults,
// which wait for the ServerHello for half as long as the handshake is
// meant to - so a proxy the settings check calls working has its
// handshake killed early on every real session.
const auto plan = mtproxy
? MtProxy::MakeAttemptPlan(stealth)
: MtProxyAttemptPlan();
if (mtproxy) {
attempt.runtimeId = _owner->_runtime->proxyRuntimeId();
}
const auto attemptStartedAt = mtproxy ? crl::now() : crl::time();
// Ordinary MTProto sessions own their connection lifecycle directly: no
// admission queue, no health cooldown, no cross-account head-of-line
// blocking. The one thing a proxy does impose is that it cannot answer
// every session of every account handshaking in the same millisecond, so
// the dial itself is paced per proxy server.
auto dial = ReserveProxyDial(_owner->_runtime, proxy);
const auto dialDelay = dial.delay();
_state.testConnections.push_back({
.data = _owner->_connectionFactory->create(
_owner->_runtime,
protocol,
_owner->thread(),
protocolSecret,
proxy,
stealth),
.priority = priority,
.endpoint = endpoint,
.mtproxyUse = mtproxyUse,
.mtproxyAttempt = attempt,
.mtproxyAttemptStartedAt = attemptStartedAt,
.mtproxyDial = std::move(dial),
.mtproxyDialDelay = dialDelay,
});
const auto weak = _state.testConnections.back().data.get();
QObject::connect(weak, &AbstractConnection::error, [=](int errorCode) {
onError(weak, errorCode);
});
QObject::connect(weak, &AbstractConnection::receivedSome, [=] {
onReceivedSome();
});
_timing.firstSentAt = 0;
if (_timing.oldConnection) {
_timing.oldConnection = false;
DEBUG_LOG(("This connection marked as not old!"));
}
_timing.oldConnectionTimer.callOnce(kMarkConnectionOldTimeout);
QObject::connect(weak, &AbstractConnection::connected, [=] {
onConnected(weak);
});
QObject::connect(weak, &AbstractConnection::disconnected, [=] {
onDisconnected(weak);
});
QObject::connect(weak, &AbstractConnection::syncTimeRequest, [=] {
InvokeQueued(_owner->_runtime, [runtime = _owner->_runtime] {
if (runtime->instance().syncHttpUnixtime) {
runtime->instance().syncHttpUnixtime();
}
});
});
const auto start = [=] {
weak->connectToServer(
ip,
port,
protocolSecret,
protocolDcId,
protocolForFiles,
{
.mtproxyAttempt = attempt,
.mtproxyPlan = plan,
.mtproxyAttemptStartedAt = attemptStartedAt,
});
};
if (dialDelay > 0) {
_owner->_runtime->async().singleShot(dialDelay, weak, start);
} else {
InvokeQueued(_state.testConnections.back().data, start);
}
armWaitForConnectedTimer();
return true;
}
void SessionTransport::destroyAllConnections(ProxyCloseOrigin) {
_owner->clearUnboundKeyCreator();
_timing.waitForBetterTimer.cancel();
_timing.waitForReceivedTimer.cancel();
_timing.waitForConnectedTimer.cancel();
clearTestConnections();
_state.connection.reset();
_state.mtproxyUse = ProxyConnectionUse::Main;
_state.mtproxyAttempt = {};
_state.mtproxyAttemptStartedAt = 0;
_state.mtprotoDataReceived = false;
}
void SessionTransport::clearTestConnections() {
for (auto &connection : _state.testConnections) {
// Whatever brings us here - a route race that someone else won, a
// proxy switch, a restart - these connections are dropped by us and
// not by the proxy. The one that really failed was already removed
// through removeTestConnection(), where its lease counts as a miss.
connection.mtproxyDial.cancel();
connection.data.reset();
}
_state.testConnections.clear();
_timing.waitForConnectedTimer.cancel();
}
void SessionTransport::armWaitForConnectedTimer() {
if (_state.testConnections.empty()) {
_timing.waitForConnectedTimer.cancel();
return;
}
// A proxied connect needs its whole budget (tcp connect with SYN
// retransmits plus the FakeTLS handshake) - killing it after
// kMinConnectedTimeout only burns a handshake against the DPI and
// reconnects, and repeated fresh handshakes are exactly what gets
// proxies throttled. Direct connections keep the short first wait.
auto wait = _timing.waitForConnected;
if (const auto &options = _owner->_sessionState.options;
options
&& options->proxy.type == ProxyData::Type::None
&& options->stealth.transport == ProxyTransport::Wss
&& !WssCustomRoute(options->stealth)) {
const auto route = WssOfficialRoute(_owner->getProtocolDcId());
if (route && route->tunnel) {
accumulate_max(wait, kTunnelMinConnectedTimeout);
}
}
if (_owner->_sessionState.options && (_owner->_sessionState.options->proxy.type != ProxyData::Type::None)) {
auto minWait = crl::time(0);
for (const auto &connection : _state.testConnections) {
// A paced attempt has not spent any of its budget yet, so the
// wait must cover the pacing delay as well or the timer fires
// before the socket is even opened. The delay belongs to this
// attempt only: folding it into waitForConnected would leave
// every later attempt of this session waiting out a queue it is
// no longer in, since that budget is reset only by a connect.
accumulate_max(
minWait,
connection.data->fullConnectTimeout()
+ connection.mtproxyDialDelay);
}
accumulate_max(wait, minWait);
}
// Connections are appended one after another, and the one that got the
// long pacing delay is rarely the first. Leaving the timer that the first
// one armed in place kills the queued attempt before it dials, which is
// exactly the wait the pacer was granting it. Never shorten the timer,
// only stretch it to the budget the widest attempt now needs.
if (!_timing.waitForConnectedTimer.isActive()
|| (wait > _timing.waitForConnectedArmed)) {
_timing.waitForConnectedArmed = wait;
_timing.waitForConnectedTimer.callOnce(wait);
}
}
void SessionTransport::retryByTimer() {
const auto proxied = _owner->_sessionState.options
&& (_owner->_sessionState.options->proxy.type != ProxyData::Type::None);
const auto maxTimeout = proxied ? kProxyReconnectMaxTimeout : 64000;
if (_timing.retryTimeout < 3) {
++_timing.retryTimeout;
} else if (_timing.retryTimeout == 3) {
_timing.retryTimeout = proxied ? kProxyReconnectMinTimeout : 1000;
} else if (_timing.retryTimeout < maxTimeout) {
_timing.retryTimeout = std::min(_timing.retryTimeout * 2, maxTimeout);
}
connectToServer();
}
void SessionTransport::restartNow() {
_timing.retryTimeout = 1;
_timing.retryTimer.cancel();
restart();
}
void SessionTransport::migrateProxy(uint64 generation) {
_state.proxyGeneration = generation;
_state.mtproxyAttempt = { .proxyGeneration = generation };
_owner->_sessionState.options = std::make_unique<SessionOptions>(_owner->_sessionState.data->options());
_timing.retryTimer.cancel();
_timing.retryTimeout = 1;
_timing.waitForReceivedTimer.cancel();
_timing.waitForConnectedTimer.cancel();
_timing.waitForBetterTimer.cancel();
_owner->logMtprotoEvent(
ProxyDiagnosticsPhase::MtpRestart,
ProxyDiagnosticsSeverity::Info,
u"proxy_route_changed"_q);
destroyAllConnections(ProxyCloseOrigin::ProxySwitch);
_state.mtproxyAttempt = { .proxyGeneration = generation };
_owner->setState(DisconnectedState);
connectToServer();
}
void SessionTransport::connectToServer(bool afterConfig) {
if (afterConfig
&& (!_state.testConnections.empty()
|| _state.connection)) {
return;
}
destroyAllConnections(ProxyCloseOrigin::RouteRaceLost);
if (_owner->realDcTypeChanged() && _owner->_authState.keyCreator) {
_owner->destroyTemporaryKey();
return;
}
_owner->_sessionState.options = std::make_unique<SessionOptions>(_owner->_sessionState.data->options());
_owner->setConnectionNotice(MTP::ConnectionNotice::None);
if (_owner->_sessionState.options->proxy.type == ProxyData::Type::None
&& _owner->_sessionState.options->stealth.transport != ProxyTransport::Wss) {
DEBUG_LOG(("MTP Info: proxy required, "
"waiting for a proxy before connecting."));
_owner->setState(-kWaitForProxyTimeout);
return;
}
const auto bareDc = BareDcId(_owner->_shiftedDcId);
_owner->_currentDcType = _owner->tryAcquireKeyCreation();
_owner->dropMismatchedTemporaryKey();
if (_owner->_currentDcType == DcType::Cdn && !_owner->_delegate->isKeysDestroyer()) {
if (!_owner->_delegate->dcOptions().hasCDNKeysForDc(bareDc)) {
requestCDNConfig();
return;
}
}
const auto protocolForFiles = isMediaClusterDcId(_owner->_shiftedDcId)
|| (_owner->_realDcType == DcType::Cdn);
const auto protocolDcId = _owner->getProtocolDcId();
_owner->setConnectionNotice(WssNeedsProxyRecommendation(
_owner->_sessionState.options->proxy,
_owner->_sessionState.options->stealth,
protocolDcId)
? MTP::ConnectionNotice::WssDirectFallback
: MTP::ConnectionNotice::None);
if (_owner->_sessionState.options->proxy.type == ProxyData::Type::Mtproto) {
// host, port, secret for mtproto proxy are taken from proxy.
if (!appendTestConnection(
DcOptions::Variants::Tcp,
{},
0,
{},
protocolForFiles)) {
return;
}
} else {
using Variants = DcOptions::Variants;
const auto special = (_owner->_currentDcType == DcType::Temporary);
const auto variants = _owner->_delegate->dcOptions().lookup(
bareDc,
_owner->_currentDcType,
_owner->_sessionState.options->proxy.type != ProxyData::Type::None);
const auto useIPv4 = special ? true : _owner->_sessionState.options->useIPv4;
const auto useIPv6 = special ? false : _owner->_sessionState.options->useIPv6;
const auto useTcp = special ? true : _owner->_sessionState.options->useTcp;
const auto useHttp = special ? false : _owner->_sessionState.options->useHttp;
const auto skipAddress = !useIPv4
? Variants::IPv4
: !useIPv6
? Variants::IPv6
: Variants::AddressTypeCount;
const auto skipProtocol = !useTcp
? Variants::Tcp
: !useHttp
? Variants::Http
: Variants::ProtocolCount;
// A WEB proxy stream reaches the same relay and the same MTProxy
// whatever DC address it is given, so racing endpoints would only
// open identical streams on the one shared carrier.
const auto single = webProxy();
const auto enough = [&] {
return single && !_state.testConnections.empty();
};
for (auto address = 0; address != Variants::AddressTypeCount && !enough(); ++address) {
if (address == skipAddress) {
continue;
}
for (auto protocol = 0; protocol != Variants::ProtocolCount && !enough(); ++protocol) {
if (protocol == skipProtocol) {
continue;
}
for (const auto &endpoint : variants.data[address][protocol]) {
(void)appendTestConnection(
static_cast<Variants::Protocol>(protocol),
QString::fromStdString(endpoint.ip),
endpoint.port,
endpoint.secret,
protocolForFiles);
if (enough()) {
break;
}
}
}
}
}
if (_state.testConnections.empty()) {
if (_owner->_delegate->isKeysDestroyer()) {
LOG(("MTP Error: DC %1 options for not found for auth key destruction!").arg(_owner->_shiftedDcId));
_owner->_delegate->keyWasPossiblyDestroyed(_owner->_shiftedDcId);
return;
} else if (afterConfig) {
LOG(("MTP Error: DC %1 options for not found right after config load!").arg(_owner->_shiftedDcId));
return restart();
}
DEBUG_LOG(("MTP Info: DC %1 options not found, waiting for config").arg(_owner->_shiftedDcId));
InvokeQueued(_owner->_instance, [delegate = _owner->_delegate] {
delegate->requestConfig();
});
return;
}
DEBUG_LOG(("Connection Info: Connecting to %1 with %2 test connections."
).arg(_owner->_shiftedDcId
).arg(_state.testConnections.size()));
if (!_state.startedConnectingAt) {
_state.startedConnectingAt = crl::now();
} else if (crl::now() - _state.startedConnectingAt > kRequestConfigTimeout) {
InvokeQueued(_owner->_instance, [delegate = _owner->_delegate] {
delegate->requestConfigIfOld();
});
}
_timing.retryTimer.cancel();
_timing.waitForConnectedTimer.cancel();
_owner->logMtprotoEvent(
ProxyDiagnosticsPhase::MtpConnecting,
ProxyDiagnosticsSeverity::Info,
u"connecting (sockets: %1)"_q.arg(_state.testConnections.size()));
_owner->setState(ConnectingState);
_owner->_authState.bindMsgId = 0;
_owner->_requestState.pingId = _owner->_requestState.pingMsgId = _owner->_requestState.pingIdToSend = _owner->_requestState.pingSendAt = 0;
_owner->_requestState.pingSentTime = 0;
_owner->reportPingTime(0);
_timing.pingSender.cancel();
if (!_state.testConnections.empty()) {
armWaitForConnectedTimer();
}
}
void SessionTransport::restart() {
DEBUG_LOG(("MTP Info: restarting Connection"));
_timing.waitForReceivedTimer.cancel();
_timing.waitForConnectedTimer.cancel();
doDisconnect();
if (_owner->_sessionState.needReset) {
_owner->resetSession();
}
if (_timing.retryTimer.isActive()) {
return;
}
if (_owner->_sessionState.options
&& (_owner->_sessionState.options->proxy.type != ProxyData::Type::None)
&& (_timing.retryTimeout < kProxyReconnectMinTimeout)) {
_timing.retryTimeout = kProxyReconnectMinTimeout;
}
DEBUG_LOG(("MTP Info: restart timeout: %1ms").arg(_timing.retryTimeout));
_owner->logMtprotoEvent(
ProxyDiagnosticsPhase::MtpRestart,
ProxyDiagnosticsSeverity::Info,
u"restarting (backoff %1ms)"_q.arg(_timing.retryTimeout));
_owner->setState(-_timing.retryTimeout);
}
void SessionTransport::onSentSome(uint64 size) {
if (!_timing.waitForReceivedTimer.isActive()) {
auto remain = static_cast<uint64>(_timing.waitForReceived);
if (_state.connection
&& _owner->_sessionState.options
&& _owner->_sessionState.options->proxy.type
== ProxyData::Type::Mtproto) {
accumulate_max(
remain,
static_cast<uint64>(kMtproxyMinReceiveTimeout));
}
if (!_timing.oldConnection) {
Assert(remain <= kMaxReceiveTimeout);
// 8kb / sec, so 512 kb give 64 sec
auto remainBySize = size * _timing.waitForReceived / 8192;
remain = std::clamp(
remainBySize,
remain,
uint64(kMaxReceiveTimeout));
if (remain != _timing.waitForReceived) {
DEBUG_LOG(("Checking connect for request with size %1 bytes, delay will be %2").arg(size).arg(remain));
}
}
_timing.waitForReceivedTimer.callOnce(remain);
_timing.waitForReceivedStartedAt = crl::now();
_timing.waitForReceivedExtended = false;
}
if (!_timing.firstSentAt) {
_timing.firstSentAt = crl::now();
}
}
void SessionTransport::onReceivedSome() {
if (_timing.oldConnection) {
_timing.oldConnection = false;
DEBUG_LOG(("This connection marked as not old!"));
}
_timing.oldConnectionTimer.callOnce(kMarkConnectionOldTimeout);
_timing.waitForReceivedTimer.cancel();
_timing.waitForReceivedStartedAt = 0;
if (_timing.firstSentAt > 0) {
const auto ms = crl::now() - _timing.firstSentAt;
DEBUG_LOG(("MTP Info: response in %1ms, _waitForReceived: %2ms"
).arg(ms
).arg(_timing.waitForReceived));
if (ms > 0 && ms * 2 < _timing.waitForReceived) {
_timing.waitForReceived = std::max(ms * 2, kMinReceiveTimeout);
}
_timing.firstSentAt = -1;
}
}
void SessionTransport::markConnectionOld() {
_timing.oldConnection = true;
_timing.waitForReceived = kMinReceiveTimeout;
DEBUG_LOG(("This connection marked as old! _waitForReceived now %1ms"
).arg(_timing.waitForReceived));
}
bool SessionTransport::webProxy() const {
return _owner->_sessionState.options
&& (_owner->_sessionState.options->proxy.type
== ProxyData::Type::Web);
}
bool SessionTransport::extendWebProxyReceiveWait() {
_timing.waitForReceivedDetails = QString();
if (!_state.connection || !webProxy()) {
return false;
}
const auto now = crl::now();
const auto startedAt = _timing.waitForReceivedStartedAt
? _timing.waitForReceivedStartedAt
: (now - _timing.waitForReceived);
const auto verdict = _state.connection->receiveWaitVerdict(startedAt);
_timing.waitForReceivedDetails = verdict.details;
if (verdict.waitMore <= 0) {
return false;
}
// The reply is plausibly still queued in the carrier that every WEB
// session shares, and that carrier is moving; a dead carrier is torn
// down by the carrier itself, for all of its streams at once.
if (!_timing.waitForReceivedExtended) {
_timing.waitForReceivedExtended = true;
_owner->logMtprotoEvent(
ProxyDiagnosticsPhase::WebCarrier,
ProxyDiagnosticsSeverity::Info,
u"receive wait extended after %1ms (%2)"_q
.arg(now - startedAt)
.arg(verdict.details));
}
_timing.waitForReceivedTimer.callOnce(verdict.waitMore);
return true;
}
void SessionTransport::waitReceivedFailed() {
Expects(_owner->_sessionState.options != nullptr);
_quietReconnect = false;
if (extendWebProxyReceiveWait()) {
return;
}
DEBUG_LOG(("MTP Info: bad connection, _waitForReceived: %1ms").arg(_timing.waitForReceived));
if (_timing.waitForReceived < kMaxReceiveTimeout) {
_timing.waitForReceived = std::min(
_timing.waitForReceived * 2,
kMaxReceiveTimeout);
}
const auto mtproxyConnection = _state.connection
&& _owner->_sessionState.options->proxy.type
== ProxyData::Type::Mtproto;
const auto silentMtproxyConnection = mtproxyConnection
&& !_state.mtprotoDataReceived;
if (silentMtproxyConnection) {
++_state.mtprotoSilentTimeouts;
}
const auto webDetails = base::take(_timing.waitForReceivedDetails);
_owner->logMtprotoEvent(
ProxyDiagnosticsPhase::MtpReceiveTimeout,
ProxyDiagnosticsSeverity::Warning,
u"no mtproto data in %1ms (received before: %2, silent strikes: %3)"_q
.arg(_timing.waitForReceived)
.arg(_state.mtprotoDataReceived ? u"yes"_q : u"no"_q)
.arg(_state.mtprotoSilentTimeouts)
+ (webDetails.isEmpty() ? QString() : (u", "_q + webDetails)));
if (_state.connection
&& _owner->_sessionState.options->proxy.type == ProxyData::Type::None
&& _owner->_sessionState.options->stealth.transport
== ProxyTransport::Wss) {
// Lets a WSS socket tell a frozen Cloudflare tunnel from a slow one;
// proxy connections keep their own receive-timeout accounting.
_state.connection->timedOut();
}
doDisconnect();
if (silentMtproxyConnection
&& (_state.mtprotoSilentTimeouts >= kSilentTimeoutsToAssumeKeyDestroyed)) {
_state.mtprotoSilentTimeouts = 0;
LOG(("MTP Info: dc %1 connected but received nothing %2 times, "
"assuming the temporary key was silently destroyed."
).arg(_owner->_shiftedDcId
).arg(kSilentTimeoutsToAssumeKeyDestroyed));
return _owner->destroyTemporaryKey();
}
if (_timing.retryTimer.isActive()) {
return;
}
if (_owner->_sessionState.options->proxy.type != ProxyData::Type::None) {
if (_timing.retryTimeout < kProxyReconnectMinTimeout) {
_timing.retryTimeout = kProxyReconnectMinTimeout;
}
DEBUG_LOG(("MTP Info: proxy reconnect backoff %1ms!"
).arg(_timing.retryTimeout));
_owner->setState(-_timing.retryTimeout);
} else {
DEBUG_LOG(("MTP Info: immediate restart!"));
InvokeQueued(_owner, [=] { connectToServer(); });
}
const auto instance = _owner->_instance;
const auto delegate = _owner->_delegate;
const auto shiftedDcId = _owner->_shiftedDcId;
InvokeQueued(instance, [=] {
delegate->restartedByTimeout(shiftedDcId);
});
}
void SessionTransport::waitConnectedFailed() {
if (_state.testConnections.empty()) {
return;
}
const auto waited = _timing.waitForConnectedArmed
? _timing.waitForConnectedArmed
: _timing.waitForConnected;
DEBUG_LOG(("MTP Info: can't connect in %1ms").arg(waited));
_quietReconnect = false;
_owner->logMtprotoEvent(
ProxyDiagnosticsPhase::MtpConnectTimeout,
ProxyDiagnosticsSeverity::Warning,
u"connect budget %1ms expired (sockets: %2)"_q
.arg(waited)
.arg(_state.testConnections.size()));
auto maxTimeout = kMaxConnectedTimeout;
for (const auto &connection : _state.testConnections) {
accumulate_max(maxTimeout, connection.data->fullConnectTimeout());
}
if (_timing.waitForConnected < maxTimeout) {
_timing.waitForConnected = std::min(maxTimeout, 2 * _timing.waitForConnected);
}
connectingTimedOut();
if (_owner->_sessionState.options
&& (_owner->_sessionState.options->proxy.type != ProxyData::Type::None)
&& !_timing.retryTimer.isActive()) {
if (_timing.retryTimeout < kProxyReconnectMinTimeout) {
_timing.retryTimeout = kProxyReconnectMinTimeout;
}
DEBUG_LOG(("MTP Info: proxy reconnect backoff %1ms!"
).arg(_timing.retryTimeout));
_owner->setState(-_timing.retryTimeout);
} else {
DEBUG_LOG(("MTP Info: immediate restart!"));
InvokeQueued(_owner, [=] { connectToServer(); });
}
}
void SessionTransport::waitBetterFailed() {
confirmBestConnection();
}
void SessionTransport::connectingTimedOut() {
for (auto &connection : _state.testConnections) {
connection.data->timedOut();
}
doDisconnect();
}
void SessionTransport::doDisconnect() {
destroyAllConnections();
_owner->setState(DisconnectedState);
}
void SessionTransport::requestCDNConfig() {
InvokeQueued(_owner->_instance, [delegate = _owner->_delegate] {
delegate->requestCDNConfig();
});
}
void SessionTransport::onConnected(
not_null<AbstractConnection*> connection) {
QObject::disconnect(connection, &AbstractConnection::connected, nullptr, nullptr);
if (!connection->isConnected()) {
LOG(("Connection Error: not connected in onConnected(), "
"state: %1").arg(connection->debugState()));
return restart();
}
_timing.waitForConnected = kMinConnectedTimeout;
_timing.waitForConnectedTimer.cancel();
const auto i = ranges::find(
_state.testConnections,
connection.get(),
[](const TestConnection &test) { return test.data.get(); });
Assert(i != end(_state.testConnections));
// The proxy answered a real Telegram reply through this socket: the
// dial slot is free for the next session even while we keep waiting for
// a higher priority route, and the failure spacing resets.
i->mtproxyDial.proven();
const auto my = i->priority;
const auto j = ranges::find_if(
_state.testConnections,
[&](const TestConnection &test) { return test.priority > my; });
if (j != end(_state.testConnections)) {
DEBUG_LOG(("MTP Info: connection %1 succeed, waiting for %2.").arg(
i->data->tag(),
j->data->tag()));
_timing.waitForBetterTimer.callOnce(kWaitForBetterTimeout);
} else {
DEBUG_LOG(("MTP Info: connection through IPv4 succeed."));
_timing.waitForBetterTimer.cancel();
_state.mtproxyUse = i->mtproxyUse;
_state.mtproxyAttempt = i->mtproxyAttempt;
_state.mtproxyAttemptStartedAt = i->mtproxyAttemptStartedAt;
_state.connection = std::move(i->data);
_state.testConnections.erase(i);
clearTestConnections();
_owner->checkAuthKey();
}
}
void SessionTransport::onDisconnected(
not_null<AbstractConnection*> connection) {
const auto found = ranges::find(
_state.testConnections,
connection.get(),
[](const TestConnection &test) { return test.data.get(); });
if (found == end(_state.testConnections)
&& _state.connection.get() != connection.get()) {
return;
}
const auto rotating = connection->rotating();
removeTestConnection(connection);
if (_state.testConnections.empty()) {
_quietReconnect = rotating;
destroyAllConnections();
restart();
} else if (!_state.testConnections.empty()) {
confirmBestConnection();
}
}
void SessionTransport::confirmBestConnection() {
if (_timing.waitForBetterTimer.isActive()) {
return;
}
const auto i = ranges::max_element(
_state.testConnections,
std::less<>(),
[](const TestConnection &test) {
return test.data->isConnected()
? test.priority
: -1;
});
Assert(i != end(_state.testConnections));
if (!i->data->isConnected()) {
return;
}
DEBUG_LOG(("MTP Info: can't connect through better, using %1."
).arg(i->data->tag()));
// This one is connected - it just was not the route we hoped for. Its
// lease used to die unclaimed here, which the pacer reads as a miss and
// answers with the failure spacing, on a proxy that had in fact just
// carried a Telegram reply.
i->mtproxyDial.proven();
_state.mtproxyAttempt = i->mtproxyAttempt;
_state.mtproxyAttemptStartedAt = i->mtproxyAttemptStartedAt;
_state.mtproxyUse = i->mtproxyUse;
_state.connection = std::move(i->data);
_state.testConnections.erase(i);
clearTestConnections();
_owner->checkAuthKey();
}
void SessionTransport::removeTestConnection(
not_null<AbstractConnection*> connection) {
const auto i = ranges::find(
_state.testConnections,
connection.get(),
[](const TestConnection &test) { return test.data.get(); });
if (i != end(_state.testConnections)) {
i->data.reset();
_state.testConnections.erase(i);
armWaitForConnectedTimer();
}
}
void SessionTransport::onError(
not_null<AbstractConnection*> connection,
qint32 errorCode) {
if (errorCode == -429) {
LOG(("Protocol Error: -429 flood code returned!"));
} else if (errorCode == -444) {
LOG(("Protocol Error: -444 bad dc_id code returned!"));
InvokeQueued(_owner->_instance, [delegate = _owner->_delegate] {
delegate->badConfigurationError();
});
}
const auto found = ranges::find(
_state.testConnections,
connection.get(),
[](const TestConnection &test) { return test.data.get(); });
if (found == end(_state.testConnections)
&& _state.connection.get() != connection.get()) {
return;
}
removeTestConnection(connection);
if (_state.testConnections.empty()) {
handleError(errorCode);
} else if (!_state.testConnections.empty()) {
confirmBestConnection();
}
}
void SessionTransport::handleError(int errorCode) {
destroyAllConnections();
_timing.waitForConnectedTimer.cancel();
if (errorCode == -404
&& webProxy()
&& (++_state.webKeyNotFoundStrikes
< kWebKeyNotFoundStrikesToAssumeKeyDestroyed)) {
_owner->logMtprotoEvent(
ProxyDiagnosticsPhase::WebCarrier,
ProxyDiagnosticsSeverity::Warning,
u"-404 on a web proxy stream (strike %1 of %2): "
"reconnecting the stream, keeping the temporary key"_q
.arg(_state.webKeyNotFoundStrikes)
.arg(kWebKeyNotFoundStrikesToAssumeKeyDestroyed));
return restart();
} else if (errorCode == -404) {
_state.webKeyNotFoundStrikes = 0;
_owner->destroyTemporaryKey();
} else {
MTP_LOG(_owner->_shiftedDcId, ("Restarting after error in connection, error code: %1...").arg(errorCode));
return restart();
}
}
} // namespace details
} // namespace MTP