diff --git a/README.md b/README.md index c715bc0..f50112d 100644 --- a/README.md +++ b/README.md @@ -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-запросом с диапазоном. diff --git a/src/index.ts b/src/index.ts index edebad6..6517f39 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 { const ytdlpVersion = await checkYtDlp(); @@ -23,7 +23,11 @@ async function main(): Promise { } 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") { diff --git a/src/sources/index.ts b/src/sources/index.ts index 81eab7b..0d2fb2a 100644 --- a/src/sources/index.ts +++ b/src/sources/index.ts @@ -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"]; diff --git a/src/sources/ytdlp.ts b/src/sources/ytdlp.ts index fb8f480..fce2a7d 100644 --- a/src/sources/ytdlp.ts +++ b/src/sources/ytdlp.ts @@ -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 { + 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"; /**