Check the proxy at startup instead of failing on first use

A proxy pointed at 127.0.0.1 from inside a container reaches the
container itself, and the only sign was a connection error buried in the
first search. Startup now probes the proxy over TCP and says what is
wrong, naming host.docker.internal when loopback was configured.

The README also covers the follow-up trap: even that address fails when
the proxy listens on loopback only, so it shows how to check the bind
address and what to change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-09-09 00:29:36 +03:00
co-authored by Claude Opus 5
parent ea19a74c12
commit 380f7bcf31
4 changed files with 57 additions and 5 deletions
+15 -2
View File
@@ -242,8 +242,21 @@ YTDLP_PROXY=socks5h://127.0.0.1:1080
Два подводных камня:
- **`127.0.0.1` внутри контейнера — это сам контейнер.** Если Psiphon или ваш SOCKS запущен на
хосте, укажите `socks5h://host.docker.internal:1080` и добавьте в `compose.yml`
к `extra_hosts` строку `"host.docker.internal:host-gateway"`.
хосте, укажите `socks5h://host.docker.internal:1080` (эта запись в `extra_hosts` уже есть).
Бот проверяет прокси при старте и пишет в лог понятную причину, если тот недоступен.
- **Прокси должен слушать интерфейс, видимый контейнеру.** Даже с `host.docker.internal`
соединение упрётся в `ECONNREFUSED`, если клиент забинден только на loopback. Проверить:
```bash
ss -lntp | grep 1080
```
Если там `127.0.0.1:1080`, разрешите прослушивание всех интерфейсов (в конфиге
psiphon-tunnel-core за это отвечает `ListenInterface`) и закройте порт снаружи файрволом.
Альтернатива без правки прокси — добавить сервису `network_mode: host`, тогда `127.0.0.1:1080`
работает как есть; но при этом перестают действовать `ports` и `extra_hosts`, то есть
`STOAT_DOMAIN` снова начнёт резолвиться публично, а панель нужно будет привязать к
`HOST=127.0.0.1`.
- **ffmpeg не умеет SOCKS.** Поэтому при заданном прокси аудио всегда идёт через yt-dlp, а не
напрямую из CDN — трафик не утекает мимо прокси. Побочный эффект: перемотка становится
медленнее, так как позиция отыгрывается декодированием, а не HTTP-запросом с диапазоном.
+6 -2
View File
@@ -3,7 +3,7 @@ import { startBot } from "./bot/index.js";
import { config } from "./config.js";
import { MusicManager } from "./core/manager.js";
import { logger } from "./logger.js";
import { checkCookies, checkYtDlp, describeProxy } from "./sources/index.js";
import { checkCookies, checkProxy, checkYtDlp, describeProxy } from "./sources/index.js";
async function main(): Promise<void> {
const ytdlpVersion = await checkYtDlp();
@@ -23,7 +23,11 @@ async function main(): Promise<void> {
}
const proxy = describeProxy();
if (proxy) logger.info({ proxy }, "routing yt-dlp through a proxy");
if (proxy) {
const problem = await checkProxy();
if (problem) logger.error({ proxy }, `proxy is configured but unreachable: ${problem}`);
else logger.info({ proxy }, "routing yt-dlp through a proxy");
}
const cookies = await checkCookies();
if (cookies === "ok") {
+1 -1
View File
@@ -5,7 +5,7 @@ import * as direct from "./direct.js";
import * as local from "./local.js";
import * as ytdlp from "./ytdlp.js";
export { checkAvailable as checkYtDlp, checkCookies, describeProxy } from "./ytdlp.js";
export { checkAvailable as checkYtDlp, checkCookies, checkProxy, describeProxy } from "./ytdlp.js";
export { isEnabled as isLocalLibraryEnabled, listFiles as listLocalFiles } from "./local.js";
const YOUTUBE_HOSTS = ["youtube.com", "youtu.be", "music.youtube.com", "m.youtube.com"];
+35
View File
@@ -1,4 +1,5 @@
import { spawn } from "node:child_process";
import { connect } from "node:net";
import { randomUUID } from "node:crypto";
import { constants } from "node:fs";
import { access } from "node:fs/promises";
@@ -52,6 +53,40 @@ export function describeProxy(): string | null {
}
}
/**
* A quick TCP probe of the proxy. The usual mistake is pointing a container at
* 127.0.0.1, which is the container itself — better to say so at startup than to
* let every search fail with a connection error.
*/
export function checkProxy(timeoutMs = 4000): Promise<string | null> {
if (!config.YTDLP_PROXY) return Promise.resolve(null);
let target: URL;
try {
target = new URL(config.YTDLP_PROXY);
} catch {
return Promise.resolve("YTDLP_PROXY не является корректным URL");
}
const port = Number(target.port) || (target.protocol === "https:" ? 443 : 1080);
return new Promise((resolve) => {
const socket = connect({ host: target.hostname, port });
const done = (result: string | null) => {
socket.destroy();
resolve(result);
};
socket.setTimeout(timeoutMs);
socket.once("connect", () => done(null));
socket.once("timeout", () => done(`прокси ${target.hostname}:${port} не отвечает`));
socket.once("error", (err: NodeJS.ErrnoException) => {
const hint =
target.hostname === "127.0.0.1" || target.hostname === "localhost"
? " — внутри контейнера это сам контейнер; используйте host.docker.internal"
: "";
done(`прокси ${target.hostname}:${port} недоступен (${err.code ?? err.message})${hint}`);
});
});
}
export type CookieStatus = "ok" | "read-only" | "missing";
/**