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>
3x-ui now renders share-link endpoints from the hosts table, which
supersedes the legacy externalProxy arrays in stream_settings. Remove
those arrays from all four inbounds and insert one host per inbound
instead (inbound_id resolved by tag):
- REALITY: panel domain, 443, security=same (inherits reality params)
- ws / xhttp / trojan-grpc: panel domain, 443, security=tls,
fingerprint firefox
Also switch the REALITY inbound uTLS fingerprint from chrome to
firefox to match.
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>
Custom header X-Proxy-Provider is not sent by ClashMi and other clients
that don't support header: in proxy-providers. URL query param works in
all clients universally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
include-all is Mihomo-only; explicit use: [sub] works in all Clash clients.
Also remove empty proxies blocks and non-standard remnawave field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Mihomo fetches proxy-providers it sends a Clash UA, causing nginx to
return clash.yaml instead of the actual proxy list — resulting in empty groups.
Fix: proxy-providers in clash.yaml now send X-Proxy-Provider: true header.
nginx map checks combined key "$is_clash_ua:$http_x_proxy_provider":
- Clash UA with no header ("1:") → serve clash.yaml (initial import)
- Clash UA with header present → pass through to x-ui sub port (provider refresh)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Regex location only matches single-segment paths (subscription IDs).
Deep asset paths like /sub_path/static/js/main.js fall through to the
prefix location /sub_path/ which proxies them to x-ui sub port normally.
Nginx regex > prefix priority ensures Clash UA detection still works for IDs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
proxy_pass with URI component is forbidden inside if blocks and regex locations.
Use rewrite ^ /__clash_api?sub_id=$clash_sub_id last; inside if (safe),
then proxy_pass with URI lives in location = /__clash_api (exact match, internal).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
expected-status: 204 ended up after the rules section instead of
inside proxy-providers.sub.health-check where it belongs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- clash.yaml template (proxy-providers -> x-ui base64 sub URL) in assets/clash/
- install_clash_sub() downloads and substitutes domain/sub_path at install time
- nginx: map $http_user_agent $is_clash_client detects Clash/Mihomo/Stash/Surfboard
- sub_path locations rewrite to internal /__clash_sub on UA match
- /__clash_sub serves /var/www/subpage/clash.yaml as text/yaml
- Regular clients still proxy to x-ui sub port unchanged
- Uninstall cleans /var/www/subpage/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Download uses fetch() with streaming progress on 100MB test file.
Upload sends 25MB to /api/upload on mtr-backend which reads the full
body before responding, giving accurate client-side timing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>