Commit Graph
39 Commits
Author SHA1 Message Date
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 fb68047c02 Fix diag 404: derive panel domain from webCertFile, purge stale vhosts
The diagnostics page 404'd because the fresh panel vhost (with the diag
SSO bridge) was written to the wrong filename and never served: the
nginx-scan domain detection picked the wrong panel domain when stale /
duplicate vhost files from older installs lingered (the loop result is
glob/locale-order dependent, and a leftover panel vhost shadowed the real
one by server_name). The vhost that actually served traffic was a stale,
pre-bridge config, so /<panel>/diag fell through to a 404.

- Panel domain now comes from the authoritative source, the cert x-ui is
  configured with (DB webCertFile -> /root/cert/<domain>/ or
  /etc/letsencrypt/live/<domain>/); nginx scan is only a fallback, and it
  no longer overwrites an already-resolved domain.
- Skip 00-maps.conf in the detection loop.
- At activation: remove stale panel/reality vhost FILES whose name doesn't
  match the managed domains, wipe ALL sites-enabled symlinks, then relink
  only the four managed configs — so nothing stale can stay loaded and
  shadow the panel vhost.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 09:55:57 +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 Opus 4.8 2ed4984989 Patch: also print the direct diagnostics URL at the end
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>
2026-07-06 08:35:42 +03:00
Ivan RazinandClaude Opus 4.8 65e095133f Patch: enforce panel HTTPS, fix set -e SIGPIPE abort, README one-liner
- 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>
2026-07-06 08:34:27 +03:00
Ivan Razin ddc7deef31 Update README.md 2026-07-04 11:52:50 +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 cb9aa79470 Back up /usr/bin/x-ui (management CLI) in addition to /usr/local/x-ui
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 16:20:35 +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 14ae6aee00 Add x-ui-patch.sh: apply current features to existing installation
Reads ports/paths from x-ui.db, detects domains from nginx configs,
regenerates all nginx configs, installs diagnostics + mtr-backend,
Clash subscription template, and updates fake cover site.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 11:01:06 +03:00
Ivan RazinandClaude Sonnet 4.6 3b344ebc9f Rewrite README: current features, YouTube-safe commands
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 20:45:43 +03:00
Ivan RazinandClaude Sonnet 4.6 c6a12850a1 Add x-ui-backup.sh: backup and restore script
Backs up nginx, x-ui DB+binary, mtr-backend, letsencrypt certs,
web content, systemd units, crontab, UFW rules. Restore installs
packages then recovers everything from archive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 20:42:50 +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 b846756a6d Fix clash.yaml: replace include-all with use for cross-client compatibility
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>
2026-06-24 19:54:27 +03:00
Ivan RazinandClaude Sonnet 4.6 35aa8e0084 Remove Smart proxy group and lgbm settings from clash.yaml template
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 19:20:32 +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 33fef3b27b Remove empty proxies: block from clash.yaml template
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 14:33:47 +03:00
Ivan RazinandClaude Sonnet 4.6 c1923d5a6e Update clash.yaml template with new config
New rule set: Telegram, WhatsApp, Discord, YouTube groups; oisd_big ad block;
refilter + ru-bundle for Russian sites; torrent direct; Smart/Fastest groups.
Proxy-providers sub URL kept with ${DOMAIN}/${SUB_PATH}/${SUB_ID} placeholders.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 14:32:57 +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 d9a51d7199 Fix clash.yaml: move orphaned expected-status back into health-check block
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>
2026-06-24 13:49:43 +03:00
Ivan Razin 7aef10624d Update clash.yaml 2026-06-24 13:44:04 +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 6bac6aa84c Increase MTR max packets to 100, update WinMTR instruction to 300 packets
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 11:27:48 +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