From d816b5edccfb66d992ac35c250cb6f1b3d95625d Mon Sep 17 00:00:00 2001 From: Ivan Razin Date: Fri, 3 Jul 2026 15:05:29 +0300 Subject: [PATCH] Gate diagnostics behind 3x-ui panel login via nginx SSO bridge The 3x-ui session cookie is Path-scoped to the panel base path, so the diag vhost locations can never see it directly. Instead a bridge location under the panel path (//diag) validates the session with auth_request against GET /panel/ (sent with X-Requested-With: XMLHttpRequest so 3x-ui answers 401 instead of a login redirect), then issues a path-scoped diag_key cookie and redirects to the diagnostics page. All diag locations return 404 without that cookie. auth_request runs in the access phase while "return" runs in the rewrite phase, so the success path hops through try_files to a named location; the cookie is set only there, never on the 401 redirect. Replaces the previous ?key= token link, which is removed entirely. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 8 +++++++- x-ui-latest.sh | 47 ++++++++++++++++++++++++++++++++++++++++++-- x-ui-patch.sh | 53 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7197d45..8555def 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,7 +37,13 @@ effect on servers only after push to `main`. Clash/Mihomo user agents get generated clash.yaml, `?provider=1` bypasses it 10. Downloads a random fake cover site (`install_fake_site`) → `/var/www/html/` 11. Installs network diagnostics (`install_diagnostics`) → `/var/www/diagnostics/` + - `mtr-backend` systemd service (hardened, dedicated user, localhost-only) + `mtr-backend` systemd service (hardened, dedicated user, localhost-only). + Access only via `//diag` (SSO bridge): nginx auth_request validates + the 3x-ui session against `GET /panel/` (with X-Requested-With header → + 401 instead of login redirect), then issues a path-scoped `diag_key` cookie and + redirects to the diag page; all diag locations 404 without that cookie. + The 3x-ui session cookie is Path-scoped to the panel base path, which is why + the bridge must live under the panel path 12. Tunes kernel/BBR (`tune_system`) 13. Sets up cron (`setup_cron`) — daily x-ui restart + nginx reload; monthly certbot renew with pre/post hooks stopping/starting nginx (certs are standalone-issued) diff --git a/x-ui-latest.sh b/x-ui-latest.sh index 81643a8..6888cd7 100644 --- a/x-ui-latest.sh +++ b/x-ui-latest.sh @@ -115,6 +115,7 @@ xhttp_path=$(gen_random_string 10) config_username=$(gen_random_string 10) config_password=$(gen_random_string 10) diag_path="/net-$(gen_random_string 12)/" +diag_token=$(gen_random_string 16) mtr_backend_port=$(make_port) # ─── Argument parsing ──────────────────────────────────────────────────────── @@ -440,6 +441,12 @@ map "\$is_clash_ua:\$arg_provider" \$serve_clash_yaml { default 0; } +# Diagnostics access: cookie issued by the SSO bridge after panel login +map \$cookie_diag_key \$diag_auth { + "${diag_token}" 1; + default 0; +} + server { server_tokens off; server_name ${domain}; @@ -490,19 +497,51 @@ server { proxy_pass https://127.0.0.1:${panel_port}; } + # ── Diagnostics SSO bridge ─────────────────────────────────────────────── + # Lives under the panel path so the browser attaches the 3x-ui session + # cookie (its Path is scoped to the panel base path). Valid panel session + # → issue the diag cookie and redirect; otherwise → panel login page. + # NOTE: auth_request runs in the access phase; a plain "return" here would + # skip it (rewrite phase), hence the try_files → named-location hop. + location = /${panel_path}/diag { + auth_request /__diag_auth; + error_page 401 403 =302 /${panel_path}/; + try_files /__nonexistent @diag_sso_ok; + } + location @diag_sso_ok { + add_header Set-Cookie "diag_key=${diag_token}; Path=${diag_path}; Secure; HttpOnly; SameSite=Lax; Max-Age=604800"; + return 302 ${diag_path}; + } + location = /__diag_auth { + internal; + proxy_pass https://127.0.0.1:${panel_port}/${panel_path}/panel/; + proxy_http_version 1.1; + proxy_set_header Host \$host; + # 3x-ui answers AJAX requests with 401 instead of a login redirect + proxy_set_header X-Requested-With XMLHttpRequest; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_intercept_errors off; + } + # ── Network diagnostics page ───────────────────────────────────────────── + # Requires the diag cookie (issued by the SSO bridge above); re-setting it + # here extends the expiry on every visit location ^~ ${diag_path} { + if (\$diag_auth = 0) { return 404; } limit_req zone=diag_page burst=10 nodelay; limit_conn per_ip 5; alias /var/www/diagnostics/; index index.html; try_files \$uri \$uri/ /index.html; + add_header Set-Cookie "diag_key=${diag_token}; Path=${diag_path}; Secure; HttpOnly; SameSite=Lax; Max-Age=604800" always; add_header Cache-Control "no-store" always; add_header X-Robots-Tag "noindex, nofollow" always; } # ── Diagnostics MTR API ────────────────────────────────────────────────── location ^~ ${diag_path}api/mtr { + if (\$diag_auth = 0) { return 404; } limit_req zone=diag_api burst=2 nodelay; limit_conn per_ip 2; proxy_pass http://127.0.0.1:${mtr_backend_port}/api/mtr; @@ -517,6 +556,7 @@ server { # No limit_req: librespeed fires many short POSTs (parallel streams). # proxy_request_buffering off = client sees true network backpressure. location ^~ ${diag_path}api/st/up { + if (\$diag_auth = 0) { return 404; } access_log off; limit_conn per_ip 8; proxy_pass http://127.0.0.1:${mtr_backend_port}/api/st/up; @@ -531,6 +571,7 @@ server { # ── LibreSpeed ping endpoint (answered by nginx, no backend hop) ───────── location = ${diag_path}api/st/ping { + if (\$diag_auth = 0) { return 404; } access_log off; limit_conn per_ip 8; add_header Cache-Control "no-store" always; @@ -540,6 +581,7 @@ server { # ── LibreSpeed client IP ───────────────────────────────────────────────── location = ${diag_path}api/st/getip { + if (\$diag_auth = 0) { return 404; } proxy_pass http://127.0.0.1:${mtr_backend_port}/api/st/getip; proxy_http_version 1.1; proxy_set_header X-Real-IP \$remote_addr; @@ -548,6 +590,7 @@ server { # ── Download test files ────────────────────────────────────────────────── location ^~ ${diag_path}testfiles/ { + if (\$diag_auth = 0) { return 404; } alias /var/www/diagnostics/testfiles/; access_log off; add_header Cache-Control "no-store, no-cache, must-revalidate" always; @@ -1047,7 +1090,7 @@ EOF systemctl enable mtr-backend systemctl restart mtr-backend - msg_ok "Network diagnostics installed at https://${domain}${diag_path}" + msg_ok "Network diagnostics installed at https://${domain}/${panel_path}/diag (panel login required)" } # ───────────────────────────────────────────────────────────────────────────── @@ -1107,7 +1150,7 @@ show_results() { echo -e "Username: ${config_username}\n" echo -e "Password: ${config_password}\n" msg_inf "────────────────────────────────────────────────────────────────────────────────" - msg_inf "Network Diagnostics: https://${domain}${diag_path}\n" + msg_inf "Network Diagnostics (panel login required): https://${domain}/${panel_path}/diag\n" msg_inf "────────────────────────────────────────────────────────────────────────────────" msg_inf "Please save this screen!" else diff --git a/x-ui-patch.sh b/x-ui-patch.sh index 7270253..f9e19ee 100644 --- a/x-ui-patch.sh +++ b/x-ui-patch.sh @@ -107,6 +107,20 @@ else blue "diag_path generated: $diag_path" fi +# ── detect or generate diag access token ───────────────────────────────────── +diag_token="" +for f in /etc/nginx/sites-available/*; do + [[ -f "$f" ]] || continue + t=$(grep -oP 'diag_key=\K[a-zA-Z0-9]+' "$f" 2>/dev/null | head -1 || true) + [[ -n "$t" ]] && { diag_token="$t"; break; } +done +if [[ -n "$diag_token" ]]; then + blue "diag_token reused" +else + diag_token=$(tr -dc 'a-zA-Z0-9'