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 (/<panel_path>/diag) validates the session with auth_request against GET <basePath>/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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
bd2c47753e
commit
d816b5edcc
@@ -37,7 +37,13 @@ effect on servers only after push to `main`.
|
|||||||
Clash/Mihomo user agents get generated clash.yaml, `?provider=1` bypasses it
|
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/`
|
10. Downloads a random fake cover site (`install_fake_site`) → `/var/www/html/`
|
||||||
11. Installs network diagnostics (`install_diagnostics`) → `/var/www/diagnostics/` +
|
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 `/<panel_path>/diag` (SSO bridge): nginx auth_request validates
|
||||||
|
the 3x-ui session against `GET <basePath>/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`)
|
12. Tunes kernel/BBR (`tune_system`)
|
||||||
13. Sets up cron (`setup_cron`) — daily x-ui restart + nginx reload; monthly certbot renew
|
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)
|
with pre/post hooks stopping/starting nginx (certs are standalone-issued)
|
||||||
|
|||||||
+45
-2
@@ -115,6 +115,7 @@ xhttp_path=$(gen_random_string 10)
|
|||||||
config_username=$(gen_random_string 10)
|
config_username=$(gen_random_string 10)
|
||||||
config_password=$(gen_random_string 10)
|
config_password=$(gen_random_string 10)
|
||||||
diag_path="/net-$(gen_random_string 12)/"
|
diag_path="/net-$(gen_random_string 12)/"
|
||||||
|
diag_token=$(gen_random_string 16)
|
||||||
mtr_backend_port=$(make_port)
|
mtr_backend_port=$(make_port)
|
||||||
|
|
||||||
# ─── Argument parsing ────────────────────────────────────────────────────────
|
# ─── Argument parsing ────────────────────────────────────────────────────────
|
||||||
@@ -440,6 +441,12 @@ map "\$is_clash_ua:\$arg_provider" \$serve_clash_yaml {
|
|||||||
default 0;
|
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 {
|
||||||
server_tokens off;
|
server_tokens off;
|
||||||
server_name ${domain};
|
server_name ${domain};
|
||||||
@@ -490,19 +497,51 @@ server {
|
|||||||
proxy_pass https://127.0.0.1:${panel_port};
|
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 ─────────────────────────────────────────────
|
# ── 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} {
|
location ^~ ${diag_path} {
|
||||||
|
if (\$diag_auth = 0) { return 404; }
|
||||||
limit_req zone=diag_page burst=10 nodelay;
|
limit_req zone=diag_page burst=10 nodelay;
|
||||||
limit_conn per_ip 5;
|
limit_conn per_ip 5;
|
||||||
alias /var/www/diagnostics/;
|
alias /var/www/diagnostics/;
|
||||||
index index.html;
|
index index.html;
|
||||||
try_files \$uri \$uri/ /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 Cache-Control "no-store" always;
|
||||||
add_header X-Robots-Tag "noindex, nofollow" always;
|
add_header X-Robots-Tag "noindex, nofollow" always;
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Diagnostics MTR API ──────────────────────────────────────────────────
|
# ── Diagnostics MTR API ──────────────────────────────────────────────────
|
||||||
location ^~ ${diag_path}api/mtr {
|
location ^~ ${diag_path}api/mtr {
|
||||||
|
if (\$diag_auth = 0) { return 404; }
|
||||||
limit_req zone=diag_api burst=2 nodelay;
|
limit_req zone=diag_api burst=2 nodelay;
|
||||||
limit_conn per_ip 2;
|
limit_conn per_ip 2;
|
||||||
proxy_pass http://127.0.0.1:${mtr_backend_port}/api/mtr;
|
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).
|
# No limit_req: librespeed fires many short POSTs (parallel streams).
|
||||||
# proxy_request_buffering off = client sees true network backpressure.
|
# proxy_request_buffering off = client sees true network backpressure.
|
||||||
location ^~ ${diag_path}api/st/up {
|
location ^~ ${diag_path}api/st/up {
|
||||||
|
if (\$diag_auth = 0) { return 404; }
|
||||||
access_log off;
|
access_log off;
|
||||||
limit_conn per_ip 8;
|
limit_conn per_ip 8;
|
||||||
proxy_pass http://127.0.0.1:${mtr_backend_port}/api/st/up;
|
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) ─────────
|
# ── LibreSpeed ping endpoint (answered by nginx, no backend hop) ─────────
|
||||||
location = ${diag_path}api/st/ping {
|
location = ${diag_path}api/st/ping {
|
||||||
|
if (\$diag_auth = 0) { return 404; }
|
||||||
access_log off;
|
access_log off;
|
||||||
limit_conn per_ip 8;
|
limit_conn per_ip 8;
|
||||||
add_header Cache-Control "no-store" always;
|
add_header Cache-Control "no-store" always;
|
||||||
@@ -540,6 +581,7 @@ server {
|
|||||||
|
|
||||||
# ── LibreSpeed client IP ─────────────────────────────────────────────────
|
# ── LibreSpeed client IP ─────────────────────────────────────────────────
|
||||||
location = ${diag_path}api/st/getip {
|
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_pass http://127.0.0.1:${mtr_backend_port}/api/st/getip;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
@@ -548,6 +590,7 @@ server {
|
|||||||
|
|
||||||
# ── Download test files ──────────────────────────────────────────────────
|
# ── Download test files ──────────────────────────────────────────────────
|
||||||
location ^~ ${diag_path}testfiles/ {
|
location ^~ ${diag_path}testfiles/ {
|
||||||
|
if (\$diag_auth = 0) { return 404; }
|
||||||
alias /var/www/diagnostics/testfiles/;
|
alias /var/www/diagnostics/testfiles/;
|
||||||
access_log off;
|
access_log off;
|
||||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||||
@@ -1047,7 +1090,7 @@ EOF
|
|||||||
systemctl enable mtr-backend
|
systemctl enable mtr-backend
|
||||||
systemctl restart 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 "Username: ${config_username}\n"
|
||||||
echo -e "Password: ${config_password}\n"
|
echo -e "Password: ${config_password}\n"
|
||||||
msg_inf "────────────────────────────────────────────────────────────────────────────────"
|
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 "────────────────────────────────────────────────────────────────────────────────"
|
||||||
msg_inf "Please save this screen!"
|
msg_inf "Please save this screen!"
|
||||||
else
|
else
|
||||||
|
|||||||
+52
-1
@@ -107,6 +107,20 @@ else
|
|||||||
blue "diag_path generated: $diag_path"
|
blue "diag_path generated: $diag_path"
|
||||||
fi
|
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' </dev/urandom | head -c 16)
|
||||||
|
blue "diag_token generated"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── detect or generate mtr_backend_port ──────────────────────────────────────
|
# ── detect or generate mtr_backend_port ──────────────────────────────────────
|
||||||
mtr_backend_port=""
|
mtr_backend_port=""
|
||||||
if [[ -f /etc/systemd/system/mtr-backend.service ]]; then
|
if [[ -f /etc/systemd/system/mtr-backend.service ]]; then
|
||||||
@@ -298,6 +312,12 @@ map "\$is_clash_ua:\$arg_provider" \$serve_clash_yaml {
|
|||||||
default 0;
|
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 {
|
||||||
server_tokens off;
|
server_tokens off;
|
||||||
server_name ${domain};
|
server_name ${domain};
|
||||||
@@ -348,16 +368,43 @@ server {
|
|||||||
proxy_pass https://127.0.0.1:${panel_port};
|
proxy_pass https://127.0.0.1:${panel_port};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Diagnostics SSO bridge: valid panel session → diag cookie + redirect.
|
||||||
|
# auth_request runs in the access phase; plain "return" would skip it,
|
||||||
|
# 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;
|
||||||
|
}
|
||||||
|
|
||||||
location ^~ ${diag_path} {
|
location ^~ ${diag_path} {
|
||||||
|
if (\$diag_auth = 0) { return 404; }
|
||||||
limit_req zone=diag_page burst=10 nodelay;
|
limit_req zone=diag_page burst=10 nodelay;
|
||||||
limit_conn per_ip 5;
|
limit_conn per_ip 5;
|
||||||
alias /var/www/diagnostics/;
|
alias /var/www/diagnostics/;
|
||||||
index index.html;
|
index index.html;
|
||||||
try_files \$uri \$uri/ /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 Cache-Control "no-store" always;
|
||||||
add_header X-Robots-Tag "noindex, nofollow" always;
|
add_header X-Robots-Tag "noindex, nofollow" always;
|
||||||
}
|
}
|
||||||
location ^~ ${diag_path}api/mtr {
|
location ^~ ${diag_path}api/mtr {
|
||||||
|
if (\$diag_auth = 0) { return 404; }
|
||||||
limit_req zone=diag_api burst=2 nodelay;
|
limit_req zone=diag_api burst=2 nodelay;
|
||||||
limit_conn per_ip 2;
|
limit_conn per_ip 2;
|
||||||
proxy_pass http://127.0.0.1:${mtr_backend_port}/api/mtr;
|
proxy_pass http://127.0.0.1:${mtr_backend_port}/api/mtr;
|
||||||
@@ -368,6 +415,7 @@ server {
|
|||||||
proxy_send_timeout 120s;
|
proxy_send_timeout 120s;
|
||||||
}
|
}
|
||||||
location ^~ ${diag_path}api/st/up {
|
location ^~ ${diag_path}api/st/up {
|
||||||
|
if (\$diag_auth = 0) { return 404; }
|
||||||
access_log off;
|
access_log off;
|
||||||
limit_conn per_ip 8;
|
limit_conn per_ip 8;
|
||||||
proxy_pass http://127.0.0.1:${mtr_backend_port}/api/st/up;
|
proxy_pass http://127.0.0.1:${mtr_backend_port}/api/st/up;
|
||||||
@@ -380,6 +428,7 @@ server {
|
|||||||
add_header Cache-Control "no-store" always;
|
add_header Cache-Control "no-store" always;
|
||||||
}
|
}
|
||||||
location = ${diag_path}api/st/ping {
|
location = ${diag_path}api/st/ping {
|
||||||
|
if (\$diag_auth = 0) { return 404; }
|
||||||
access_log off;
|
access_log off;
|
||||||
limit_conn per_ip 8;
|
limit_conn per_ip 8;
|
||||||
add_header Cache-Control "no-store" always;
|
add_header Cache-Control "no-store" always;
|
||||||
@@ -387,12 +436,14 @@ server {
|
|||||||
return 200 "";
|
return 200 "";
|
||||||
}
|
}
|
||||||
location = ${diag_path}api/st/getip {
|
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_pass http://127.0.0.1:${mtr_backend_port}/api/st/getip;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
add_header Cache-Control "no-store" always;
|
add_header Cache-Control "no-store" always;
|
||||||
}
|
}
|
||||||
location ^~ ${diag_path}testfiles/ {
|
location ^~ ${diag_path}testfiles/ {
|
||||||
|
if (\$diag_auth = 0) { return 404; }
|
||||||
alias /var/www/diagnostics/testfiles/;
|
alias /var/www/diagnostics/testfiles/;
|
||||||
access_log off;
|
access_log off;
|
||||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||||
@@ -589,5 +640,5 @@ green "════════════════════════
|
|||||||
green " Patch complete"
|
green " Patch complete"
|
||||||
green "══════════════════════════════════════════════"
|
green "══════════════════════════════════════════════"
|
||||||
printf "\n Panel: https://%s/%s/\n" "$domain" "$panel_path"
|
printf "\n Panel: https://%s/%s/\n" "$domain" "$panel_path"
|
||||||
printf " Diagnostics: https://%s%s\n" "$domain" "$diag_path"
|
printf " Diagnostics (panel login): https://%s/%s/diag\n" "$domain" "$panel_path"
|
||||||
echo
|
echo
|
||||||
|
|||||||
Reference in New Issue
Block a user