From 14ae6aee002f69d52fcb293136a93d2593a37791 Mon Sep 17 00:00:00 2001 From: Ivan Razin Date: Fri, 26 Jun 2026 11:01:06 +0300 Subject: [PATCH] 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 --- x-ui-patch.sh | 556 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 556 insertions(+) create mode 100644 x-ui-patch.sh diff --git a/x-ui-patch.sh b/x-ui-patch.sh new file mode 100644 index 0000000..ed53c9d --- /dev/null +++ b/x-ui-patch.sh @@ -0,0 +1,556 @@ +#!/usr/bin/env bash +# x-ui-patch.sh — apply current features to an existing 3x-ui installation +# +# What it does (non-destructively, no DB changes): +# 1. Reads ports/paths from /etc/x-ui/x-ui.db +# 2. Detects domains from existing nginx configs +# 3. Regenerates all nginx configs +# 4. Installs network diagnostics (MTR + speed test) +# 5. Installs Clash subscription template (UA-based routing) +# 6. Replaces fake cover site with a current one +# +# Usage: +# wget -qO x-ui-patch.sh https://raw.githubusercontent.com/mozaroc/3x-ui-pro/main/x-ui-patch.sh +# bash x-ui-patch.sh +set -Eeuo pipefail + +XUIDB="/etc/x-ui/x-ui.db" +GITHUB_RAW="https://raw.githubusercontent.com/mozaroc/3x-ui-pro/main" +FAKE_SITE_COUNT=50 +LIB_DIR="/usr/local/lib/3x-ui-pro" +DIAG_ROOT="/var/www/diagnostics" + +# ── colour helpers ──────────────────────────────────────────────────────────── +red() { printf '\033[31m%s\033[0m\n' "$*"; } +green() { printf '\033[32m%s\033[0m\n' "$*"; } +blue() { printf '\033[34m[*] %s\033[0m\n' "$*"; } +die() { red "ERROR: $*" >&2; exit 1; } + +[[ $EUID -eq 0 ]] || die "Run as root: sudo bash $0" +[[ -f "$XUIDB" ]] || die "x-ui.db not found at $XUIDB — is 3x-ui installed?" + +# ── ensure sqlite3 available ────────────────────────────────────────────────── +command -v sqlite3 &>/dev/null || apt-get install -y -q sqlite3 + +# ── read settings from DB ───────────────────────────────────────────────────── +blue "Reading settings from x-ui.db..." +db() { sqlite3 "$XUIDB" "$1"; } + +sub_port=$( db "SELECT value FROM settings WHERE key='subPort';") +sub_path=$( db "SELECT value FROM settings WHERE key='subPath';" | sed 's|^/||;s|/$||') +json_path=$( db "SELECT value FROM settings WHERE key='subJsonPath';" | sed 's|^/||;s|/$||') +panel_port=$( db "SELECT value FROM settings WHERE key='webPort';") +panel_path=$( db "SELECT value FROM settings WHERE key='webBasePath';" | sed 's|^/||;s|/$||') + +[[ -n "$sub_port" ]] || die "subPort not found in DB — re-run the main installer" +[[ -n "$sub_path" ]] || die "subPath not found in DB" +[[ -n "$panel_port" ]] || die "webPort not found in DB" +[[ -n "$panel_path" ]] || die "webBasePath not found in DB" + +printf " sub_port = %-6s sub_path = %s\n" "$sub_port" "$sub_path" +printf " panel_port = %-6s panel_path = %s\n" "$panel_port" "$panel_path" +printf " json_path = %s\n" "$json_path" + +# ── detect domains from existing nginx ─────────────────────────────────────── +blue "Detecting domains from nginx configs..." +domain="" +reality_domain="" + +for f in /etc/nginx/sites-available/*; do + [[ -f "$f" ]] || continue + [[ "$(basename "$f")" == "80.conf" ]] && continue + if grep -q 'listen 7443' "$f" 2>/dev/null; then + domain=$(awk '/server_name/{print $2; exit}' "$f" | tr -d ';') + elif grep -q 'listen 9443' "$f" 2>/dev/null; then + reality_domain=$(awk '/server_name/{print $2; exit}' "$f" | tr -d ';') + fi +done + +[[ -n "$domain" ]] || die "Could not find panel domain (nginx config with 'listen 7443')" +[[ -n "$reality_domain" ]] || die "Could not find reality domain (nginx config with 'listen 9443')" +printf " domain = %s\n" "$domain" +printf " reality_domain = %s\n" "$reality_domain" + +# ── detect xhttp_path ───────────────────────────────────────────────────────── +xhttp_path="" +# 1. from existing includes.conf +if [[ -f /etc/nginx/snippets/includes.conf ]]; then + xhttp_path=$(grep -A1 '#XHTTP' /etc/nginx/snippets/includes.conf \ + | grep 'location' | grep -oP 'location /\K[^ {]+' | head -1 || true) +fi +# 2. from DB (xhttp inbound stream_settings) +if [[ -z "$xhttp_path" ]]; then + xhttp_path=$(db "SELECT json_extract(stream_settings,'$.xhttpSettings.path') + FROM inbounds + WHERE json_extract(stream_settings,'$.network')='xhttp' + LIMIT 1;" 2>/dev/null | sed 's|^/||;s|/$||' || true) +fi +# 3. generate new +if [[ -z "$xhttp_path" ]]; then + xhttp_path=$(tr -dc 'a-zA-Z0-9' /dev/null | head -1 || true) + [[ -n "$p" ]] && { diag_path="$p"; break; } +done +if [[ -n "$diag_path" ]]; then + blue "diag_path reused: $diag_path" +else + diag_path="/net-$(tr -dc 'a-zA-Z0-9' /dev/null || { mtr_backend_port=$p; break; } + done + blue "mtr_backend_port generated: $mtr_backend_port" +else + blue "mtr_backend_port reused: $mtr_backend_port" +fi + +# ── install required packages ───────────────────────────────────────────────── +blue "Installing packages..." +apt-get update -qq +DEBIAN_FRONTEND=noninteractive apt-get install -y -q mtr-tiny python3 curl wget sqlite3 + +# ───────────────────────────────────────────────────────────────────────────── +# NGINX CONFIGS +# ───────────────────────────────────────────────────────────────────────────── +blue "Regenerating nginx configs..." + +mkdir -p /etc/nginx/stream-enabled /etc/nginx/snippets \ + /etc/nginx/sites-available /etc/nginx/sites-enabled + +# ── SNI stream ──────────────────────────────────────────────────────────────── +cat > /etc/nginx/stream-enabled/stream.conf <> /etc/nginx/nginx.conf +grep -xqFR "load_module modules/ngx_stream_module.so;" /etc/nginx/* \ + || sed -i '1s/^/load_module \/usr\/lib\/nginx\/modules\/ngx_stream_module.so; /' /etc/nginx/nginx.conf +grep -xqFR "load_module modules/ngx_stream_geoip2_module.so;" /etc/nginx/* \ + || sed -i '2s/^/load_module \/usr\/lib\/nginx\/modules\/ngx_stream_geoip2_module.so; /' /etc/nginx/nginx.conf +grep -xqFR "worker_rlimit_nofile 16384;" /etc/nginx/* \ + || echo "worker_rlimit_nofile 16384;" >> /etc/nginx/nginx.conf +sed -i "/worker_connections/c\worker_connections 4096;" /etc/nginx/nginx.conf + +# ── HTTP → HTTPS redirect ───────────────────────────────────────────────────── +cat > /etc/nginx/sites-available/80.conf < /etc/nginx/snippets/includes.conf <[^/]+)\$ { + if (\$hack = 1) { return 404; } + if (\$serve_clash_yaml = 1) { rewrite ^ /__clash_api?sub_id=\$clash_sub_id last; } + proxy_redirect off; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_pass https://127.0.0.1:${sub_port}; + } + location /assets { proxy_pass https://127.0.0.1:${sub_port}; } + location /assets/ { proxy_pass https://127.0.0.1:${sub_port}; } + + #Subscription (json) + location /${json_path} { + if (\$hack = 1) { return 404; } + proxy_redirect off; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_pass https://127.0.0.1:${sub_port}; + } + location /${json_path}/ { + if (\$hack = 1) { return 404; } + proxy_redirect off; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_pass https://127.0.0.1:${sub_port}; + } + + #XHTTP + location /${xhttp_path} { + grpc_pass grpc://unix:/dev/shm/uds2023.sock; + grpc_buffer_size 16k; + grpc_socket_keepalive on; + grpc_read_timeout 1h; + grpc_send_timeout 1h; + grpc_set_header Connection ""; + grpc_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + grpc_set_header X-Forwarded-Proto \$scheme; + grpc_set_header X-Forwarded-Port \$server_port; + grpc_set_header Host \$host; + grpc_set_header X-Forwarded-Host \$host; + } + + #Xray generic proxy (WS / gRPC by port+path) + location ~ ^/(?\d+)/(?.*)\$ { + if (\$hack = 1) { return 404; } + client_max_body_size 0; + client_body_timeout 1d; + grpc_read_timeout 1d; + grpc_socket_keepalive on; + proxy_read_timeout 1d; + proxy_http_version 1.1; + proxy_buffering off; + proxy_request_buffering off; + proxy_socket_keepalive on; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + if (\$content_type ~* "GRPC") { + grpc_pass grpc://127.0.0.1:\$fwdport\$is_args\$args; + break; + } + if (\$http_upgrade ~* "(WEBSOCKET|WS)") { + proxy_pass http://127.0.0.1:\$fwdport\$is_args\$args; + break; + } + if (\$request_method ~* ^(PUT|POST|GET)\$) { + proxy_pass http://127.0.0.1:\$fwdport\$is_args\$args; + break; + } + } + + location / { try_files \$uri \$uri/ =404; } +EOF + +# ── Panel domain vhost ──────────────────────────────────────────────────────── +cat > "/etc/nginx/sites-available/${domain}" <|\.\.\.|\.\.\/|\/\/\/)") { set \$hack 1; } + error_page 400 401 402 403 500 501 502 503 504 =404 /404; + proxy_intercept_errors on; + + location /${panel_path}/ { + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_pass https://127.0.0.1:${panel_port}; + } + location /${panel_path} { + proxy_http_version 1.1; + proxy_set_header Upgrade \$http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_pass https://127.0.0.1:${panel_port}; + } + + location ^~ ${diag_path} { + limit_req zone=diag_page burst=10 nodelay; + limit_conn per_ip 5; + alias /var/www/diagnostics/; + index index.html; + try_files \$uri \$uri/ /index.html; + add_header Cache-Control "no-store" always; + add_header X-Robots-Tag "noindex, nofollow" always; + } + location ^~ ${diag_path}api/mtr { + limit_req zone=diag_api burst=2 nodelay; + limit_conn per_ip 2; + proxy_pass http://127.0.0.1:${mtr_backend_port}/api/mtr; + proxy_http_version 1.1; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_read_timeout 120s; + proxy_send_timeout 120s; + } + location ^~ ${diag_path}api/upload { + limit_req zone=diag_page burst=5 nodelay; + proxy_pass http://127.0.0.1:${mtr_backend_port}/api/upload; + proxy_http_version 1.1; + proxy_set_header X-Real-IP \$remote_addr; + proxy_request_buffering off; + client_max_body_size 600m; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + add_header Cache-Control "no-store" always; + } + location ^~ ${diag_path}testfiles/ { + alias /var/www/diagnostics/testfiles/; + access_log off; + add_header Cache-Control "no-store, no-cache, must-revalidate" always; + add_header Content-Disposition "attachment" always; + } + location = /__clash_api { + internal; + proxy_pass http://127.0.0.1:${mtr_backend_port}/api/clash\$is_args\$args; + proxy_http_version 1.1; + proxy_set_header X-Real-IP \$remote_addr; + add_header Content-Type "text/yaml; charset=utf-8" always; + add_header Content-Disposition "attachment; filename=clash.yaml" always; + add_header Cache-Control "no-store" always; + } + + include /etc/nginx/snippets/includes.conf; +} +EOF + +# ── Reality domain vhost ────────────────────────────────────────────────────── +cat > "/etc/nginx/sites-available/${reality_domain}" <|\.\.\.|\.\.\/|\/\/\/)") { set \$hack 1; } + error_page 400 401 402 403 500 501 502 503 504 =404 /404; + proxy_intercept_errors on; + + location /${panel_path}/ { + proxy_redirect off; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_pass http://127.0.0.1:${panel_port}; + } + location /${panel_path} { + proxy_redirect off; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_pass http://127.0.0.1:${panel_port}; + } + + include /etc/nginx/snippets/includes.conf; +} +EOF + +# ── activate sites ──────────────────────────────────────────────────────────── +rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default +ln -sf "/etc/nginx/sites-available/${domain}" /etc/nginx/sites-enabled/ +ln -sf "/etc/nginx/sites-available/${reality_domain}" /etc/nginx/sites-enabled/ +ln -sf "/etc/nginx/sites-available/80.conf" /etc/nginx/sites-enabled/ + +# ───────────────────────────────────────────────────────────────────────────── +# MTR-BACKEND +# ───────────────────────────────────────────────────────────────────────────── +blue "Installing mtr-backend..." + +mkdir -p "$LIB_DIR" +curl -fsSL "${GITHUB_RAW}/assets/diagnostics/mtr-backend.py" -o "${LIB_DIR}/mtr-backend.py" +chmod +x "${LIB_DIR}/mtr-backend.py" + +id mtr-backend &>/dev/null || \ + useradd --system --no-create-home --shell /usr/sbin/nologin mtr-backend + +if command -v setcap &>/dev/null; then + setcap cap_net_raw+ep "$(command -v mtr)" 2>/dev/null || true +fi + +cat > /etc/systemd/system/mtr-backend.service < "${DIAG_ROOT}/index.html" + +# Generate speed test files if missing +[[ -f "${DIAG_ROOT}/testfiles/test-10m.bin" ]] || \ + dd if=/dev/zero bs=1048576 count=10 of="${DIAG_ROOT}/testfiles/test-10m.bin" status=none +[[ -f "${DIAG_ROOT}/testfiles/test-100m.bin" ]] || \ + dd if=/dev/zero bs=1048576 count=100 of="${DIAG_ROOT}/testfiles/test-100m.bin" status=none +[[ -f "${DIAG_ROOT}/testfiles/test-512m.bin" ]] || \ + dd if=/dev/zero bs=1048576 count=512 of="${DIAG_ROOT}/testfiles/test-512m.bin" status=none + +chown -R www-data:www-data "$DIAG_ROOT" + +# ───────────────────────────────────────────────────────────────────────────── +# CLASH SUBSCRIPTION TEMPLATE +# ───────────────────────────────────────────────────────────────────────────── +blue "Installing Clash subscription template..." + +clash_dir="/var/www/subpage" +mkdir -p "$clash_dir" +if curl -fsSL "${GITHUB_RAW}/assets/clash/clash.yaml" -o "${clash_dir}/clash.yaml.tpl"; then + sed -i "s|\${DOMAIN}|${domain}|g" "${clash_dir}/clash.yaml.tpl" + sed -i "s|\${SUB_PATH}|${sub_path}|g" "${clash_dir}/clash.yaml.tpl" + chown -R www-data:www-data "$clash_dir" + chmod 644 "${clash_dir}/clash.yaml.tpl" + green "Clash template installed." +else + red "Failed to download clash.yaml template (non-fatal)." +fi + +# ───────────────────────────────────────────────────────────────────────────── +# FAKE COVER SITE +# ───────────────────────────────────────────────────────────────────────────── +blue "Updating fake cover site..." + +pick=$(( RANDOM % FAKE_SITE_COUNT + 1 )) +site_num=$(printf "%02d" "$pick") +mkdir -p /var/www/html + +if curl -fsSL "${GITHUB_RAW}/assets/fake-sites/site-${site_num}/index.html" \ + -o /var/www/html/index.html; then + chown -R www-data:www-data /var/www/html + chmod 644 /var/www/html/index.html + green "Fake site #${site_num} installed." +else + red "Failed to download fake site (non-fatal)." +fi + +# ───────────────────────────────────────────────────────────────────────────── +# NGINX TEST & RELOAD +# ───────────────────────────────────────────────────────────────────────────── +blue "Testing nginx config..." +if nginx -t 2>&1 | grep -q successful; then + systemctl reload nginx + green "nginx reloaded OK." +else + red "nginx config test failed — check errors above." + nginx -t + exit 1 +fi + +# ───────────────────────────────────────────────────────────────────────────── +# RESULTS +# ───────────────────────────────────────────────────────────────────────────── +echo +green "══════════════════════════════════════════════" +green " Patch complete" +green "══════════════════════════════════════════════" +printf "\n Panel: https://%s/%s/\n" "$domain" "$panel_path" +printf " Sub (plain): https://%s/%s/\n" "$domain" "$sub_path" +printf " Sub (Clash): https://%s/%s/\n" "$domain" "$sub_path" +printf " (Clash/Mihomo clients get clash.yaml automatically)\n" +printf " Diagnostics: https://%s%s\n" "$domain" "$diag_path" +echo