Commit Graph
26 Commits
Author SHA1 Message Date
Ivan RazinandClaude Fable 5 882ad252b4 fix(installer): assign group_id to hosts so panel can edit/delete them
Hosts inserted without group_id cannot be edited or deleted from the
panel UI. Generate a random 16-char lowercase alphanumeric id per host,
matching the format the panel assigns on manual add.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 09:25:42 +03:00
Ivan RazinandClaude Fable 5 6d0876fd19 feat(installer): add -version flag to pin 3x-ui release
Accepts 3.4.2 or v3.4.2; validates the tag exists on GitHub before
download, falls back to latest when omitted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 16:03:13 +03:00
Ivan RazinandClaude Opus 4.8 906822b42b Fix MTR failing under systemd: grant CAP_NET_RAW via ambient capabilities
mtr ran fine as the mtr-backend user in a shell but failed under the
systemd service with:
  mtr-packet: Failure to open IPv4 sockets: Permission denied

Cause: the unit sets NoNewPrivileges=yes, which strips file capabilities,
and AmbientCapabilities/CapabilityBoundingSet were empty — so mtr-packet
(the helper that opens the raw ICMP socket) had no CAP_NET_RAW. Interactive
runs worked because they had no NoNewPrivileges.

- Grant CAP_NET_RAW the systemd-native way (AmbientCapabilities +
  CapabilityBoundingSet), which survives NoNewPrivileges. Fixed in both
  the full installer and the patch service unit.
- setcap now also targets mtr-packet, not just mtr.
- Add proxy_intercept_errors off to the diag /api/mtr location: the
  server-level "proxy_intercept_errors on" was rewriting a backend 500 into
  an HTML 404, which broke the frontend's response.json() and left the
  client IP stuck on "detecting…".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:25:01 +03:00
Ivan RazinandClaude Opus 4.8 e4fc8a60bb Fix nginx emerg "unknown serve_clash_yaml variable"
The Clash maps ($is_clash_ua, $serve_clash_yaml) were defined only inside
the panel vhost file, but snippets/includes.conf consumes $serve_clash_yaml
and is included by BOTH the panel and reality vhosts. Any state where the
reality vhost loaded while the panel file did not left the variable
undefined globally -> "[emerg] unknown \"serve_clash_yaml\" variable" and
nginx -t failed.

Move both clash maps into a standalone http-level file
(sites-available/00-maps.conf) that is always symlinked into sites-enabled,
so the shared snippet's variable is defined independently of any single
vhost. Reproduced and verified the fix (and the failure without it) with
nginx 1.30.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 09:42:40 +03:00
Ivan RazinandClaude Opus 4.8 88466b9b21 Fix diagnostics 500: harden auth bridge + repair missing panel cert
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>
2026-07-06 09:34:40 +03:00
Ivan RazinandClaude Fable 5 24492dfd3f Fix diag redirects pointing at internal :7443 (unreachable by browser)
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>
2026-07-03 16:16:00 +03:00
Ivan RazinandClaude Fable 5 f864864501 Fix diagnostics SSO deny path returning a 302 with no Location
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>
2026-07-03 16:10:11 +03:00
Ivan RazinandClaude Fable 5 ba6d9ca1d2 Add h2/http1.1 ALPN to tls hosts; make raw diag link self-authorize
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>
2026-07-03 15:54:56 +03:00
Ivan RazinandClaude Fable 5 716840a691 Replace externalProxy arrays with hosts table entries
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>
2026-07-03 15:26:47 +03:00
Ivan RazinandClaude Fable 5 d816b5edcc 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>
2026-07-03 15:05:29 +03:00
Ivan RazinandClaude Fable 5 bd2c47753e Fix installer bugs, version-aware http2 directive, cleaner results screen
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>
2026-07-03 14:29:46 +03:00
Ivan RazinandClaude Fable 5 de1d95e6e0 Replace speed test with LibreSpeed engine: fix 4x-slow upload
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>
2026-07-03 14:15:22 +03:00
Ivan RazinandClaude Sonnet 4.6 2ab9abcf8e Validate that panel and REALITY domains are different before install
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 11:03:57 +03:00
Ivan RazinandClaude Sonnet 4.6 482749c966 Fix proxy-provider bypass: use ?provider=1 param instead of custom header
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>
2026-06-24 20:07:33 +03:00
Ivan RazinandClaude Sonnet 4.6 aa6c744070 Fix proxy-providers: bypass clash.yaml for Mihomo provider refresh requests
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>
2026-06-24 14:43:32 +03:00
Ivan RazinandClaude Sonnet 4.6 f294cf4b6e Fix subscription page white screen: restore prefix location for deep paths
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>
2026-06-24 14:24:06 +03:00
Ivan RazinandClaude Sonnet 4.6 4b5c56785c Fix nginx: use rewrite+internal location instead of proxy_pass inside if/regex
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>
2026-06-24 14:13:46 +03:00
Ivan RazinandClaude Sonnet 4.6 9bb6ed5d5c Serve per-client clash.yaml dynamically via mtr-backend
- clash.yaml template: proxy-provider URL uses ${SUB_ID} placeholder
- install_clash_sub() saves to clash.yaml.tpl (DOMAIN/SUB_PATH substituted, SUB_ID left)
- mtr-backend GET /api/clash?sub_id=xxx reads .tpl, fills in subscription ID, returns YAML
- nginx regex ^/sub_path/(?<clash_sub_id>[^/]*)$ proxies Clash UA to
  mtr-backend /api/clash?sub_id=$clash_sub_id; regular clients pass through to x-ui
- Removed static /__clash_sub internal location

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 14:03:30 +03:00
Ivan RazinandClaude Sonnet 4.6 138e41574c Add Clash subscription with User-Agent routing
- 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>
2026-06-24 13:29:16 +03:00
Ivan RazinandClaude Sonnet 4.6 c548d9934a Fix upload ERR: raise client_max_body_size to 600m and backend cap to 600MB
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 11:45:37 +03:00
Ivan RazinandClaude Sonnet 4.6 ce5876952d Increase speed test to 512 MB for both download and upload
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 11:39:09 +03:00
Ivan RazinandClaude Sonnet 4.6 222d748f4e Replace OpenSpeedTest with custom speed test widget
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>
2026-06-24 11:34:05 +03:00
Ivan RazinandClaude Sonnet 4.6 eeae08de1a Fix MTR API path: nginx rewrites diag_path prefix, backend accepts full paths
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 11:20:04 +03:00
Ivan RazinandClaude Sonnet 4.6 174fee0078 Add OS and CPU pre-flight checks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 11:00:00 +03:00
Ivan RazinandClaude Sonnet 4.6 e2a6505909 Add network diagnostics page with MTR backend and speed test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 10:57:16 +03:00
Ivan RazinandClaude Sonnet 4.6 411490ab82 Initial commit: modular single-file 3x-ui installer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 10:33:07 +03:00