ZaStoGram/TMessagesProj/jni/tgnet/wss/WssPool.cpp
loop-uh 4669d9cc02
Some checks failed
ZaStoGram source guards / guards (push) Failing after 45s
Build three ZaStoGram APKs / build (armeabi-v7a, ZaStoGram-standalone-armeabi-v7a, Armv7, armv7) (push) Failing after 3m17s
Build three ZaStoGram APKs / build (arm64-v8a, ZaStoGram-standalone-arm64-v8a, Arm64, arm64) (push) Failing after 3m19s
Build three ZaStoGram APKs / build (x86, ZaStoGram-standalone-x86, X86, x86) (push) Failing after 3m20s
Держать WSS на релее и открывать соединения из пула
Короткая пачка TCP-таймаутов на старте отключала kws домашнего DC на
10 минут и уводила его в туннель Cloudflare, где на мобильной сети
TCP замерзает после ~16 КБ. Теперь таймаут при недавнем успешном TCP
к тому же адресу считается потерей потока, провалы в пределах 2 с
считаются одним, подавление длится 2 минуты, а включение WSS на
старте не рвёт соединения, если ни одно ещё не открыто.

Пул держит по одному готовому (TCP+TLS+upgrade) сокету на маршрут
kwsN, который соединения недавно запрашивали; неудачи его пробных
сокетов не влияют на выбор маршрута.
2026-09-24 19:39:26 +03:00

298 lines
10 KiB
C++

/*
* This is the source code of tgnet library v. 1.1
* It is licensed under GNU GPL v. 2 or later.
*/
#include "WssPool.h"
#include "../EventObject.h"
#include "../FileLog.h"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <algorithm>
#include <cstring>
namespace tgnet {
namespace wss {
namespace {
constexpr size_t kSparePerRoute = 1;
// Keep warming a route only while connections keep asking for it.
constexpr int64_t kDemandTtlMs = 2 * 60 * 1000;
// The first spare waits: at startup every account opens its connections at
// once, and adding more sockets to that burst is what loses SYNs.
constexpr int64_t kFirstOpenDelayMs = 3000;
// kws relays close a socket that stays silent after the upgrade: measured
// 2026-09-24, 90 s idle still answered, 110 s was already closed.
constexpr int64_t kMaxIdleMs = 70 * 1000;
// Same limits as ConnectionSocket uses for its own handshakes.
constexpr int64_t kTcpConnectTimeoutMs = 2500;
constexpr int64_t kHandshakeTimeoutMs = 8000;
constexpr int64_t kMinBackoffMs = 2000;
constexpr int64_t kMaxBackoffMs = 60 * 1000;
std::string keyFor(const Route &route) {
return route.connectHost + ":" + std::to_string(route.relayPort) + "|" + route.domain + route.path;
}
bool poolable(const Route &route) {
struct in_addr parsed;
// The tunnel carries a per-DC destination and freezes on throttled
// networks anyway; a DNS-named relay would need a resolve on this thread.
return !route.tunnel
&& !route.viaFallback
&& inet_pton(AF_INET, route.connectHost.c_str(), &parsed) == 1;
}
} // namespace
struct Pool::Entry {
Pool *pool = nullptr;
std::string key;
std::unique_ptr<Socket> socket;
EventObject *eventObject = nullptr;
int64_t openedAt = 0;
int64_t readyAt = 0;
bool registered = false;
bool writeInterest = false;
};
void DispatchPoolEvent(void *entry, uint32_t events) {
auto poolEntry = static_cast<Pool::Entry *>(entry);
poolEntry->pool->onEntryEvent(poolEntry, events);
}
Pool::Pool() = default;
Pool::~Pool() {
clear("destroyed");
collectGarbage();
}
void Pool::attach(int fd, std::function<int64_t()> monotonicClock) {
epollFd = fd;
clock = std::move(monotonicClock);
}
size_t Pool::countFor(const std::string &key) const {
return static_cast<size_t>(std::count_if(entries.begin(), entries.end(), [&key](const std::unique_ptr<Entry> &entry) {
return entry->key == key;
}));
}
std::unique_ptr<Socket> Pool::take(const Route &route, int64_t now) {
if (!poolable(route)) {
return nullptr;
}
const std::string key = keyFor(route);
auto demandIt = demands.find(key);
if (demandIt == demands.end()) {
Demand demand;
demand.route = route;
demand.nextOpenAt = now + kFirstOpenDelayMs;
demandIt = demands.emplace(key, std::move(demand)).first;
}
demandIt->second.lastWanted = now;
for (auto &entry : entries) {
if (entry->key != key || entry->readyAt == 0 || !entry->socket->isReady()) {
continue;
}
if (entry->registered) {
epoll_ctl(epollFd, EPOLL_CTL_DEL, entry->socket->fd(), nullptr);
entry->registered = false;
}
std::unique_ptr<Socket> socket = std::move(entry->socket);
socket->setSpeculative(false);
if (LOGS_ENABLED) {
DEBUG_D("wss_pool hit domain=%s relay=%s idle_ms=%lld",
route.domain.c_str(), route.connectHost.c_str(), (long long) (now - entry->readyAt));
}
retire(entry.get(), false, nullptr);
return socket;
}
return nullptr;
}
void Pool::tick(int64_t now, bool allowed) {
if (!allowed) {
if (!entries.empty()) {
clear("not_allowed");
}
return;
}
std::vector<Entry *> expired;
for (auto &entry : entries) {
if (entry->readyAt == 0) {
const bool tcpPending = entry->socket->handshakePhase() == transport::HandshakePhase::None;
if (now - entry->openedAt > (tcpPending ? kTcpConnectTimeoutMs : kHandshakeTimeoutMs)) {
expired.push_back(entry.get());
}
} else if (now - entry->readyAt > kMaxIdleMs) {
expired.push_back(entry.get());
}
}
for (Entry *entry : expired) {
if (entry->readyAt == 0) {
entry->socket->timedOut();
retire(entry, true, "handshake_timeout");
} else {
retire(entry, false, "idle_expired");
}
}
for (auto it = demands.begin(); it != demands.end();) {
Demand &demand = it->second;
if (now - demand.lastWanted > kDemandTtlMs) {
it = demands.erase(it);
continue;
}
// A route that is suppressed or switched to its DNS name will not be
// asked for in this form, so a spare for it would just idle out.
if (now >= demand.nextOpenAt && countFor(it->first) < kSparePerRoute && RouteUsable(demand.route)) {
open(it->first, demand, now);
}
++it;
}
}
void Pool::open(const std::string &key, Demand &demand, int64_t now) {
if (epollFd < 0) {
return;
}
struct sockaddr_in address;
memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(demand.route.relayPort);
if (inet_pton(AF_INET, demand.route.connectHost.c_str(), &address.sin_addr) != 1) {
return;
}
auto entry = std::make_unique<Entry>();
entry->pool = this;
entry->key = key;
entry->openedAt = now;
entry->socket = std::make_unique<Socket>(demand.route);
entry->socket->setSpeculative(true);
std::string diagnostic;
if (!entry->socket->open(reinterpret_cast<const struct sockaddr *>(&address), sizeof(address), &diagnostic)) {
demand.backoffMs = std::min(kMaxBackoffMs, std::max(kMinBackoffMs, demand.backoffMs * 2));
demand.nextOpenAt = now + demand.backoffMs;
if (LOGS_ENABLED) {
DEBUG_D("wss_pool open_failed domain=%s diagnostic=%s", demand.route.domain.c_str(), diagnostic.c_str());
}
return;
}
entry->eventObject = new EventObject(entry.get(), EventObjectTypeWssPool);
struct epoll_event event;
memset(&event, 0, sizeof(event));
// Level-triggered like ConnectionSocket: OpenSSL flips between wanting to
// read and to write during the handshake.
event.events = EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLERR;
event.data.ptr = entry->eventObject;
if (epoll_ctl(epollFd, EPOLL_CTL_ADD, entry->socket->fd(), &event) != 0) {
entry->socket->close();
delete entry->eventObject;
demand.nextOpenAt = now + kMinBackoffMs;
return;
}
entry->registered = true;
entry->writeInterest = true;
// Until this spare is ready or gone, do not open another for the route.
demand.nextOpenAt = now + kMaxBackoffMs;
if (LOGS_ENABLED) {
DEBUG_D("wss_pool open domain=%s relay=%s", demand.route.domain.c_str(), demand.route.connectHost.c_str());
}
entries.push_back(std::move(entry));
}
void Pool::onEntryEvent(Entry *entry, uint32_t events) {
std::vector<std::vector<uint8_t>> payloads;
std::string diagnostic;
const bool alive = entry->socket->onEvent(events, payloads, &diagnostic);
if (!alive || !payloads.empty()) {
// Before the init frame the relay has nothing to say; data or a close
// here means the socket is no longer a clean spare. Back off either
// way, or a relay that drops idle sockets would be redialled in a loop.
retire(entry, true, !alive ? (diagnostic.empty() ? "closed" : diagnostic.c_str()) : "unexpected_data");
return;
}
if (entry->readyAt == 0 && entry->socket->isReady()) {
entry->readyAt = clock();
auto demandIt = demands.find(entry->key);
if (demandIt != demands.end()) {
demandIt->second.backoffMs = 0;
demandIt->second.nextOpenAt = entry->readyAt;
}
if (LOGS_ENABLED) {
DEBUG_D("wss_pool ready domain=%s handshake_ms=%lld",
entry->socket->route().domain.c_str(), (long long) (entry->readyAt - entry->openedAt));
}
}
updateInterest(entry);
}
void Pool::updateInterest(Entry *entry) {
const bool wantWrite = entry->socket->wantsWrite();
if (!entry->registered || wantWrite == entry->writeInterest) {
return;
}
struct epoll_event event;
memset(&event, 0, sizeof(event));
event.events = EPOLLIN | EPOLLRDHUP | EPOLLERR | (wantWrite ? EPOLLOUT : 0);
event.data.ptr = entry->eventObject;
if (epoll_ctl(epollFd, EPOLL_CTL_MOD, entry->socket->fd(), &event) == 0) {
entry->writeInterest = wantWrite;
}
}
void Pool::retire(Entry *entry, bool backoff, const char *reason) {
auto demandIt = demands.find(entry->key);
if (entry->socket != nullptr) {
if (entry->registered) {
epoll_ctl(epollFd, EPOLL_CTL_DEL, entry->socket->fd(), nullptr);
}
entry->socket->close();
if (LOGS_ENABLED && reason != nullptr) {
DEBUG_D("wss_pool drop domain=%s reason=%s", entry->socket->route().domain.c_str(), reason);
}
}
if (demandIt != demands.end()) {
Demand &demand = demandIt->second;
const int64_t base = entry->readyAt != 0 ? entry->readyAt : entry->openedAt;
if (backoff) {
demand.backoffMs = std::min(kMaxBackoffMs, std::max(kMinBackoffMs, demand.backoffMs * 2));
demand.nextOpenAt = base + demand.backoffMs;
} else {
demand.nextOpenAt = 0;
}
}
if (entry->eventObject != nullptr) {
// An event for this socket may still sit later in the current epoll
// batch; the object stays valid and inert until collectGarbage().
entry->eventObject->eventObject = nullptr;
graveyard.push_back(entry->eventObject);
entry->eventObject = nullptr;
}
entries.erase(std::remove_if(entries.begin(), entries.end(), [entry](const std::unique_ptr<Entry> &candidate) {
return candidate.get() == entry;
}), entries.end());
}
void Pool::clear(const char *reason) {
while (!entries.empty()) {
retire(entries.back().get(), false, reason);
}
if (reason != nullptr && strcmp(reason, "network_changed") == 0) {
demands.clear();
}
}
void Pool::collectGarbage() {
for (EventObject *eventObject : graveyard) {
delete eventObject;
}
graveyard.clear();
}
} // namespace wss
} // namespace tgnet