ZaStoGram_desktop/Telegram/SourceFiles/mtproto/session/private/auth.cpp
loop-uh d5744ab90f
All checks were successful
Desktop source guards / guards (push) Successful in 6s
Держать файловые сессии тёплыми и не использовать забытый ключ
По логу ПК 01.10 каждая из 577 загрузок шла по новой сессии: ~1.8 с из
2.3 с уходило на открытие соединения через фронт (TCP, TLS, апгрейд
WebSocket, проверка транспорта, соль).

- Простаивающая файловая сессия живёт 75 с вместо 15 с, и на загрузке,
  и на отправке. Через туннель остаётся 15 с: там соединение и так
  открывается заново после каждого куска.
- stopSession() искал upload-сессию по id без датацентра и не находил
  её: getSession() запускает такую сессию на основном DC. Две сессии
  оставались после первой отправки и переподключались каждые 93 с до
  конца работы (454 соединения на 8 отправок).
- Сессия перед использованием временного ключа проверяет, что он всё
  ещё ключ своего DC. updateAuthKey() пропускает сессию, которая
  переподключается, и после смены маршрута между WEB-прокси и WSS один
  уничтоженный ключ отвергался у каждой сессии отдельно: загрузки
  стояли 20–30 с.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 17:57:18 +03:00

433 lines
14 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 "core/version.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/transport_policy.h"
#include "mtproto/session/session.h"
#include "mtproto/protocol/mtproto_response.h"
#include "mtproto/config/mtproto_dc_options.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 kTemporaryExpiresIn = TimeId(86400);
constexpr auto kBindKeyAdditionalExpiresTimeout = TimeId(30);
constexpr auto kKeyOldEnoughForDestroy = 60 * crl::time(1000);
} // namespace
SessionMessageHandler::HandleResult SessionMessageHandler::handleBindResponse(
mtpMsgId requestMsgId,
const mtpBuffer &response) {
if (!_owner->_authState.keyCreator
|| !_owner->_authState.bindMsgId
|| _owner->_authState.bindMsgId != requestMsgId) {
return HandleResult::Ignored;
}
_owner->_authState.bindMsgId = 0;
const auto result = _owner->_authState.keyCreator->handleBindResponse(
response);
switch (result) {
case DcKeyBindState::Success:
if (!_owner->_sessionState.data->releaseKeyCreationOnDone(
_owner->_sessionState.encryptionKey,
base::take(_owner->_authState.keyCreator)->bindPersistentKey())) {
return HandleResult::DestroyTemporaryKey;
}
_owner->logMtprotoEvent(
ProxyDiagnosticsPhase::MtpKeyReady,
ProxyDiagnosticsSeverity::Info,
u"temporary key bound (id %1)"_q.arg(_owner->_sessionState.keyId));
_owner->_sessionState.data->queueNeedToResumeAndSend();
return HandleResult::Success;
case DcKeyBindState::DefinitelyDestroyed:
if (_owner->destroyOldEnoughPersistentKey()) {
_owner->logMtprotoEvent(
ProxyDiagnosticsPhase::MtpBindFailed,
ProxyDiagnosticsSeverity::Warning,
u"bind failed, persistent key destroyed on server"_q);
return HandleResult::DestroyTemporaryKey;
}
[[fallthrough]];
case DcKeyBindState::Failed:
_owner->logMtprotoEvent(
ProxyDiagnosticsPhase::MtpBindFailed,
ProxyDiagnosticsSeverity::Warning,
u"temporary key bind failed"_q);
_owner->_sessionState.data->queueNeedToResumeAndSend();
return HandleResult::Success;
}
Unexpected("Result of SessionBoundKeyCreator::handleBindResponse.");
}
void SessionPrivate::checkAuthKey() {
logMtprotoEvent(
ProxyDiagnosticsPhase::MtpTransportReady,
ProxyDiagnosticsSeverity::Info,
u"transport ready via %1, handshake %2ms, key id %3"_q
.arg(_transport.connectionTag())
.arg(_transport.connectionPingTime())
.arg(_sessionState.keyId));
dropForgottenTemporaryKey();
if (_sessionState.keyId) {
authKeyChecked();
} else if (_delegate->isKeysDestroyer()) {
applyAuthKey(_sessionState.data->getPersistentKey());
} else {
applyAuthKey(_sessionState.data->getTemporaryKey(
TemporaryKeyTypeByDcType(_currentDcType)));
}
}
void SessionPrivate::updateAuthKey() {
if (_delegate->isKeysDestroyer()
|| _authState.keyCreator
|| !_transport.connection()) {
return;
}
DEBUG_LOG(("AuthKey Info: Connection updating key from Session, dc %1"
).arg(_shiftedDcId));
applyAuthKey(_sessionState.data->getTemporaryKey(
TemporaryKeyTypeByDcType(_currentDcType)));
}
void SessionPrivate::setCurrentKeyId(uint64 newKeyId) {
if (_sessionState.keyId == newKeyId) {
return;
}
_sessionState.keyId = newKeyId;
DEBUG_LOG(("MTP Info: auth key id set to id %1").arg(newKeyId));
changeSessionId();
}
void SessionPrivate::applyAuthKey(AuthKeyPtr &&encryptionKey) {
_sessionState.encryptionKey = std::move(encryptionKey);
const auto newKeyId = _sessionState.encryptionKey ? _sessionState.encryptionKey->keyId() : 0;
if (_sessionState.keyId) {
if (_sessionState.keyId == newKeyId) {
return;
}
setCurrentKeyId(0);
DEBUG_LOG(("MTP Info: auth_key id for dc %1 changed, restarting..."
).arg(_shiftedDcId));
if (_transport.connection()) {
restart();
}
return;
}
if (!_transport.connection()) {
return;
}
setCurrentKeyId(newKeyId);
DEBUG_LOG(("AuthKey Info: Connection update key from Session, "
"dc %1 result: %2"
).arg(_shiftedDcId
).arg(Logs::mb(&_sessionState.keyId, sizeof(_sessionState.keyId)).str()));
if (_sessionState.keyId) {
return authKeyChecked();
}
if (_delegate->isKeysDestroyer()) {
// We are here to destroy an old key, so we're done.
LOG(("MTP Error: No key %1 in updateAuthKey() for destroying."
).arg(_shiftedDcId));
_delegate->keyWasPossiblyDestroyed(_shiftedDcId);
} else if (noMediaKeyWithExistingRegularKey()) {
DEBUG_LOG(("AuthKey Info: No key in updateAuthKey() for media, "
"but someone has created regular, trying to acquire."));
const auto dcType = tryAcquireKeyCreation();
if (_authState.keyCreator && dcType != _currentDcType) {
DEBUG_LOG(("AuthKey Info: "
"Dc type changed for creation, restarting."));
restart();
return;
}
}
if (_authState.keyCreator) {
DEBUG_LOG(("AuthKey Info: No key in updateAuthKey(), creating."));
logMtprotoEvent(
ProxyDiagnosticsPhase::MtpKeyCreating,
ProxyDiagnosticsSeverity::Info,
u"no auth key, starting key creation"_q);
_authState.keyCreator->start(
BareDcId(_shiftedDcId),
getProtocolDcId(),
not_null<AbstractConnection*>{ _transport.connection() },
&_delegate->dcOptions());
} else {
DEBUG_LOG(("AuthKey Info: No key in updateAuthKey(), "
"but someone is creating already, waiting."));
logMtprotoEvent(
ProxyDiagnosticsPhase::MtpKeyCreating,
ProxyDiagnosticsSeverity::Info,
u"no auth key, waiting for creation by another session"_q);
}
}
bool SessionPrivate::noMediaKeyWithExistingRegularKey() const {
return (TemporaryKeyTypeByDcType(_currentDcType)
== TemporaryKeyType::MediaCluster)
&& _sessionState.data->getTemporaryKey(TemporaryKeyType::Regular);
}
void SessionPrivate::dropMismatchedTemporaryKey() {
// A media session creates the regular temporary key itself when it is
// the first session of its DC (see Dcenter::acquireKeyCreation), and it
// kept that key after the next connect switched it to the media
// cluster. Through MTProxy and WEB proxies the DC id in the transport
// header picks the backend, -N is the media cluster, and it does not
// know the regular key: every packet came back as -404, the regular key
// of the whole DC was destroyed, recreated by the same session through
// the regular DC and the loop started over. Give the media session the
// key of its own cluster (created on this connection if missing).
const auto proxy = _sessionState.options
? _sessionState.options->proxy.type
: ProxyData::Type::None;
// WSS is the same: the kwsN-1 relay and the Cloudflare tunnel (with -N
// in the obfuscated header) both land on the media cluster. Desktop log
// 25.09: DC1 media through the tunnel got -404 on every connection and
// spent the ~16 KB each tunnel connection gets on creating keys again.
const auto wss = (proxy == ProxyData::Type::None)
&& _sessionState.options
&& (_sessionState.options->stealth.transport
== ProxyTransport::Wss);
if (!_sessionState.encryptionKey
|| _delegate->isKeysDestroyer()
|| (proxy != ProxyData::Type::Mtproto
&& proxy != ProxyData::Type::Web
&& !wss)
|| (TemporaryKeyTypeByDcType(_currentDcType)
!= TemporaryKeyType::MediaCluster)) {
return;
}
const auto regular = _sessionState.data->getTemporaryKey(
TemporaryKeyType::Regular);
if (!regular || regular != _sessionState.encryptionKey) {
return;
}
logMtprotoEvent(
ProxyDiagnosticsPhase::MtpKeyCreating,
ProxyDiagnosticsSeverity::Info,
u"media session left the regular temporary key (id %1) "
"for the media cluster key"_q.arg(regular->keyId()));
applyAuthKey(nullptr);
}
void SessionPrivate::dropForgottenTemporaryKey() {
// A session keeps its own pointer to the temporary key of its DC, and
// updateAuthKey() skips a session that is reconnecting. After one
// session found the key destroyed, the others connected with it again
// and each had to be refused on its own: 20-30 s of file sessions
// reconnecting after every switch between a WEB proxy and WSS (desktop
// log 01.10, 17:23 and 17:47, one key destroyed by four sessions in
// turn). A key the DC no longer holds is left before it is used; a key
// this session is still binding is not in the DC yet and stays.
const auto &key = _sessionState.encryptionKey;
if (!key || _authState.keyCreator || _delegate->isKeysDestroyer()) {
return;
}
const auto &data = _sessionState.data;
if (key == data->getTemporaryKey(TemporaryKeyType::Regular)
|| key == data->getTemporaryKey(TemporaryKeyType::MediaCluster)) {
return;
}
logMtprotoEvent(
ProxyDiagnosticsPhase::MtpKeyDestroyed,
ProxyDiagnosticsSeverity::Info,
u"temporary key (id %1) is no longer the key of its dc, left"_q
.arg(key->keyId()));
_sessionState.encryptionKey = nullptr;
setCurrentKeyId(0);
}
bool SessionPrivate::destroyOldEnoughPersistentKey() {
Expects(_authState.keyCreator != nullptr);
const auto key = _authState.keyCreator->bindPersistentKey();
Assert(key != nullptr);
const auto created = key->creationTime();
if (created > 0 && crl::now() - created < kKeyOldEnoughForDestroy) {
return false;
}
const auto instance = _instance;
const auto delegate = _delegate;
const auto shiftedDcId = _shiftedDcId;
const auto keyId = key->keyId();
InvokeQueued(instance, [=] {
delegate->keyDestroyedOnServer(shiftedDcId, keyId);
});
return true;
}
DcType SessionPrivate::tryAcquireKeyCreation() {
if (_authState.keyCreator) {
return _currentDcType;
} else if (_delegate->isKeysDestroyer()) {
return _realDcType;
}
const auto acquired = _sessionState.data->acquireKeyCreation(_realDcType);
if (acquired == CreatingKeyType::None) {
return _realDcType;
}
using Result = DcKeyResult;
using Error = DcKeyError;
auto delegate = SessionAuthKeyDelegate();
delegate.unboundReady = [=](base::expected<Result, Error> result) {
if (!result) {
releaseKeyCreationOnFail();
if (result.error() == Error::UnknownPublicKey) {
if (_realDcType == DcType::Cdn) {
LOG(("Warning: CDN public RSA key not found"));
_transport.requestCDNConfig();
return;
}
LOG(("AuthKey Error: could not choose public RSA key"));
}
restart();
return;
}
DEBUG_LOG(("AuthKey Info: unbound key creation succeed, "
"ids: (%1, %2) server salts: (%3, %4)"
).arg(result->temporaryKey
? result->temporaryKey->keyId()
: 0
).arg(result->persistentKey
? result->persistentKey->keyId()
: 0
).arg(result->temporaryServerSalt
).arg(result->persistentServerSalt));
_sessionState.sessionSalt = result->temporaryServerSalt;
result->temporaryKey->setExpiresAt(base::unixtime::now()
+ kTemporaryExpiresIn
+ kBindKeyAdditionalExpiresTimeout);
if (_realDcType != DcType::Cdn) {
auto key = result->persistentKey
? std::move(result->persistentKey)
: _sessionState.data->getPersistentKey();
if (!key) {
releaseKeyCreationOnFail();
restart();
return;
}
_authState.keyCreator->bind(std::move(key));
}
applyAuthKey(std::move(result->temporaryKey));
if (_realDcType == DcType::Cdn) {
_authState.keyCreator = nullptr;
if (!_sessionState.data->releaseCdnKeyCreationOnDone(_sessionState.encryptionKey)) {
restart();
} else {
_sessionState.data->queueNeedToResumeAndSend();
}
}
};
delegate.sentSome = [=](uint64 size) {
onSentSome(size);
};
delegate.receivedSome = [=] {
onReceivedSome();
};
auto request = DcKeyRequest();
request.persistentNeeded = (acquired == CreatingKeyType::Persistent);
request.temporaryExpiresIn = kTemporaryExpiresIn;
_authState.keyCreator = _authKeyFactory->create(
request,
std::move(delegate));
const auto forceUseRegular = (_realDcType == DcType::MediaCluster)
&& (acquired != CreatingKeyType::TemporaryMediaCluster);
return forceUseRegular ? DcType::Regular : _realDcType;
}
void SessionPrivate::authKeyChecked() {
connect(_transport.connection(), &AbstractConnection::receivedData, [=] {
handleReceived();
});
logMtprotoEvent(
ProxyDiagnosticsPhase::MtpKeyReady,
ProxyDiagnosticsSeverity::Info,
u"auth key ready (id %1), %2"_q
.arg(_sessionState.keyId)
.arg(_sessionState.sessionSalt
? u"session connected"_q
: u"requesting server salt"_q));
if (_sessionState.sessionSalt && setState(ConnectedState)) {
_messageHandler.resendAll();
} // else receive salt in bad_server_salt first, then try to send all the requests
_requestState.pingIdToSend = base::RandomValue<uint64>(); // get server_salt
_sessionState.data->queueNeedToResumeAndSend();
}
void SessionPrivate::destroyTemporaryKey() {
if (_delegate->isKeysDestroyer()) {
LOG(("MTP Info: -404 error received in destroyer %1, assuming key was destroyed.").arg(_shiftedDcId));
logMtprotoEvent(
ProxyDiagnosticsPhase::MtpKeyDestroyed,
ProxyDiagnosticsSeverity::Info,
u"key destroyer confirmed key gone"_q);
_delegate->keyWasPossiblyDestroyed(_shiftedDcId);
return;
}
LOG(("MTP Info: -404 error received in %1 with temporary key, assuming it was destroyed.").arg(_shiftedDcId));
logMtprotoEvent(
ProxyDiagnosticsPhase::MtpKeyDestroyed,
ProxyDiagnosticsSeverity::Warning,
u"temporary key (id %1) assumed destroyed by server, recreating"_q
.arg(_sessionState.keyId));
releaseKeyCreationOnFail();
if (_sessionState.encryptionKey) {
_sessionState.data->destroyTemporaryKey(_sessionState.encryptionKey->keyId());
}
applyAuthKey(nullptr);
restart();
}
void SessionPrivate::clearUnboundKeyCreator() {
if (_authState.keyCreator) {
_authState.keyCreator->stop();
}
}
void SessionPrivate::releaseKeyCreationOnFail() {
if (!_authState.keyCreator) {
return;
}
_authState.keyCreator = nullptr;
_sessionState.data->releaseKeyCreationOnFail();
}
} // namespace details
} // namespace MTP