From a1c146aff3e7ebc7e936bae92ac352f0ca1a61cc Mon Sep 17 00:00:00 2001 From: ospab Date: Fri, 31 Jul 2026 20:00:29 +0300 Subject: [PATCH] =?UTF-8?q?fix(relay):=20explain=20the=20404=20=E2=80=94?= =?UTF-8?q?=20the=20API=20lives=20under=20the=20panel's=20secret=20webpath?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A relay configured with upstream_api_url = "http://HOST:9090" fails every key sync with a bare "API returned HTTP 404", which reads like the server is down or the token is wrong. Neither is true: the management API is nested under the target server's api.webpath (create_api_router mounts it at "/{webpath}/api"), because that secret segment is what keeps the panel from being discoverable by scanners. A bare host:port therefore resolves to a route that does not exist and the token is never even looked at. Nothing said so — the config template, the wizard prompt and the shipped example all suggested exactly the host:port form that cannot work. - sync_keys now reports the full URL and, for 404 specifically, states that the webpath must be included and what the URL should look like. 401 is called out separately as a token mismatch, since the two are otherwise indistinguishable from the log. - The relay config template, the shipped example and the wizard prompt now show the path-bearing form, and the wizard warns when the URL entered has no path segment rather than letting it fail later. Docs under docs/ and the wiki are being rewritten concurrently and are left alone here. --- docs/relay-config-example.json | 12 +++++++++--- ostp-server/src/relay_node.rs | 24 +++++++++++++++++++++++- ostp/src/main.rs | 30 +++++++++++++++++++++++++++--- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/docs/relay-config-example.json b/docs/relay-config-example.json index 2523f71..7d63e3b 100644 --- a/docs/relay-config-example.json +++ b/docs/relay-config-example.json @@ -20,9 +20,15 @@ // Адрес следующего узла в цепочке — UDP "upstream_udp": "TARGET_SERVER_IP:50000", - // URL API конечного (целевого) сервера для синхронизации access_keys - // Должен быть доступен с этого relay-сервера (можно через SSH-туннель) - "upstream_api_url": "http://TARGET_SERVER_IP:9090", + // URL API конечного (целевого) сервера для синхронизации access_keys. + // Должен быть доступен с этого relay-сервера (можно через SSH-туннель). + // + // ВАЖНО: URL обязан включать секретный путь панели (api.webpath целевого + // сервера). Management API смонтирован ВНУТРИ этого пути — именно он скрывает + // панель от сканеров, — поэтому голый host:port попадает в несуществующий + // маршрут, и синхронизация падает с 404 ещё до проверки токена. + // Это тот же адрес, по которому вы открываете веб-панель. + "upstream_api_url": "http://TARGET_SERVER_IP:9090/TARGET_SERVER_WEBPATH", // Bearer-токен для доступа к API целевого сервера // Должен совпадать с api.token в конфиге target-сервера diff --git a/ostp-server/src/relay_node.rs b/ostp-server/src/relay_node.rs index 919ab58..2280ef5 100644 --- a/ostp-server/src/relay_node.rs +++ b/ostp-server/src/relay_node.rs @@ -95,7 +95,29 @@ async fn sync_keys(cfg: &RelayConfig, shared_keys: &SharedKeys) -> Result let resp = req.send().await?; if !resp.status().is_success() { - anyhow::bail!("API returned HTTP {}", resp.status()); + // 404 here almost always means the URL is missing the panel's secret + // path segment rather than the server being down or the token being + // wrong. The management API is not served at /api — it is nested under + // the configured `api.webpath` (see create_api_router), which exists to + // keep the panel from being discoverable by scanners. A bare + // host:port therefore resolves to a route that does not exist, and the + // token is never even looked at, which makes "404" a deeply misleading + // thing to report on its own. + if resp.status() == reqwest::StatusCode::NOT_FOUND { + anyhow::bail!( + "API returned HTTP 404 for {url}. The management API is served under the \ + target server's secret `api.webpath`, not at /api — set upstream_api_url \ + to include it, e.g. \"http://HOST:9090/\" (the same path you open \ + the web panel at). Check `api.webpath` in the target server's config." + ); + } + if resp.status() == reqwest::StatusCode::UNAUTHORIZED { + anyhow::bail!( + "API returned HTTP 401 for {url}: upstream_api_token does not match the \ + target server's `api.token`." + ); + } + anyhow::bail!("API returned HTTP {} for {url}", resp.status()); } #[derive(serde::Deserialize)] diff --git a/ostp/src/main.rs b/ostp/src/main.rs index 0abf405..b9089be 100644 --- a/ostp/src/main.rs +++ b/ostp/src/main.rs @@ -799,8 +799,26 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> { let listen = wizard_prompt("Listen address (host:port)", "0.0.0.0:50000"); let upstream = wizard_prompt("Upstream server address (host:port)", ""); if upstream.is_empty() { anyhow::bail!("Upstream address cannot be empty."); } - let api_url = wizard_prompt("Upstream server API URL (e.g. http://1.2.3.4:9090)", ""); - let api_token = wizard_prompt("Upstream API token (leave blank if none)", ""); + let api_url = wizard_prompt( + "Upstream API URL, including the panel's secret path (e.g. http://1.2.3.4:9090/bNAzr8Ss)", + "", + ); + // The management API lives under the target server's api.webpath, so + // a bare host:port 404s on every key sync without ever checking the + // token. Catch that here instead of leaving it to be debugged from + // relay logs. + let has_path = api_url + .split("://") + .nth(1) + .map(|rest| rest.contains('/') && !rest.trim_end_matches('/').split('/').nth(1).unwrap_or("").is_empty()) + .unwrap_or(false); + if !api_url.is_empty() && !has_path { + wizard_warn( + "This URL has no path segment. The API is served under the target server's \ + api.webpath - key sync will fail with 404 unless you append it.", + ); + } + let api_token = wizard_prompt("Upstream API token (must equal api.token on the target server)", ""); wizard_step(2, TOTAL, "Saving configuration"); let relay_json = serde_json::json!({ @@ -1213,7 +1231,13 @@ async fn run_app() -> Result<()> { "listen": "0.0.0.0:50000", "upstream_tcp": "TARGET_SERVER_IP:50000", "upstream_udp": "TARGET_SERVER_IP:50000", - "upstream_api_url": "http://TARGET_SERVER_IP:9090", + // MUST include the target server's secret api.webpath. The management API is + // nested under it (that path is what hides the panel from scanners), so a + // bare host:port hits a route that does not exist and key sync fails with 404 + // before the token is ever checked. This is the same URL you open the panel + // at, e.g. "http://1.2.3.4:9090/bNAzr8Ss". + "upstream_api_url": "http://TARGET_SERVER_IP:9090/TARGET_SERVER_WEBPATH", + // Must equal api.token on the target server (NOT the panel password). "upstream_api_token": "YOUR_API_TOKEN_HERE", "sync_interval_secs": 30, "debug": false