Root cause: nginx auth_request returns a raw 500 to the browser whenever
the /__diag_auth subrequest to the panel returns anything other than
2xx/401/403. Two ways that happens:
- the panel's TLS cert is recorded in the DB (webCertFile) but the file
or symlink is missing on disk (e.g. dangling /root/cert symlink), so
the panel can't serve HTTPS and proxy_pass https:// yields a 502; or
- the panel returns a login 302 for the AJAX auth probe.
Either way the browser got a 500 on the diagnostics page.
- Both scripts: add proxy_intercept_errors on + an error_page in
/__diag_auth that coerces every non-allow status (3xx/4xx/5xx, incl.
401/403 so the server-level "=404" trap can't rewrite a genuine deny)
into a 401 -> @diag_denied, so the flow degrades to a panel-login
redirect instead of a 500. Verified with nginx 1.30.3 across
200/401/302/502 upstream backends: all return a clean 302.
- x-ui-patch.sh: the panel-HTTPS check now also treats a configured-but-
missing cert (-e follows symlinks) as needing repair, re-symlinking the
Let's Encrypt cert and re-running `x-ui cert`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The results block already shows the SSO-bridge entry (/panel/diag); add
the direct diag page URL (https://domain/net-XXX/) so the actual page is
visible after patching.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- x-ui-patch.sh: detect when the panel has no TLS cert (empty webCertFile)
and enable HTTPS the way the main installer does — symlink the Let's
Encrypt cert into /root/cert/$domain and register it via `x-ui cert`.
Without this the panel serves plain HTTP and every proxy_pass
https://127.0.0.1:panel_port (including the diag SSO bridge) fails.
- x-ui-patch.sh: guard the `tr </dev/urandom | head -c N` random generators
with `|| true`. head closes the pipe, tr dies with SIGPIPE (141), and
under `pipefail`+`errexit` that intermittently aborts the whole script.
- README: replace the two-step patch download/run block with a single
curl|bash one-liner (also drops a stale wget filename mismatch).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The panel vhost listens on 7443 behind the SNI stream (public port is
443). nginx builds absolute redirect Location headers from the server's
listen port, so `return 302 /path` in the diag SSO bridge emitted
`Location: https://<domain>:7443/...`. Browsers followed that to port
7443, which UFW blocks (only 22/80/443 open) → the diagnostics page
hung and never loaded.
Set `absolute_redirect off` on the panel vhost so nginx emits relative
Location headers; the browser resolves them against the real origin
(:443). Verified with nginx: listen 7443 + return 302 goes from
`http://host:7443/panelXXXX/` to a bare `/panelXXXX/`.
This was the actual cause of the "diagnostics page doesn't load after
panel login" report; earlier fixes addressed real but secondary issues
on the same path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bridge used `error_page 401 403 =302 /<panel>/`, which does an
nginx internal redirect: it serves the panel login page body with a
302 status but sends NO Location header. Browsers (and curl) can't
follow that, so an unauthenticated or expired-session visit to the
diag link rendered a blank page ("doesn't load").
Route the deny path through a named location that does a real
`return 302 /<panel>/`, so the client gets a proper Location and
lands on the panel login. The authorized path (auth_request 200 →
@diag_sso_ok → 302 + diag cookie) is unchanged.
Verified with nginx + a TLS mock panel: unauthenticated hit now
redirects to the panel login; logged-in hit (browser-like cookie
handling) resolves to the diag page in 1-2 redirects, no loop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Host ALPN: ws/xhttp/trojan hosts now advertise ["h2","http/1.1"];
reality host gets [] (its ALPN comes from the reality stream).
Diagnostics access: opening the diag path directly without the
diag_key cookie previously 404'd, so a bookmarked link only worked
after manually visiting /<panel_path>/diag. Now the diag page
302-bounces unauthenticated hits through the SSO bridge, which
validates the 3x-ui panel session and mints the cookie, then redirects
back - so the link works once you're logged into the panel. API and
asset sub-locations still 404 without the cookie (only the HTML page
redirects, never XHR/asset requests).
Verified end to end with nginx + the auth semantics of the installed
3x-ui v3.4.2: logged-in raw hit resolves to the page with no loop,
anonymous hit lands on the panel login.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Bug fixes:
- Root check: exit with message instead of "sudo su -" (which continued
the script as non-root after the shell exited)
- Move destructive cleanup into clean_previous_install(), called after
domain validation - wrong args no longer wipe a working install
- x25519 keygen: use xray-linux-$(_arch) instead of hardcoded amd64,
with fallback for renamed ARM binaries
- Cert renewal: certs are standalone-issued, so renew with pre/post
hooks stopping nginx instead of --nginx (which could not bind :80)
- Drop ngx_stream_geoip2 load_module insertion: module was never
installed or used and broke nginx -t when the .so was absent
- emoji_flag: 10s curl timeout + fallback when ipwho.is unavailable
- Uninstall: rm -f /usr/bin/x-ui (trailing slash made rm fail on file)
nginx >= 1.25.1 deprecates "listen ... http2"; emit "http2 on;" there
and keep the old syntax on older versions (Debian 12, Ubuntu 24.04).
Results screen: drop certbot/ssl cert dumps and Clash sub links, keep
panel URL + credentials + diagnostics. Update CLAUDE.md to current
repo state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Single 512 MB POST over HTTP/2 was throttled by the per-stream h2
flow-control window, reading ~4x low. LibreSpeed uses parallel upload
streams measured via XHR progress events, which amortizes the window
limit (h2 must stay on for trojan-gRPC).
- Vendor speedtest.js + speedtest_worker.js (LGPL) into assets
- mtr-backend.py: ThreadingHTTPServer, /api/st/up upload sink,
/api/st/getip; parallel streams need concurrent handling
- nginx: speedtest locations without limit_req (limit_conn instead),
ping answered by nginx directly, h2 preread/body buffer tuning
- No telemetry, no database
- Drop 512 MB test file; patch script now also substitutes
__SERVER_DOMAIN__/__SERVER_IP__ placeholders
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>