mirror of
https://github.com/by-sonic/tglock.git
synced 2026-09-05 18:16:09 +03:00
e9af114f3c
Скрипт воркера писал в сокет Telegram так:
server.addEventListener("message", (event) => {
writer.write(chunk).catch(shutdown);
});
`write()` вызывался поверх незавершённого, `writer.ready` не спрашивался вовсе.
Пока в клиенте голодала отправка, настоящего потока вверх через воркер не
возникало, и код держался. В beta.12 голодание починили — поток появился, и
репортёр #42 сразу получил переподключения на обоих клиентах, которых на
beta.11 с тем же воркером не было.
Запись сериализована цепочкой промисов: следующий чанк уходит после того, как
записан предыдущий, и только когда писатель готов. Кто разворачивал воркер
раньше — нужен передеплой, о чём сказано в docs/CLOUDFLARE_WORKER.md.
Причина у репортёра не подтверждена: рантайма Workers у меня нет, проверить
можно только у него.
Заодно счётчик «промолчали». Соединение, которое открылось и ничего не
прислало за `IO_TIMEOUT`, закрывалось и не попадало ни в один счётчик:
`unknown_clients` растёт, только когда запрос пришёл и не разобрался, а не
когда его не дождались. Тот же репортёр сообщил, что его телефон
переустанавливает соединение примерно раз в десять секунд — ровно период
`IO_TIMEOUT`. Проверить это по диагностике было нечем, теперь есть чем.
Тесты: Ping через туннель (путь не был покрыт вовсе, а Ping бывает только на
маршруте воркера) и молчащий клиент. Второй гоняет виртуальное время, чтобы не
ждать десять секунд по-настоящему, — отсюда dev-зависимость на tokio/test-util.
Co-authored-by: by-sonic <171230345+by-sonic@users.noreply.github.com>
118 lines
4.8 KiB
JavaScript
118 lines
4.8 KiB
JavaScript
// Резервный маршрут TGLock через Cloudflare Worker.
|
|
//
|
|
// Нужен в одном случае: провайдер заблокировал саму веб-инфраструктуру
|
|
// Telegram, и все обычные маршруты TGLock перестали отвечать. Тогда соединение
|
|
// идёт на твой домен *.workers.dev, а воркер доводит его до Telegram.
|
|
//
|
|
// Инструкция по установке: docs/CLOUDFLARE_WORKER.md
|
|
//
|
|
// Контракт, который ожидает клиент (src/transport.rs):
|
|
// wss://<домен>/apiws?dst=<telegram-ip>&dc=<номер-dc>
|
|
// заголовок Sec-WebSocket-Protocol: binary — его обязательно нужно
|
|
// подтвердить в ответе, иначе клиент разорвёт рукопожатие;
|
|
// бинарные frames в обе стороны, без обёрток.
|
|
|
|
import { connect } from "cloudflare:sockets";
|
|
|
|
// Только те адреса, которые запрашивает сам TGLock. Без этого списка любой,
|
|
// кто узнает адрес воркера, получит через твой аккаунт произвольный
|
|
// TCP-прокси.
|
|
const ALLOWED_DESTINATIONS = new Set([
|
|
"91.105.192.100",
|
|
"149.154.167.51",
|
|
"149.154.167.91",
|
|
"149.154.167.220",
|
|
"149.154.171.5",
|
|
"149.154.175.50",
|
|
"149.154.175.100",
|
|
]);
|
|
|
|
const TELEGRAM_PORT = 443;
|
|
|
|
export default {
|
|
async fetch(request, env) {
|
|
const url = new URL(request.url);
|
|
|
|
if (url.pathname !== "/apiws") {
|
|
return new Response("not found", { status: 404 });
|
|
}
|
|
if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket") {
|
|
return new Response("expected a websocket upgrade", { status: 426 });
|
|
}
|
|
// Необязательный общий секрет: задай переменную TGLOCK_TOKEN в настройках
|
|
// воркера, и посторонние подключиться не смогут.
|
|
if (env.TGLOCK_TOKEN && url.searchParams.get("token") !== env.TGLOCK_TOKEN) {
|
|
return new Response("forbidden", { status: 403 });
|
|
}
|
|
|
|
const destination = url.searchParams.get("dst");
|
|
if (!destination || !ALLOWED_DESTINATIONS.has(destination)) {
|
|
return new Response("destination not allowed", { status: 403 });
|
|
}
|
|
|
|
const [client, server] = Object.values(new WebSocketPair());
|
|
server.accept();
|
|
|
|
const upstream = connect({ hostname: destination, port: TELEGRAM_PORT });
|
|
const writer = upstream.writable.getWriter();
|
|
let closed = false;
|
|
|
|
const shutdown = () => {
|
|
if (closed) return;
|
|
closed = true;
|
|
writer.close().catch(() => {});
|
|
try {
|
|
server.close();
|
|
} catch {
|
|
// соединение уже закрыто
|
|
}
|
|
};
|
|
|
|
// Запись сериализуется: следующий чанк уходит только после того, как
|
|
// записан предыдущий, и только когда писатель к этому готов.
|
|
//
|
|
// Раньше `write()` вызывался поверх незавершённого, а `writer.ready` не
|
|
// спрашивался вовсе — backpressure не применялся. Пока в клиенте отправка
|
|
// голодала, поверх воркера настоящего потока вверх не бывало и это не
|
|
// проявлялось. Как только голодание починили, в воркер пошёл настоящий
|
|
// поток (by-sonic/tglock#42).
|
|
let pending = Promise.resolve();
|
|
|
|
server.addEventListener("message", (event) => {
|
|
const chunk =
|
|
event.data instanceof ArrayBuffer
|
|
? new Uint8Array(event.data)
|
|
: event.data;
|
|
pending = pending
|
|
.then(() => writer.ready)
|
|
.then(() => writer.write(chunk))
|
|
.catch(shutdown);
|
|
});
|
|
server.addEventListener("close", shutdown);
|
|
server.addEventListener("error", shutdown);
|
|
|
|
// Обратное направление: всё, что приходит от Telegram, уходит клиенту.
|
|
(async () => {
|
|
const reader = upstream.readable.getReader();
|
|
try {
|
|
for (;;) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
server.send(value);
|
|
}
|
|
} catch {
|
|
// разрыв соединения — обычная ситуация, не ошибка
|
|
}
|
|
shutdown();
|
|
})();
|
|
|
|
return new Response(null, {
|
|
status: 101,
|
|
webSocket: client,
|
|
// Обязательно: клиент запрашивает подпротокол binary и без
|
|
// подтверждения рвёт рукопожатие.
|
|
headers: { "Sec-WebSocket-Protocol": "binary" },
|
|
});
|
|
},
|
|
};
|