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