diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..99bb78cc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.git +db +cert +*.log +Dockerfile +docker-compose.yml +.tmp +.idea +.vscode +LICENSE +README.* \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d68ea808..e77af2c3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,6 +82,7 @@ jobs: cp x-ui.service.debian x-ui/ cp x-ui.service.rhel x-ui/ cp x-ui.sh x-ui/ + cp -r lib x-ui/ mv x-ui/xui-release x-ui/x-ui mkdir x-ui/bin cd x-ui/bin @@ -178,12 +179,12 @@ jobs: $env:GOOS="windows" $env:GOARCH="amd64" go build -ldflags "-w -s" -o xui-release.exe -v main.go - + mkdir x-ui Copy-Item xui-release.exe x-ui\ mkdir x-ui\bin cd x-ui\bin - + # Download Xray for Windows $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.12.8/" Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip" diff --git a/.gitignore b/.gitignore index 8fa4eeb0..69b9c69d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# shared volume +geodata/ + # Ignore editor and IDE settings .idea/ .vscode/ diff --git a/DockerEntrypoint.sh b/DockerEntrypoint.sh index 7511d2ea..455c3211 100644 --- a/DockerEntrypoint.sh +++ b/DockerEntrypoint.sh @@ -1,7 +1,28 @@ #!/bin/sh +set -eu + +: "${MAX_GEODATA_DIR_WAIT:=30}" +: "${WAIT_INTERVAL:=10}" +: "${GEODATA_DIR:?GEODATA_DIR is required}" + +FINISH_FILE="$GEODATA_DIR/cron-job-finished.txt" +ELAPSED=0 + +while [ ! -f "$FINISH_FILE" ] && [ "$ELAPSED" -lt "$MAX_GEODATA_DIR_WAIT" ]; do + echo "Waiting for geodata initialization... ($ELAPSED/$MAX_GEODATA_DIR_WAIT seconds)" + sleep $WAIT_INTERVAL + ELAPSED=$((ELAPSED + WAIT_INTERVAL)) +done + +if [ ! -f "$FINISH_FILE" ]; then + echo "ERROR: Geodata initialization timed out after $MAX_GEODATA_DIR_WAIT seconds" + echo "Container startup aborted." + exit 1 +fi + # Start fail2ban -[ $XUI_ENABLE_FAIL2BAN == "true" ] && fail2ban-client -x start +[ "$XUI_ENABLE_FAIL2BAN" = "true" ] && fail2ban-client -x start # Run x-ui exec /app/x-ui diff --git a/DockerInit.sh b/DockerInit.sh deleted file mode 100755 index debfbbb8..00000000 --- a/DockerInit.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/sh -case $1 in - amd64) - ARCH="64" - FNAME="amd64" - ;; - i386) - ARCH="32" - FNAME="i386" - ;; - armv8 | arm64 | aarch64) - ARCH="arm64-v8a" - FNAME="arm64" - ;; - armv7 | arm | arm32) - ARCH="arm32-v7a" - FNAME="arm32" - ;; - armv6) - ARCH="arm32-v6" - FNAME="armv6" - ;; - *) - ARCH="64" - FNAME="amd64" - ;; -esac -mkdir -p build/bin -cd build/bin -curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v25.12.8/Xray-linux-${ARCH}.zip" -unzip "Xray-linux-${ARCH}.zip" -rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat -mv xray "xray-linux-${FNAME}" -curl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat -curl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat -curl -sfLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat -curl -sfLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat -curl -sfLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat -curl -sfLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat -cd ../../ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index bdf877ce..a58aba1e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,41 +2,41 @@ # Stage: Builder # ======================================================== FROM golang:1.25-alpine AS builder -WORKDIR /app -ARG TARGETARCH -RUN apk --no-cache --update add \ +WORKDIR /app + +RUN apk add --no-cache \ build-base \ - gcc \ - curl \ - unzip + gcc + +# docker CACHE +COPY go.mod go.sum ./ +RUN go mod download COPY . . ENV CGO_ENABLED=1 ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE" RUN go build -ldflags "-w -s" -o build/x-ui main.go -RUN ./DockerInit.sh "$TARGETARCH" # ======================================================== # Stage: Final Image of 3x-ui # ======================================================== FROM alpine -ENV TZ=Asia/Tehran + WORKDIR /app -RUN apk add --no-cache --update \ +RUN apk add --no-cache \ ca-certificates \ tzdata \ fail2ban \ bash \ curl +COPY DockerEntrypoint.sh /app/ COPY --from=builder /app/build/ /app/ -COPY --from=builder /app/DockerEntrypoint.sh /app/ COPY --from=builder /app/x-ui.sh /usr/bin/x-ui - # Configure fail2ban RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \ && cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local \ @@ -52,5 +52,5 @@ RUN chmod +x \ ENV XUI_ENABLE_FAIL2BAN="true" EXPOSE 2053 VOLUME [ "/etc/x-ui" ] -CMD [ "./x-ui" ] + ENTRYPOINT [ "/app/DockerEntrypoint.sh" ] diff --git a/README.md b/README.md index f00a2fb0..1fd770ce 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md) +[English](/README.md) | [فارسی](docs/README.fa_IR.md) | [العربية](docs/README.ar_EG.md) | [中文](docs/README.zh_CN.md) | [Español](docs/README.es_ES.md) | [Русский](docs/README.ru_RU.md)

- - 3x-ui + + 3x-ui

@@ -44,12 +44,12 @@ For full documentation, please visit the [project Wiki](https://github.com/MHSan **If this project is helpful to you, you may wish to give it a**:star2: -Buy Me A Coffee +Buy Me A Coffee - +

- Crypto donation button by NOWPayments + Crypto donation button by NOWPayments ## Stargazers over Time diff --git a/docker-compose.yml b/docker-compose.yml index 198df198..0fa748c1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,57 @@ services: - 3xui: + 3x-ui: build: context: . - dockerfile: ./Dockerfile container_name: 3xui_app - # hostname: yourhostname <- optional volumes: - $PWD/db/:/etc/x-ui/ - $PWD/cert/:/root/cert/ + - $PWD/geodata/:/app/bin environment: + TZ: "Asia/Tehran" XRAY_VMESS_AEAD_FORCED: "false" XUI_ENABLE_FAIL2BAN: "true" + GEODATA_DIR: "/app/bin" tty: true network_mode: host restart: unless-stopped + depends_on: + - geodata-cron + + docker-proxy: + image: tecnativa/docker-socket-proxy + container_name: docker_proxy + restart: unless-stopped + environment: + - CONTAINERS=1 + - POST=1 + - ALLOW_RESTARTS=1 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - docker-internal + + geodata-cron: + build: + context: docker-cron-runner + args: + XRAY_VERSION: "${XRAY_VERSION:-v25.10.15}" + XRAY_BUILD_DIR: "/app/xray" + container_name: geodata_cron + restart: unless-stopped + depends_on: + - docker-proxy + environment: + TZ: "UTC" + DOCKER_PROXY_URL: "http://docker-proxy:2375" + TARGET_CONTAINER_NAME: "3xui_app" + CRON_SCHEDULE: "0 */6 * * *" + SHARED_VOLUME_PATH: "/app/bin" + volumes: + - $PWD/geodata/:/app/bin/ + networks: + - docker-internal + +networks: + docker-internal: + driver: bridge \ No newline at end of file diff --git a/docker-cron-runner/Dockerfile b/docker-cron-runner/Dockerfile new file mode 100644 index 00000000..224dca41 --- /dev/null +++ b/docker-cron-runner/Dockerfile @@ -0,0 +1,28 @@ +FROM alpine:3.20 + +ARG TARGETARCH +ARG XRAY_VERSION +ARG XRAY_BUILD_DIR + +WORKDIR /app + +RUN apk add --no-cache \ + wget \ + unzip \ + curl \ + bash \ + ca-certificates \ + tzdata + + +COPY xray-tools.sh entrypoint.sh cron-job-script.sh ./ + +RUN chmod +x /app/xray-tools.sh /app/entrypoint.sh /app/cron-job-script.sh \ + && mkdir -p "$XRAY_BUILD_DIR" \ + && ./xray-tools.sh install_xray_core "$TARGETARCH" "$XRAY_BUILD_DIR" "$XRAY_VERSION" \ + && ./xray-tools.sh update_geodata_in_docker "$XRAY_BUILD_DIR" + +ENV XRAY_BUILD_DIR=${XRAY_BUILD_DIR} + +#CMD ["/app/entrypoint.sh"] \ +ENTRYPOINT ["/app/entrypoint.sh"] \ No newline at end of file diff --git a/docker-cron-runner/cron-job-script.sh b/docker-cron-runner/cron-job-script.sh new file mode 100644 index 00000000..ec2eff8b --- /dev/null +++ b/docker-cron-runner/cron-job-script.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env sh +set -eu + +echo "[$(date)] Starting geodata update..." + +FINISHED_FLAG="${SHARED_VOLUME_PATH}/cron-job-finished.txt" + +if [ -f "$FINISHED_FLAG" ]; then + rm -f "$FINISHED_FLAG" +fi + +/app/xray-tools.sh update_geodata_in_docker "${SHARED_VOLUME_PATH}" +touch "$FINISHED_FLAG" + +echo "[$(date)] Geodata update finished, restarting container..." + +HTTP_CODE=$( + curl -s -X POST \ + "${DOCKER_PROXY_URL}/containers/${TARGET_CONTAINER_NAME}/restart" \ + -o /dev/null -w "%{http_code}" +) + +echo "[$(date)] Restart request sent, HTTP status: ${HTTP_CODE}" \ No newline at end of file diff --git a/docker-cron-runner/entrypoint.sh b/docker-cron-runner/entrypoint.sh new file mode 100644 index 00000000..9d4cc001 --- /dev/null +++ b/docker-cron-runner/entrypoint.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env sh + +set -eu + +: "${CRON_SCHEDULE:=0 */6 * * *}" +: "${DOCKER_PROXY_URL:?DOCKER_PROXY_URL is required}" +: "${TARGET_CONTAINER_NAME:?TARGET_CONTAINER_NAME is required}" # required for cron-job-script.sh for container restart +: "${SHARED_VOLUME_PATH:?SHARED_VOLUME_PATH is required}" + +CRON_ENV_FILE="/env.sh" + +env | grep -v '^CRON_SCHEDULE=' | sed 's/^/export /' > "$CRON_ENV_FILE" +echo "${CRON_SCHEDULE} . ${CRON_ENV_FILE} && /app/cron-job-script.sh >> /var/log/cron.log 2>&1" > /etc/crontabs/root + +echo "Starting crond with schedule: ${CRON_SCHEDULE}" + +mkdir -p /var/log +touch /var/log/cron.log + +mkdir -p "$SHARED_VOLUME_PATH" +cp -r "$XRAY_BUILD_DIR"/* "$SHARED_VOLUME_PATH"/ + +touch "$SHARED_VOLUME_PATH/cron-job-finished.txt" # cron job execution imitation + +exec crond -f -l 2 \ No newline at end of file diff --git a/docker-cron-runner/xray-tools.sh b/docker-cron-runner/xray-tools.sh new file mode 100644 index 00000000..35ec12b6 --- /dev/null +++ b/docker-cron-runner/xray-tools.sh @@ -0,0 +1,173 @@ +#!/bin/sh + +safe_download_and_update() { + url="$1" + dest="$2" + + # Create a temporary file + tmp=$(mktemp "${dest}.XXXXXX") || return 1 + + # Download file into a temporary location + if wget -q -O "$tmp" "$url"; then + # Check that the downloaded file is not empty + if [ -s "$tmp" ]; then + # Atomically replace the destination file + mv "$tmp" "$dest" + echo "[OK] Downloaded: $dest" + else + echo "[ERR] Downloaded file is empty: $url" + rm -f "$tmp" + return 1 + fi + else + echo "[ERR] Failed to download: $url" + rm -f "$tmp" + return 1 + fi +} + +update_all_geofiles() { + update_main_geofiles + update_ir_geofiles + update_ru_geofiles +} + +update_main_geofiles() { + safe_download_and_update \ + "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" \ + "geoip.dat" + + safe_download_and_update \ + "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" \ + "geosite.dat" +} + +update_ir_geofiles() { + safe_download_and_update \ + "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" \ + "geoip_IR.dat" + + safe_download_and_update \ + "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" \ + "geosite_IR.dat" +} + +update_ru_geofiles() { + safe_download_and_update \ + "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" \ + "geoip_RU.dat" + + safe_download_and_update \ + "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" \ + "geosite_RU.dat" +} + +update_geodata_in_docker() { + XRAYDIR="$1" + OLD_DIR=$(pwd) + trap 'cd "$OLD_DIR"' EXIT + + echo "[$(date)] Running update_geodata" + + if [ ! -d "$XRAYDIR" ]; then + mkdir -p "$XRAYDIR" + fi + cd "$XRAYDIR" + + update_all_geofiles + echo "[$(date)] All geo files have been updated successfully!" +} + + +install_xray_core() { + TARGETARCH="$1" + XRAYDIR="$2" + XRAY_VERSION="$3" + + OLD_DIR=$(pwd) + trap 'cd "$OLD_DIR"' EXIT + + echo "[$(date)] Running install_xray_core" + + case $1 in + amd64) + ARCH="64" + FNAME="amd64" + ;; + i386) + ARCH="32" + FNAME="i386" + ;; + armv8 | arm64 | aarch64) + ARCH="arm64-v8a" + FNAME="arm64" + ;; + armv7 | arm | arm32) + ARCH="arm32-v7a" + FNAME="arm32" + ;; + armv6) + ARCH="arm32-v6" + FNAME="armv6" + ;; + *) + ARCH="64" + FNAME="amd64" + ;; + esac + + if [ ! -d "$XRAYDIR" ]; then + mkdir -p "$XRAYDIR" + fi + cd "$XRAYDIR" + + wget -q "https://github.com/XTLS/Xray-core/releases/download/${XRAY_VERSION}/Xray-linux-${ARCH}.zip" + + # Validate the downloaded zip file + if [ ! -f "Xray-linux-${ARCH}.zip" ] || [ ! -s "Xray-linux-${ARCH}.zip" ]; then + echo "[ERR] Failed to download Xray-core zip or file is empty" + exit 1 + fi + + unzip -q "Xray-linux-${ARCH}.zip" -d ./xray-unzip + + # Validate the extracted xray binary + if [ -f "./xray-unzip/xray" ]; then + cp ./xray-unzip/xray ./"xray-linux-${FNAME}" + rm -r xray-unzip + rm "Xray-linux-${ARCH}.zip" + else + echo "[ERR] Failed to extract xray binary" + exit 1 + fi +} + +if [ "${0##*/}" = "xray-tools.sh" ]; then + cmd="$1" + shift || true + + case "$cmd" in + install_xray_core) + # args: TARGETARCH XRAYDIR XRAY_VERSION + install_xray_core "$@" + ;; + update_geodata_in_docker) + # args: XRAYDIR + update_geodata_in_docker "$@" + ;; + update_all_geofiles) + update_all_geofiles + ;; + ""|help|-h|--help) + echo "Usage:" + echo " $0 install_xray_core TARGETARCH XRAYDIR XRAY_VERSION" + echo " $0 update_geodata_in_docker XRAYDIR" + exit 0 + ;; + *) + echo "Unknown command: $cmd" >&2 + echo "Try: $0 help" >&2 + exit 1 + ;; + esac +fi \ No newline at end of file diff --git a/README.ar_EG.md b/docs/README.ar_EG.md similarity index 94% rename from README.ar_EG.md rename to docs/README.ar_EG.md index 01acad34..04774f33 100644 --- a/README.ar_EG.md +++ b/docs/README.ar_EG.md @@ -1,4 +1,4 @@ -[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md) +[English](../README.md) | [فارسی](README.fa_IR.md) | [العربية](README.ar_EG.md) | [中文](README.zh_CN.md) | [Español](README.es_ES.md) | [Русский](README.ru_RU.md)

@@ -46,6 +46,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install. Buy Me A Coffee +

Crypto donation button by NOWPayments diff --git a/README.es_ES.md b/docs/README.es_ES.md similarity index 94% rename from README.es_ES.md rename to docs/README.es_ES.md index 63d6ce49..915ab48d 100644 --- a/README.es_ES.md +++ b/docs/README.es_ES.md @@ -1,4 +1,4 @@ -[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md) +[English](../README.md) | [فارسی](README.fa_IR.md) | [العربية](README.ar_EG.md) | [中文](README.zh_CN.md) | [Español](README.es_ES.md) | [Русский](README.ru_RU.md)

@@ -46,7 +46,7 @@ Para documentación completa, visita la [Wiki del proyecto](https://github.com/M Buy Me A Coffee - +

Crypto donation button by NOWPayments diff --git a/README.fa_IR.md b/docs/README.fa_IR.md similarity index 94% rename from README.fa_IR.md rename to docs/README.fa_IR.md index 94165260..15223eef 100644 --- a/README.fa_IR.md +++ b/docs/README.fa_IR.md @@ -1,4 +1,4 @@ -[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md) +[English](../README.md) | [فارسی](README.fa_IR.md) | [العربية](README.ar_EG.md) | [中文](README.zh_CN.md) | [Español](README.es_ES.md) | [Русский](README.ru_RU.md)

@@ -46,7 +46,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install. Buy Me A Coffee - +

Crypto donation button by NOWPayments diff --git a/README.ru_RU.md b/docs/README.ru_RU.md similarity index 95% rename from README.ru_RU.md rename to docs/README.ru_RU.md index 6623a801..5ee8ccd1 100644 --- a/README.ru_RU.md +++ b/docs/README.ru_RU.md @@ -1,4 +1,4 @@ -[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md) +[English](../README.md) | [فارسی](README.fa_IR.md) | [العربية](README.ar_EG.md) | [中文](README.zh_CN.md) | [Español](README.es_ES.md) | [Русский](README.ru_RU.md)

@@ -46,7 +46,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install. Buy Me A Coffee - +

Crypto donation button by NOWPayments diff --git a/README.zh_CN.md b/docs/README.zh_CN.md similarity index 93% rename from README.zh_CN.md rename to docs/README.zh_CN.md index 6eb30ee0..e32f47ac 100644 --- a/README.zh_CN.md +++ b/docs/README.zh_CN.md @@ -1,4 +1,5 @@ -[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md) +[English](../README.md) | [فارسی](README.fa_IR.md) | [العربية](README.ar_EG.md) | [中文](README.zh_CN.md) | [Español](README.es_ES.md) | [Русский](README.ru_RU.md) +

@@ -46,7 +47,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install. Buy Me A Coffee - +

Crypto donation button by NOWPayments diff --git a/docs/media/3x-ui-dark.png b/docs/media/3x-ui-dark.png new file mode 100644 index 00000000..e5f76b19 Binary files /dev/null and b/docs/media/3x-ui-dark.png differ diff --git a/docs/media/3x-ui-light.png b/docs/media/3x-ui-light.png new file mode 100644 index 00000000..a77c830d Binary files /dev/null and b/docs/media/3x-ui-light.png differ diff --git a/docs/media/default-yellow.png b/docs/media/default-yellow.png new file mode 100644 index 00000000..21ed36b7 Binary files /dev/null and b/docs/media/default-yellow.png differ diff --git a/docs/media/donation-button-black.svg b/docs/media/donation-button-black.svg new file mode 100644 index 00000000..25217129 --- /dev/null +++ b/docs/media/donation-button-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/install.sh b/install.sh index d8e95e22..00111845 100644 --- a/install.sh +++ b/install.sh @@ -702,17 +702,20 @@ install_x-ui() { # Download resources if [ $# == 0 ]; then - tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') +# tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') + tag_version=$(curl -Ls "https://api.github.com/repos/mixa2130/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') if [[ ! -n "$tag_version" ]]; then echo -e "${yellow}Trying to fetch version with IPv4...${plain}" - tag_version=$(curl -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') +# tag_version=$(curl -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') + tag_version=$(curl -4 -Ls "https://api.github.com/repos/mixa2130/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') if [[ ! -n "$tag_version" ]]; then echo -e "${red}Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later${plain}" exit 1 fi fi echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..." - curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz +# curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz + curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/mixa2130/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz if [[ $? -ne 0 ]]; then echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}" exit 1 @@ -721,12 +724,12 @@ install_x-ui() { tag_version=$1 tag_version_numeric=${tag_version#v} min_version="2.3.5" - + if [[ "$(printf '%s\n' "$min_version" "$tag_version_numeric" | sort -V | head -n1)" != "$min_version" ]]; then echo -e "${red}Please use a newer version (at least v2.3.5). Exiting installation.${plain}" exit 1 fi - + url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz" echo -e "Beginning to install x-ui $1" curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz ${url} @@ -735,11 +738,14 @@ install_x-ui() { exit 1 fi fi - curl -4fLRo /usr/bin/x-ui-temp https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh - if [[ $? -ne 0 ]]; then - echo -e "${red}Failed to download x-ui.sh${plain}" - exit 1 - fi + + + +# curl -4fLRo /usr/bin/x-ui-temp https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh +# if [[ $? -ne 0 ]]; then +# echo -e "${red}Failed to download x-ui.sh${plain}" +# exit 1 +# fi # Stop x-ui service and remove old resources if [[ -e ${xui_folder}/ ]]; then @@ -767,7 +773,8 @@ install_x-ui() { chmod +x x-ui bin/xray-linux-$(arch) # Update x-ui cli and se set permission - mv -f /usr/bin/x-ui-temp /usr/bin/x-ui +# mv -f /usr/bin/x-ui-temp /usr/bin/x-ui + cp x-ui.sh /usr/bin/x-ui chmod +x /usr/bin/x-ui mkdir -p /var/log/x-ui config_after_install diff --git a/lib/bbr.sh b/lib/bbr.sh new file mode 100644 index 00000000..d9ee9236 --- /dev/null +++ b/lib/bbr.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# lib/bbr.sh - BBR TCP congestion control management + +# Include guard +[[ -n "${__X_UI_BBR_INCLUDED:-}" ]] && return 0 +__X_UI_BBR_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" + +bbr_menu() { + echo -e "${green}\t1.${plain} Enable BBR" + echo -e "${green}\t2.${plain} Disable BBR" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -rp "Choose an option: " choice + case "$choice" in + 0) + show_menu + ;; + 1) + enable_bbr + bbr_menu + ;; + 2) + disable_bbr + bbr_menu + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + bbr_menu + ;; + esac +} + +disable_bbr() { + + if ! grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf || ! grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then + echo -e "${yellow}BBR is not currently enabled.${plain}" + before_show_menu + fi + + # Replace BBR with CUBIC configurations + sed -i 's/net.core.default_qdisc=fq/net.core.default_qdisc=pfifo_fast/' /etc/sysctl.conf + sed -i 's/net.ipv4.tcp_congestion_control=bbr/net.ipv4.tcp_congestion_control=cubic/' /etc/sysctl.conf + + # Apply changes + sysctl -p + + # Verify that BBR is replaced with CUBIC + if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "cubic" ]]; then + echo -e "${green}BBR has been replaced with CUBIC successfully.${plain}" + else + echo -e "${red}Failed to replace BBR with CUBIC. Please check your system configuration.${plain}" + fi +} + +enable_bbr() { + if grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf && grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then + echo -e "${green}BBR is already enabled!${plain}" + before_show_menu + fi + + # Enable BBR + echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf + echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf + + # Apply changes + sysctl -p + + # Verify that BBR is enabled + if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then + echo -e "${green}BBR has been enabled successfully.${plain}" + else + echo -e "${red}Failed to enable BBR. Please check your system configuration.${plain}" + fi +} diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 00000000..ca1cc80d --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# lib/common.sh - Base library with colors, logging, helpers, and global variables + +# Include guard +[[ -n "${__X_UI_COMMON_INCLUDED:-}" ]] && return 0 +__X_UI_COMMON_INCLUDED=1 + +# Colors +red='\033[0;31m' +green='\033[0;32m' +blue='\033[0;34m' +yellow='\033[0;33m' +plain='\033[0m' + +# Logging functions +LOGD() { + echo -e "${yellow}[DEG] $* ${plain}" +} + +LOGE() { + echo -e "${red}[ERR] $* ${plain}" +} + +LOGI() { + echo -e "${green}[INF] $* ${plain}" +} + +# Simple helpers for domain/IP validation +is_ipv4() { + [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1 +} + +is_ipv6() { + [[ "$1" =~ : ]] && return 0 || return 1 +} + +is_ip() { + is_ipv4 "$1" || is_ipv6 "$1" +} + +is_domain() { + [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1 +} + +# Generate random string +gen_random_string() { + local length="$1" + local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' &2 + exit 1 +fi + +os_version="" +os_version=$(grep "^VERSION_ID" /etc/os-release | cut -d '=' -f2 | tr -d '"' | tr -d '.') + +# Declare global variables +xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}" +xui_service="${XUI_SERVICE:=/etc/systemd/system}" +log_folder="${XUI_LOG_FOLDER:=/var/log/x-ui}" +mkdir -p "${log_folder}" +iplimit_log_path="${log_folder}/3xipl.log" +iplimit_banned_log_path="${log_folder}/3xipl-banned.log" diff --git a/lib/extras.sh b/lib/extras.sh new file mode 100644 index 00000000..b246f6aa --- /dev/null +++ b/lib/extras.sh @@ -0,0 +1,142 @@ +#!/bin/bash +# lib/extras.sh - Extra utilities (speedtest, SSH port forwarding) + +# Include guard +[[ -n "${__X_UI_EXTRAS_INCLUDED:-}" ]] && return 0 +__X_UI_EXTRAS_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" +source "${LIB_DIR}/service.sh" + +run_speedtest() { + # Check if Speedtest is already installed + if ! command -v speedtest &>/dev/null; then + # If not installed, determine installation method + if command -v snap &>/dev/null; then + # Use snap to install Speedtest + echo "Installing Speedtest using snap..." + snap install speedtest + else + # Fallback to using package managers + local pkg_manager="" + local speedtest_install_script="" + + if command -v dnf &>/dev/null; then + pkg_manager="dnf" + speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh" + elif command -v yum &>/dev/null; then + pkg_manager="yum" + speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh" + elif command -v apt-get &>/dev/null; then + pkg_manager="apt-get" + speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh" + elif command -v apt &>/dev/null; then + pkg_manager="apt" + speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh" + fi + + if [[ -z $pkg_manager ]]; then + echo "Error: Package manager not found. You may need to install Speedtest manually." + return 1 + else + echo "Installing Speedtest using $pkg_manager..." + curl -s $speedtest_install_script | bash + $pkg_manager install -y speedtest + fi + fi + fi + + speedtest +} + +SSH_port_forwarding() { + local URL_lists=( + "https://api4.ipify.org" + "https://ipv4.icanhazip.com" + "https://v4.api.ipinfo.io/ip" + "https://ipv4.myexternalip.com/raw" + "https://4.ident.me" + "https://check-host.net/ip" + ) + local server_ip="" + for ip_address in "${URL_lists[@]}"; do + server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]') + if [[ -n "${server_ip}" ]]; then + break + fi + done + local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') + local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') + local existing_listenIP=$(${xui_folder}/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}') + local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep -Eo 'cert: .+' | awk '{print $2}') + local existing_key=$(${xui_folder}/x-ui setting -getCert true | grep -Eo 'key: .+' | awk '{print $2}') + + local config_listenIP="" + local listen_choice="" + + if [[ -n "$existing_cert" && -n "$existing_key" ]]; then + echo -e "${green}Panel is secure with SSL.${plain}" + before_show_menu + fi + if [[ -z "$existing_cert" && -z "$existing_key" && (-z "$existing_listenIP" || "$existing_listenIP" == "0.0.0.0") ]]; then + echo -e "\n${red}Warning: No Cert and Key found! The panel is not secure.${plain}" + echo "Please obtain a certificate or set up SSH port forwarding." + fi + + if [[ -n "$existing_listenIP" && "$existing_listenIP" != "0.0.0.0" && (-z "$existing_cert" && -z "$existing_key") ]]; then + echo -e "\n${green}Current SSH Port Forwarding Configuration:${plain}" + echo -e "Standard SSH command:" + echo -e "${yellow}ssh -L 2222:${existing_listenIP}:${existing_port} root@${server_ip}${plain}" + echo -e "\nIf using SSH key:" + echo -e "${yellow}ssh -i -L 2222:${existing_listenIP}:${existing_port} root@${server_ip}${plain}" + echo -e "\nAfter connecting, access the panel at:" + echo -e "${yellow}http://localhost:2222${existing_webBasePath}${plain}" + fi + + echo -e "\nChoose an option:" + echo -e "${green}1.${plain} Set listen IP" + echo -e "${green}2.${plain} Clear listen IP" + echo -e "${green}0.${plain} Back to Main Menu" + read -rp "Choose an option: " num + + case "$num" in + 1) + if [[ -z "$existing_listenIP" || "$existing_listenIP" == "0.0.0.0" ]]; then + echo -e "\nNo listenIP configured. Choose an option:" + echo -e "1. Use default IP (127.0.0.1)" + echo -e "2. Set a custom IP" + read -rp "Select an option (1 or 2): " listen_choice + + config_listenIP="127.0.0.1" + [[ "$listen_choice" == "2" ]] && read -rp "Enter custom IP to listen on: " config_listenIP + + ${xui_folder}/x-ui setting -listenIP "${config_listenIP}" >/dev/null 2>&1 + echo -e "${green}listen IP has been set to ${config_listenIP}.${plain}" + echo -e "\n${green}SSH Port Forwarding Configuration:${plain}" + echo -e "Standard SSH command:" + echo -e "${yellow}ssh -L 2222:${config_listenIP}:${existing_port} root@${server_ip}${plain}" + echo -e "\nIf using SSH key:" + echo -e "${yellow}ssh -i -L 2222:${config_listenIP}:${existing_port} root@${server_ip}${plain}" + echo -e "\nAfter connecting, access the panel at:" + echo -e "${yellow}http://localhost:2222${existing_webBasePath}${plain}" + restart + else + config_listenIP="${existing_listenIP}" + echo -e "${green}Current listen IP is already set to ${config_listenIP}.${plain}" + fi + ;; + 2) + ${xui_folder}/x-ui setting -listenIP 0.0.0.0 >/dev/null 2>&1 + echo -e "${green}Listen IP has been cleared.${plain}" + restart + ;; + 0) + show_menu + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + SSH_port_forwarding + ;; + esac +} diff --git a/lib/firewall.sh b/lib/firewall.sh new file mode 100644 index 00000000..c1994b4b --- /dev/null +++ b/lib/firewall.sh @@ -0,0 +1,200 @@ +#!/bin/bash +# lib/firewall.sh - UFW firewall management + +# Include guard +[[ -n "${__X_UI_FIREWALL_INCLUDED:-}" ]] && return 0 +__X_UI_FIREWALL_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" + +firewall_menu() { + echo -e "${green}\t1.${plain} ${green}Install${plain} Firewall" + echo -e "${green}\t2.${plain} Port List [numbered]" + echo -e "${green}\t3.${plain} ${green}Open${plain} Ports" + echo -e "${green}\t4.${plain} ${red}Delete${plain} Ports from List" + echo -e "${green}\t5.${plain} ${green}Enable${plain} Firewall" + echo -e "${green}\t6.${plain} ${red}Disable${plain} Firewall" + echo -e "${green}\t7.${plain} Firewall Status" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -rp "Choose an option: " choice + case "$choice" in + 0) + show_menu + ;; + 1) + install_firewall + firewall_menu + ;; + 2) + ufw status numbered + firewall_menu + ;; + 3) + open_ports + firewall_menu + ;; + 4) + delete_ports + firewall_menu + ;; + 5) + ufw enable + firewall_menu + ;; + 6) + ufw disable + firewall_menu + ;; + 7) + ufw status verbose + firewall_menu + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + firewall_menu + ;; + esac +} + +install_firewall() { + if ! command -v ufw &>/dev/null; then + echo "ufw firewall is not installed. Installing now..." + apt-get update + apt-get install -y ufw + else + echo "ufw firewall is already installed" + fi + + # Check if the firewall is inactive + if ufw status | grep -q "Status: active"; then + echo "Firewall is already active" + else + echo "Activating firewall..." + # Open the necessary ports + ufw allow ssh + ufw allow http + ufw allow https + ufw allow 2053/tcp #webPort + ufw allow 2096/tcp #subport + + # Enable the firewall + ufw --force enable + fi +} + +open_ports() { + # Prompt the user to enter the ports they want to open + read -rp "Enter the ports you want to open (e.g. 80,443,2053 or range 400-500): " ports + + # Check if the input is valid + if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then + echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2 + exit 1 + fi + + # Open the specified ports using ufw + IFS=',' read -ra PORT_LIST <<<"$ports" + for port in "${PORT_LIST[@]}"; do + if [[ $port == *-* ]]; then + # Split the range into start and end ports + start_port=$(echo $port | cut -d'-' -f1) + end_port=$(echo $port | cut -d'-' -f2) + # Open the port range + ufw allow $start_port:$end_port/tcp + ufw allow $start_port:$end_port/udp + else + # Open the single port + ufw allow "$port" + fi + done + + # Confirm that the ports are opened + echo "Opened the specified ports:" + for port in "${PORT_LIST[@]}"; do + if [[ $port == *-* ]]; then + start_port=$(echo $port | cut -d'-' -f1) + end_port=$(echo $port | cut -d'-' -f2) + # Check if the port range has been successfully opened + (ufw status | grep -q "$start_port:$end_port") && echo "$start_port-$end_port" + else + # Check if the individual port has been successfully opened + (ufw status | grep -q "$port") && echo "$port" + fi + done +} + +delete_ports() { + # Display current rules with numbers + echo "Current UFW rules:" + ufw status numbered + + # Ask the user how they want to delete rules + echo "Do you want to delete rules by:" + echo "1) Rule numbers" + echo "2) Ports" + read -rp "Enter your choice (1 or 2): " choice + + if [[ $choice -eq 1 ]]; then + # Deleting by rule numbers + read -rp "Enter the rule numbers you want to delete (1, 2, etc.): " rule_numbers + + # Validate the input + if ! [[ $rule_numbers =~ ^([0-9]+)(,[0-9]+)*$ ]]; then + echo "Error: Invalid input. Please enter a comma-separated list of rule numbers." >&2 + exit 1 + fi + + # Split numbers into an array + IFS=',' read -ra RULE_NUMBERS <<<"$rule_numbers" + for rule_number in "${RULE_NUMBERS[@]}"; do + # Delete the rule by number + ufw delete "$rule_number" || echo "Failed to delete rule number $rule_number" + done + + echo "Selected rules have been deleted." + + elif [[ $choice -eq 2 ]]; then + # Deleting by ports + read -rp "Enter the ports you want to delete (e.g. 80,443,2053 or range 400-500): " ports + + # Validate the input + if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then + echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2 + exit 1 + fi + + # Split ports into an array + IFS=',' read -ra PORT_LIST <<<"$ports" + for port in "${PORT_LIST[@]}"; do + if [[ $port == *-* ]]; then + # Split the port range + start_port=$(echo $port | cut -d'-' -f1) + end_port=$(echo $port | cut -d'-' -f2) + # Delete the port range + ufw delete allow $start_port:$end_port/tcp + ufw delete allow $start_port:$end_port/udp + else + # Delete a single port + ufw delete allow "$port" + fi + done + + # Confirmation of deletion + echo "Deleted the specified ports:" + for port in "${PORT_LIST[@]}"; do + if [[ $port == *-* ]]; then + start_port=$(echo $port | cut -d'-' -f1) + end_port=$(echo $port | cut -d'-' -f2) + # Check if the port range has been deleted + (ufw status | grep -q "$start_port:$end_port") || echo "$start_port-$end_port" + else + # Check if the individual port has been deleted + (ufw status | grep -q "$port") || echo "$port" + fi + done + else + echo "${red}Error:${plain} Invalid choice. Please enter 1 or 2." >&2 + exit 1 + fi +} diff --git a/lib/geo.sh b/lib/geo.sh new file mode 100644 index 00000000..0280d691 --- /dev/null +++ b/lib/geo.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# lib/geo.sh - Geo files management + +# Include guard +[[ -n "${__X_UI_GEO_INCLUDED:-}" ]] && return 0 +__X_UI_GEO_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" +source "${LIB_DIR}/service.sh" + +update_all_geofiles() { + update_geofiles "main" + update_geofiles "IR" + update_geofiles "RU" +} + +update_geofiles() { + case "${1}" in + "main") dat_files=(geoip geosite); dat_source="Loyalsoldier/v2ray-rules-dat";; + "IR") dat_files=(geoip_IR geosite_IR); dat_source="chocolate4u/Iran-v2ray-rules" ;; + "RU") dat_files=(geoip_RU geosite_RU); dat_source="runetfreedom/russia-v2ray-rules-dat";; + esac + for dat in "${dat_files[@]}"; do + curl -fLRo ${xui_folder}/bin/${dat}.dat -z ${xui_folder}/bin/${dat}.dat \ + https://github.com/${dat_source}/releases/latest/download/${dat%%_}.dat + done +} + +update_geo() { + echo -e "${green}\t1.${plain} Loyalsoldier (geoip.dat, geosite.dat)" + echo -e "${green}\t2.${plain} chocolate4u (geoip_IR.dat, geosite_IR.dat)" + echo -e "${green}\t3.${plain} runetfreedom (geoip_RU.dat, geosite_RU.dat)" + echo -e "${green}\t4.${plain} All" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -rp "Choose an option: " choice + + case "$choice" in + 0) + show_menu + ;; + 1) + update_geofiles "main" + echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}" + restart + ;; + 2) + update_geofiles "IR" + echo -e "${green}chocolate4u datasets have been updated successfully!${plain}" + restart + ;; + 3) + update_geofiles "RU" + echo -e "${green}runetfreedom datasets have been updated successfully!${plain}" + restart + ;; + 4) + update_all_geofiles + echo -e "${green}All geo files have been updated successfully!${plain}" + restart + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + update_geo + ;; + esac + + before_show_menu +} diff --git a/lib/install.sh b/lib/install.sh new file mode 100644 index 00000000..f7e5b2f9 --- /dev/null +++ b/lib/install.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# lib/install.sh - Install, update, and uninstall functions + +# Include guard +[[ -n "${__X_UI_INSTALL_INCLUDED:-}" ]] && return 0 +__X_UI_INSTALL_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" +source "${LIB_DIR}/service.sh" + +install() { + bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/install.sh) + if [[ $? == 0 ]]; then + if [[ $# == 0 ]]; then + start + else + start 0 + fi + fi +} + +update() { + confirm "This function will update all x-ui components to the latest version, and the data will not be lost. Do you want to continue?" "y" + if [[ $? != 0 ]]; then + LOGE "Cancelled" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 0 + fi + bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/update.sh) + if [[ $? == 0 ]]; then + LOGI "Update is complete, Panel has automatically restarted " + before_show_menu + fi +} + +update_menu() { + echo -e "${yellow}Updating Menu${plain}" + confirm "This function will update the menu to the latest changes." "y" + if [[ $? != 0 ]]; then + LOGE "Cancelled" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 0 + fi + + curl -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh + chmod +x ${xui_folder}/x-ui.sh + chmod +x /usr/bin/x-ui + + if [[ $? == 0 ]]; then + echo -e "${green}Update successful. The panel has automatically restarted.${plain}" + exit 0 + else + echo -e "${red}Failed to update the menu.${plain}" + return 1 + fi +} + +legacy_version() { + echo -n "Enter the panel version (like 2.4.0):" + read -r tag_version + + if [ -z "$tag_version" ]; then + echo "Panel version cannot be empty. Exiting." + exit 1 + fi + # Use the entered panel version in the download link + install_command="bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/v$tag_version/install.sh") v$tag_version" + + echo "Downloading and installing panel version $tag_version..." + eval $install_command +} + +# Function to handle the deletion of the script file +delete_script() { + rm "$0" # Remove the script file itself + exit 1 +} + +uninstall() { + confirm "Are you sure you want to uninstall the panel? xray will also uninstalled!" "n" + if [[ $? != 0 ]]; then + if [[ $# == 0 ]]; then + show_menu + fi + return 0 + fi + + if [[ $release == "alpine" ]]; then + rc-service x-ui stop + rc-update del x-ui + rm /etc/init.d/x-ui -f + else + systemctl stop x-ui + systemctl disable x-ui + rm ${xui_service}/x-ui.service -f + systemctl daemon-reload + systemctl reset-failed + fi + + rm /etc/x-ui/ -rf + rm ${xui_folder}/ -rf + + echo "" + echo -e "Uninstalled Successfully.\n" + echo "If you need to install this panel again, you can use below command:" + echo -e "${green}bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)${plain}" + echo "" + # Trap the SIGTERM signal + trap delete_script SIGTERM + delete_script +} + +update_shell() { + curl -fLRo /usr/bin/x-ui -z /usr/bin/x-ui https://github.com/MHSanaei/3x-ui/raw/main/x-ui.sh + if [[ $? != 0 ]]; then + echo "" + LOGE "Failed to download script, Please check whether the machine can connect Github" + before_show_menu + else + chmod +x /usr/bin/x-ui + LOGI "Upgrade script succeeded, Please rerun the script" + before_show_menu + fi +} diff --git a/lib/iplimit.sh b/lib/iplimit.sh new file mode 100644 index 00000000..1fdd2d08 --- /dev/null +++ b/lib/iplimit.sh @@ -0,0 +1,398 @@ +#!/bin/bash +# lib/iplimit.sh - Fail2ban IP limiting management + +# Include guard +[[ -n "${__X_UI_IPLIMIT_INCLUDED:-}" ]] && return 0 +__X_UI_IPLIMIT_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" +source "${LIB_DIR}/service.sh" + +ip_validation() { + ipv6_regex="^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$" + ipv4_regex="^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)$" +} + +iplimit_main() { + echo -e "\n${green}\t1.${plain} Install Fail2ban and configure IP Limit" + echo -e "${green}\t2.${plain} Change Ban Duration" + echo -e "${green}\t3.${plain} Unban Everyone" + echo -e "${green}\t4.${plain} Ban Logs" + echo -e "${green}\t5.${plain} Ban an IP Address" + echo -e "${green}\t6.${plain} Unban an IP Address" + echo -e "${green}\t7.${plain} Real-Time Logs" + echo -e "${green}\t8.${plain} Service Status" + echo -e "${green}\t9.${plain} Service Restart" + echo -e "${green}\t10.${plain} Uninstall Fail2ban and IP Limit" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -rp "Choose an option: " choice + case "$choice" in + 0) + show_menu + ;; + 1) + confirm "Proceed with installation of Fail2ban & IP Limit?" "y" + if [[ $? == 0 ]]; then + install_iplimit + else + iplimit_main + fi + ;; + 2) + read -rp "Please enter new Ban Duration in Minutes [default 30]: " NUM + if [[ $NUM =~ ^[0-9]+$ ]]; then + create_iplimit_jails ${NUM} + if [[ $release == "alpine" ]]; then + rc-service fail2ban restart + else + systemctl restart fail2ban + fi + else + echo -e "${red}${NUM} is not a number! Please, try again.${plain}" + fi + iplimit_main + ;; + 3) + confirm "Proceed with Unbanning everyone from IP Limit jail?" "y" + if [[ $? == 0 ]]; then + fail2ban-client reload --restart --unban 3x-ipl + truncate -s 0 "${iplimit_banned_log_path}" + echo -e "${green}All users Unbanned successfully.${plain}" + iplimit_main + else + echo -e "${yellow}Cancelled.${plain}" + fi + iplimit_main + ;; + 4) + show_banlog + iplimit_main + ;; + 5) + read -rp "Enter the IP address you want to ban: " ban_ip + ip_validation + if [[ $ban_ip =~ $ipv4_regex || $ban_ip =~ $ipv6_regex ]]; then + fail2ban-client set 3x-ipl banip "$ban_ip" + echo -e "${green}IP Address ${ban_ip} has been banned successfully.${plain}" + else + echo -e "${red}Invalid IP address format! Please try again.${plain}" + fi + iplimit_main + ;; + 6) + read -rp "Enter the IP address you want to unban: " unban_ip + ip_validation + if [[ $unban_ip =~ $ipv4_regex || $unban_ip =~ $ipv6_regex ]]; then + fail2ban-client set 3x-ipl unbanip "$unban_ip" + echo -e "${green}IP Address ${unban_ip} has been unbanned successfully.${plain}" + else + echo -e "${red}Invalid IP address format! Please try again.${plain}" + fi + iplimit_main + ;; + 7) + tail -f /var/log/fail2ban.log + iplimit_main + ;; + 8) + service fail2ban status + iplimit_main + ;; + 9) + if [[ $release == "alpine" ]]; then + rc-service fail2ban restart + else + systemctl restart fail2ban + fi + iplimit_main + ;; + 10) + remove_iplimit + iplimit_main + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + iplimit_main + ;; + esac +} + +install_iplimit() { + if ! command -v fail2ban-client &>/dev/null; then + echo -e "${green}Fail2ban is not installed. Installing now...!${plain}\n" + + # Check the OS and install necessary packages + case "${release}" in + ubuntu) + apt-get update + if [[ "${os_version}" -ge 24 ]]; then + apt-get install python3-pip -y + python3 -m pip install pyasynchat --break-system-packages + fi + apt-get install fail2ban -y + ;; + debian) + apt-get update + if [ "$os_version" -ge 12 ]; then + apt-get install -y python3-systemd + fi + apt-get install -y fail2ban + ;; + armbian) + apt-get update && apt-get install fail2ban -y + ;; + fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) + dnf -y update && dnf -y install fail2ban + ;; + centos) + if [[ "${VERSION_ID}" =~ ^7 ]]; then + yum update -y && yum install epel-release -y + yum -y install fail2ban + else + dnf -y update && dnf -y install fail2ban + fi + ;; + arch | manjaro | parch) + pacman -Syu --noconfirm fail2ban + ;; + alpine) + apk add fail2ban + ;; + *) + echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" + exit 1 + ;; + esac + + if ! command -v fail2ban-client &>/dev/null; then + echo -e "${red}Fail2ban installation failed.${plain}\n" + exit 1 + fi + + echo -e "${green}Fail2ban installed successfully!${plain}\n" + else + echo -e "${yellow}Fail2ban is already installed.${plain}\n" + fi + + echo -e "${green}Configuring IP Limit...${plain}\n" + + # make sure there's no conflict for jail files + iplimit_remove_conflicts + + # Check if log file exists + if ! test -f "${iplimit_banned_log_path}"; then + touch ${iplimit_banned_log_path} + fi + + # Check if service log file exists so fail2ban won't return error + if ! test -f "${iplimit_log_path}"; then + touch ${iplimit_log_path} + fi + + # Create the iplimit jail files + # we didn't pass the bantime here to use the default value + create_iplimit_jails + + # Launching fail2ban + if [[ $release == "alpine" ]]; then + if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then + rc-service fail2ban start + else + rc-service fail2ban restart + fi + rc-update add fail2ban + else + if ! systemctl is-active --quiet fail2ban; then + systemctl start fail2ban + else + systemctl restart fail2ban + fi + systemctl enable fail2ban + fi + + echo -e "${green}IP Limit installed and configured successfully!${plain}\n" + before_show_menu +} + +remove_iplimit() { + echo -e "${green}\t1.${plain} Only remove IP Limit configurations" + echo -e "${green}\t2.${plain} Uninstall Fail2ban and IP Limit" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -rp "Choose an option: " num + case "$num" in + 1) + rm -f /etc/fail2ban/filter.d/3x-ipl.conf + rm -f /etc/fail2ban/action.d/3x-ipl.conf + rm -f /etc/fail2ban/jail.d/3x-ipl.conf + if [[ $release == "alpine" ]]; then + rc-service fail2ban restart + else + systemctl restart fail2ban + fi + echo -e "${green}IP Limit removed successfully!${plain}\n" + before_show_menu + ;; + 2) + rm -rf /etc/fail2ban + if [[ $release == "alpine" ]]; then + rc-service fail2ban stop + else + systemctl stop fail2ban + fi + case "${release}" in + ubuntu | debian | armbian) + apt-get remove -y fail2ban + apt-get purge -y fail2ban -y + apt-get autoremove -y + ;; + fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) + dnf remove fail2ban -y + dnf autoremove -y + ;; + centos) + if [[ "${VERSION_ID}" =~ ^7 ]]; then + yum remove fail2ban -y + yum autoremove -y + else + dnf remove fail2ban -y + dnf autoremove -y + fi + ;; + arch | manjaro | parch) + pacman -Rns --noconfirm fail2ban + ;; + alpine) + apk del fail2ban + ;; + *) + echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n" + exit 1 + ;; + esac + echo -e "${green}Fail2ban and IP Limit removed successfully!${plain}\n" + before_show_menu + ;; + 0) + show_menu + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + remove_iplimit + ;; + esac +} + +show_banlog() { + local system_log="/var/log/fail2ban.log" + + echo -e "${green}Checking ban logs...${plain}\n" + + if [[ $release == "alpine" ]]; then + if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then + echo -e "${red}Fail2ban service is not running!${plain}\n" + return 1 + fi + else + if ! systemctl is-active --quiet fail2ban; then + echo -e "${red}Fail2ban service is not running!${plain}\n" + return 1 + fi + fi + + if [[ -f "$system_log" ]]; then + echo -e "${green}Recent system ban activities from fail2ban.log:${plain}" + grep "3x-ipl" "$system_log" | grep -E "Ban|Unban" | tail -n 10 || echo -e "${yellow}No recent system ban activities found${plain}" + echo "" + fi + + if [[ -f "${iplimit_banned_log_path}" ]]; then + echo -e "${green}3X-IPL ban log entries:${plain}" + if [[ -s "${iplimit_banned_log_path}" ]]; then + grep -v "INIT" "${iplimit_banned_log_path}" | tail -n 10 || echo -e "${yellow}No ban entries found${plain}" + else + echo -e "${yellow}Ban log file is empty${plain}" + fi + else + echo -e "${red}Ban log file not found at: ${iplimit_banned_log_path}${plain}" + fi + + echo -e "\n${green}Current jail status:${plain}" + fail2ban-client status 3x-ipl || echo -e "${yellow}Unable to get jail status${plain}" +} + +create_iplimit_jails() { + # Use default bantime if not passed => 30 minutes + local bantime="${1:-30}" + + # Uncomment 'allowipv6 = auto' in fail2ban.conf + sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf + + # On Debian 12+ fail2ban's default backend should be changed to systemd + if [[ "${release}" == "debian" && ${os_version} -ge 12 ]]; then + sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf + fi + + cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf +[3x-ipl] +enabled=true +backend=auto +filter=3x-ipl +action=3x-ipl +logpath=${iplimit_log_path} +maxretry=2 +findtime=32 +bantime=${bantime}m +EOF + + cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf +[Definition] +datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S +failregex = \[LIMIT_IP\]\s*Email\s*=\s*.+\s*\|\|\s*SRC\s*=\s* +ignoreregex = +EOF + + cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf +[INCLUDES] +before = iptables-allports.conf + +[Definition] +actionstart = -N f2b- + -A f2b- -j + -I -p -j f2b- + +actionstop = -D -p -j f2b- + + -X f2b- + +actioncheck = -n -L | grep -q 'f2b-[ \t]' + +actionban = -I f2b- 1 -s -j + echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = [IP] = banned for seconds." >> ${iplimit_banned_log_path} + +actionunban = -D f2b- -s -j + echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = [IP] = unbanned." >> ${iplimit_banned_log_path} + +[Init] +name = default +protocol = tcp +chain = INPUT +EOF + + echo -e "${green}Ip Limit jail files created with a bantime of ${bantime} minutes.${plain}" +} + +iplimit_remove_conflicts() { + local jail_files=( + /etc/fail2ban/jail.conf + /etc/fail2ban/jail.local + ) + + for file in "${jail_files[@]}"; do + # Check for [3x-ipl] config in jail file then remove it + if test -f "${file}" && grep -qw '3x-ipl' ${file}; then + sed -i "/\[3x-ipl\]/,/^$/d" ${file} + echo -e "${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\n" + fi + done +} diff --git a/lib/service.sh b/lib/service.sh new file mode 100644 index 00000000..c1488300 --- /dev/null +++ b/lib/service.sh @@ -0,0 +1,287 @@ +#!/bin/bash +# lib/service.sh - Service control functions (start, stop, restart, status, enable, disable) + +# Include guard +[[ -n "${__X_UI_SERVICE_INCLUDED:-}" ]] && return 0 +__X_UI_SERVICE_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" + +# 0: running, 1: not running, 2: not installed +check_status() { + if [[ $release == "alpine" ]]; then + if [[ ! -f /etc/init.d/x-ui ]]; then + return 2 + fi + if [[ $(rc-service x-ui status | grep -F 'status: started' -c) == 1 ]]; then + return 0 + else + return 1 + fi + else + if [[ ! -f ${xui_service}/x-ui.service ]]; then + return 2 + fi + temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1) + if [[ "${temp}" == "running" ]]; then + return 0 + else + return 1 + fi + fi +} + +check_enabled() { + if [[ $release == "alpine" ]]; then + if [[ $(rc-update show | grep -F 'x-ui' | grep default -c) == 1 ]]; then + return 0 + else + return 1 + fi + else + temp=$(systemctl is-enabled x-ui) + if [[ "${temp}" == "enabled" ]]; then + return 0 + else + return 1 + fi + fi +} + +check_uninstall() { + check_status + if [[ $? != 2 ]]; then + echo "" + LOGE "Panel installed, Please do not reinstall" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 1 + else + return 0 + fi +} + +check_install() { + check_status + if [[ $? == 2 ]]; then + echo "" + LOGE "Please install the panel first" + if [[ $# == 0 ]]; then + before_show_menu + fi + return 1 + else + return 0 + fi +} + +show_status() { + check_status + case $? in + 0) + echo -e "Panel state: ${green}Running${plain}" + show_enable_status + ;; + 1) + echo -e "Panel state: ${yellow}Not Running${plain}" + show_enable_status + ;; + 2) + echo -e "Panel state: ${red}Not Installed${plain}" + ;; + esac + show_xray_status +} + +show_enable_status() { + check_enabled + if [[ $? == 0 ]]; then + echo -e "Start automatically: ${green}Yes${plain}" + else + echo -e "Start automatically: ${red}No${plain}" + fi +} + +check_xray_status() { + count=$(ps -ef | grep "xray-linux" | grep -v "grep" | wc -l) + if [[ count -ne 0 ]]; then + return 0 + else + return 1 + fi +} + +show_xray_status() { + check_xray_status + if [[ $? == 0 ]]; then + echo -e "xray state: ${green}Running${plain}" + else + echo -e "xray state: ${red}Not Running${plain}" + fi +} + +start() { + check_status + if [[ $? == 0 ]]; then + echo "" + LOGI "Panel is running, No need to start again, If you need to restart, please select restart" + else + if [[ $release == "alpine" ]]; then + rc-service x-ui start + else + systemctl start x-ui + fi + sleep 2 + check_status + if [[ $? == 0 ]]; then + LOGI "x-ui Started Successfully" + else + LOGE "panel Failed to start, Probably because it takes longer than two seconds to start, Please check the log information later" + fi + fi + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +stop() { + check_status + if [[ $? == 1 ]]; then + echo "" + LOGI "Panel stopped, No need to stop again!" + else + if [[ $release == "alpine" ]]; then + rc-service x-ui stop + else + systemctl stop x-ui + fi + sleep 2 + check_status + if [[ $? == 1 ]]; then + LOGI "x-ui and xray stopped successfully" + else + LOGE "Panel stop failed, Probably because the stop time exceeds two seconds, Please check the log information later" + fi + fi + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +restart() { + if [[ $release == "alpine" ]]; then + rc-service x-ui restart + else + systemctl restart x-ui + fi + sleep 2 + check_status + if [[ $? == 0 ]]; then + LOGI "x-ui and xray Restarted successfully" + else + LOGE "Panel restart failed, Probably because it takes longer than two seconds to start, Please check the log information later" + fi + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +status() { + if [[ $release == "alpine" ]]; then + rc-service x-ui status + else + systemctl status x-ui -l + fi + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +enable() { + if [[ $release == "alpine" ]]; then + rc-update add x-ui + else + systemctl enable x-ui + fi + if [[ $? == 0 ]]; then + LOGI "x-ui Set to boot automatically on startup successfully" + else + LOGE "x-ui Failed to set Autostart" + fi + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +disable() { + if [[ $release == "alpine" ]]; then + rc-update del x-ui + else + systemctl disable x-ui + fi + if [[ $? == 0 ]]; then + LOGI "x-ui Autostart Cancelled successfully" + else + LOGE "x-ui Failed to cancel autostart" + fi + + if [[ $# == 0 ]]; then + before_show_menu + fi +} + +show_log() { + if [[ $release == "alpine" ]]; then + echo -e "${green}\t1.${plain} Debug Log" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -rp "Choose an option: " choice + + case "$choice" in + 0) + show_menu + ;; + 1) + grep -F 'x-ui[' /var/log/messages + if [[ $# == 0 ]]; then + before_show_menu + fi + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + show_log + ;; + esac + else + echo -e "${green}\t1.${plain} Debug Log" + echo -e "${green}\t2.${plain} Clear All logs" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -rp "Choose an option: " choice + + case "$choice" in + 0) + show_menu + ;; + 1) + journalctl -u x-ui -e --no-pager -f -p debug + if [[ $# == 0 ]]; then + before_show_menu + fi + ;; + 2) + sudo journalctl --rotate + sudo journalctl --vacuum-time=1s + echo "All Logs cleared." + restart + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + show_log + ;; + esac + fi +} diff --git a/lib/settings.sh b/lib/settings.sh new file mode 100644 index 00000000..38ef3da2 --- /dev/null +++ b/lib/settings.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# lib/settings.sh - Panel settings management + +# Include guard +[[ -n "${__X_UI_SETTINGS_INCLUDED:-}" ]] && return 0 +__X_UI_SETTINGS_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" +source "${LIB_DIR}/service.sh" +source "${LIB_DIR}/ssl.sh" + +reset_user() { + confirm "Are you sure to reset the username and password of the panel?" "n" + if [[ $? != 0 ]]; then + if [[ $# == 0 ]]; then + show_menu + fi + return 0 + fi + + read -rp "Please set the login username [default is a random username]: " config_account + [[ -z $config_account ]] && config_account=$(gen_random_string 10) + read -rp "Please set the login password [default is a random password]: " config_password + [[ -z $config_password ]] && config_password=$(gen_random_string 18) + + read -rp "Do you want to disable currently configured two-factor authentication? (y/n): " twoFactorConfirm + if [[ $twoFactorConfirm != "y" && $twoFactorConfirm != "Y" ]]; then + ${xui_folder}/x-ui setting -username ${config_account} -password ${config_password} -resetTwoFactor false >/dev/null 2>&1 + else + ${xui_folder}/x-ui setting -username ${config_account} -password ${config_password} -resetTwoFactor true >/dev/null 2>&1 + echo -e "Two factor authentication has been disabled." + fi + + echo -e "Panel login username has been reset to: ${green} ${config_account} ${plain}" + echo -e "Panel login password has been reset to: ${green} ${config_password} ${plain}" + echo -e "${green} Please use the new login username and password to access the X-UI panel. Also remember them! ${plain}" + confirm_restart +} + +reset_webbasepath() { + echo -e "${yellow}Resetting Web Base Path${plain}" + + read -rp "Are you sure you want to reset the web base path? (y/n): " confirm + if [[ $confirm != "y" && $confirm != "Y" ]]; then + echo -e "${yellow}Operation canceled.${plain}" + return + fi + + config_webBasePath=$(gen_random_string 18) + + # Apply the new web base path setting + ${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}" >/dev/null 2>&1 + + echo -e "Web base path has been reset to: ${green}${config_webBasePath}${plain}" + echo -e "${green}Please use the new web base path to access the panel.${plain}" + restart +} + +reset_config() { + confirm "Are you sure you want to reset all panel settings, Account data will not be lost, Username and password will not change" "n" + if [[ $? != 0 ]]; then + if [[ $# == 0 ]]; then + show_menu + fi + return 0 + fi + ${xui_folder}/x-ui setting -reset + echo -e "All panel settings have been reset to default." + restart +} + +check_config() { + local info=$(${xui_folder}/x-ui setting -show true) + if [[ $? != 0 ]]; then + LOGE "get current settings error, please check logs" + show_menu + return + fi + LOGI "${info}" + + local existing_webBasePath=$(echo "$info" | grep -Eo 'webBasePath: .+' | awk '{print $2}') + local existing_port=$(echo "$info" | grep -Eo 'port: .+' | awk '{print $2}') + local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') + local server_ip=$(curl -s --max-time 3 https://api.ipify.org) + if [ -z "$server_ip" ]; then + server_ip=$(curl -s --max-time 3 https://4.ident.me) + fi + + if [[ -n "$existing_cert" ]]; then + local domain=$(basename "$(dirname "$existing_cert")") + + if [[ "$domain" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then + echo -e "${green}Access URL: https://${domain}:${existing_port}${existing_webBasePath}${plain}" + else + echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" + fi + else + echo -e "${red}⚠ WARNING: No SSL certificate configured!${plain}" + echo -e "${yellow}You can get a Let's Encrypt certificate for your IP address (valid ~6 days, auto-renews).${plain}" + read -rp "Generate SSL certificate for IP now? [y/N]: " gen_ssl + if [[ "$gen_ssl" == "y" || "$gen_ssl" == "Y" ]]; then + stop >/dev/null 2>&1 + ssl_cert_issue_for_ip + if [[ $? -eq 0 ]]; then + echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" + # ssl_cert_issue_for_ip already restarts the panel, but ensure it's running + start >/dev/null 2>&1 + else + LOGE "IP certificate setup failed." + echo -e "${yellow}You can try again via option 18 (SSL Certificate Management).${plain}" + start >/dev/null 2>&1 + fi + else + echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}" + echo -e "${yellow}For security, please configure SSL certificate using option 18 (SSL Certificate Management)${plain}" + fi + fi +} + +set_port() { + echo -n "Enter port number[1-65535]: " + read -r port + if [[ -z "${port}" ]]; then + LOGD "Cancelled" + before_show_menu + else + ${xui_folder}/x-ui setting -port ${port} + echo -e "The port is set, Please restart the panel now, and use the new port ${green}${port}${plain} to access web panel" + confirm_restart + fi +} diff --git a/lib/ssl.sh b/lib/ssl.sh new file mode 100644 index 00000000..5ba2bb19 --- /dev/null +++ b/lib/ssl.sh @@ -0,0 +1,628 @@ +#!/bin/bash +# lib/ssl.sh - SSL certificate management (acme.sh, Let's Encrypt, Cloudflare) + +# Include guard +[[ -n "${__X_UI_SSL_INCLUDED:-}" ]] && return 0 +__X_UI_SSL_INCLUDED=1 + +# Source dependencies +source "${LIB_DIR}/common.sh" +source "${LIB_DIR}/service.sh" + +install_acme() { + # Check if acme.sh is already installed + if command -v ~/.acme.sh/acme.sh &>/dev/null; then + LOGI "acme.sh is already installed." + return 0 + fi + + LOGI "Installing acme.sh..." + cd ~ || return 1 # Ensure you can change to the home directory + + curl -s https://get.acme.sh | sh + if [ $? -ne 0 ]; then + LOGE "Installation of acme.sh failed." + return 1 + else + LOGI "Installation of acme.sh succeeded." + fi + + return 0 +} + +ssl_cert_issue_main() { + echo -e "${green}\t1.${plain} Get SSL (Domain)" + echo -e "${green}\t2.${plain} Revoke" + echo -e "${green}\t3.${plain} Force Renew" + echo -e "${green}\t4.${plain} Show Existing Domains" + echo -e "${green}\t5.${plain} Set Cert paths for the panel" + echo -e "${green}\t6.${plain} Get SSL for IP Address (6-day cert, auto-renews)" + echo -e "${green}\t0.${plain} Back to Main Menu" + + read -rp "Choose an option: " choice + case "$choice" in + 0) + show_menu + ;; + 1) + ssl_cert_issue + ssl_cert_issue_main + ;; + 2) + local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) + if [ -z "$domains" ]; then + echo "No certificates found to revoke." + else + echo "Existing domains:" + echo "$domains" + read -rp "Please enter a domain from the list to revoke the certificate: " domain + if echo "$domains" | grep -qw "$domain"; then + ~/.acme.sh/acme.sh --revoke -d ${domain} + LOGI "Certificate revoked for domain: $domain" + else + echo "Invalid domain entered." + fi + fi + ssl_cert_issue_main + ;; + 3) + local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) + if [ -z "$domains" ]; then + echo "No certificates found to renew." + else + echo "Existing domains:" + echo "$domains" + read -rp "Please enter a domain from the list to renew the SSL certificate: " domain + if echo "$domains" | grep -qw "$domain"; then + ~/.acme.sh/acme.sh --renew -d ${domain} --force + LOGI "Certificate forcefully renewed for domain: $domain" + else + echo "Invalid domain entered." + fi + fi + ssl_cert_issue_main + ;; + 4) + local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) + if [ -z "$domains" ]; then + echo "No certificates found." + else + echo "Existing domains and their paths:" + for domain in $domains; do + local cert_path="/root/cert/${domain}/fullchain.pem" + local key_path="/root/cert/${domain}/privkey.pem" + if [[ -f "${cert_path}" && -f "${key_path}" ]]; then + echo -e "Domain: ${domain}" + echo -e "\tCertificate Path: ${cert_path}" + echo -e "\tPrivate Key Path: ${key_path}" + else + echo -e "Domain: ${domain} - Certificate or Key missing." + fi + done + fi + ssl_cert_issue_main + ;; + 5) + local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) + if [ -z "$domains" ]; then + echo "No certificates found." + else + echo "Available domains:" + echo "$domains" + read -rp "Please choose a domain to set the panel paths: " domain + + if echo "$domains" | grep -qw "$domain"; then + local webCertFile="/root/cert/${domain}/fullchain.pem" + local webKeyFile="/root/cert/${domain}/privkey.pem" + + if [[ -f "${webCertFile}" && -f "${webKeyFile}" ]]; then + ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + echo "Panel paths set for domain: $domain" + echo " - Certificate File: $webCertFile" + echo " - Private Key File: $webKeyFile" + restart + else + echo "Certificate or private key not found for domain: $domain." + fi + else + echo "Invalid domain entered." + fi + fi + ssl_cert_issue_main + ;; + 6) + echo -e "${yellow}Let's Encrypt SSL Certificate for IP Address${plain}" + echo -e "This will obtain a certificate for your server's IP using the shortlived profile." + echo -e "${yellow}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}" + echo -e "${yellow}Port 80 must be open and accessible from the internet.${plain}" + confirm "Do you want to proceed?" "y" + if [[ $? == 0 ]]; then + ssl_cert_issue_for_ip + fi + ssl_cert_issue_main + ;; + + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + ssl_cert_issue_main + ;; + esac +} + +ssl_cert_issue_for_ip() { + LOGI "Starting automatic SSL certificate generation for server IP..." + LOGI "Using Let's Encrypt shortlived profile (~6 days validity, auto-renews)" + + local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') + local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') + + # Get server IP + local server_ip=$(curl -s --max-time 3 https://api.ipify.org) + if [ -z "$server_ip" ]; then + server_ip=$(curl -s --max-time 3 https://4.ident.me) + fi + + if [ -z "$server_ip" ]; then + LOGE "Failed to get server IP address" + return 1 + fi + + LOGI "Server IP detected: ${server_ip}" + + # Ask for optional IPv6 + local ipv6_addr="" + read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr + ipv6_addr="${ipv6_addr// /}" # Trim whitespace + + # check for acme.sh first + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + LOGI "acme.sh not found, installing..." + install_acme + if [ $? -ne 0 ]; then + LOGE "Failed to install acme.sh" + return 1 + fi + fi + + # install socat + case "${release}" in + ubuntu | debian | armbian) + apt-get update >/dev/null 2>&1 && apt-get install socat -y >/dev/null 2>&1 + ;; + fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) + dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 + ;; + centos) + if [[ "${VERSION_ID}" =~ ^7 ]]; then + yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1 + else + dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 + fi + ;; + arch | manjaro | parch) + pacman -Sy --noconfirm socat >/dev/null 2>&1 + ;; + opensuse-tumbleweed | opensuse-leap) + zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1 + ;; + alpine) + apk add socat curl openssl >/dev/null 2>&1 + ;; + *) + LOGW "Unsupported OS for automatic socat installation" + ;; + esac + + # Create certificate directory + certPath="/root/cert/ip" + mkdir -p "$certPath" + + # Build domain arguments + local domain_args="-d ${server_ip}" + if [[ -n "$ipv6_addr" ]] && is_ipv6 "$ipv6_addr"; then + domain_args="${domain_args} -d ${ipv6_addr}" + LOGI "Including IPv6 address: ${ipv6_addr}" + fi + + # Use port 80 for certificate issuance + local WebPort=80 + LOGI "Using port ${WebPort} to issue certificate for IP: ${server_ip}" + LOGI "Make sure port ${WebPort} is open and not in use..." + + # Reload command - restarts panel after renewal + local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null" + + # issue the certificate for IP with shortlived profile + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt + ~/.acme.sh/acme.sh --issue \ + ${domain_args} \ + --standalone \ + --server letsencrypt \ + --certificate-profile shortlived \ + --days 6 \ + --httpport ${WebPort} \ + --force + + if [ $? -ne 0 ]; then + LOGE "Failed to issue certificate for IP: ${server_ip}" + LOGE "Make sure port ${WebPort} is open and the server is accessible from the internet" + # Cleanup acme.sh data for both IPv4 and IPv6 if specified + rm -rf ~/.acme.sh/${server_ip} 2>/dev/null + [[ -n "$ipv6_addr" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null + rm -rf ${certPath} 2>/dev/null + return 1 + else + LOGI "Certificate issued successfully for IP: ${server_ip}" + fi + + # Install the certificate + # Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails, + # but the cert files are still installed. We check for files instead of exit code. + ~/.acme.sh/acme.sh --installcert -d ${server_ip} \ + --key-file "${certPath}/privkey.pem" \ + --fullchain-file "${certPath}/fullchain.pem" \ + --reloadcmd "${reloadCmd}" 2>&1 || true + + # Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero) + if [[ ! -f "${certPath}/fullchain.pem" || ! -f "${certPath}/privkey.pem" ]]; then + LOGE "Certificate files not found after installation" + # Cleanup acme.sh data for both IPv4 and IPv6 if specified + rm -rf ~/.acme.sh/${server_ip} 2>/dev/null + [[ -n "$ipv6_addr" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null + rm -rf ${certPath} 2>/dev/null + return 1 + fi + + LOGI "Certificate files installed successfully" + + # enable auto-renew + ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 + chmod 600 $certPath/privkey.pem 2>/dev/null + chmod 644 $certPath/fullchain.pem 2>/dev/null + + # Set certificate paths for the panel + local webCertFile="${certPath}/fullchain.pem" + local webKeyFile="${certPath}/privkey.pem" + + if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then + ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + LOGI "Certificate configured for panel" + LOGI " - Certificate File: $webCertFile" + LOGI " - Private Key File: $webKeyFile" + LOGI " - Validity: ~6 days (auto-renews via acme.sh cron)" + echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" + LOGI "Panel will restart to apply SSL certificate..." + restart + return 0 + else + LOGE "Certificate files not found after installation" + return 1 + fi +} + +ssl_cert_issue() { + local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') + local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') + # check for acme.sh first + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + echo "acme.sh could not be found. we will install it" + install_acme + if [ $? -ne 0 ]; then + LOGE "install acme failed, please check logs" + exit 1 + fi + fi + + # install socat + case "${release}" in + ubuntu | debian | armbian) + apt-get update >/dev/null 2>&1 && apt-get install socat -y >/dev/null 2>&1 + ;; + fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) + dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 + ;; + centos) + if [[ "${VERSION_ID}" =~ ^7 ]]; then + yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1 + else + dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 + fi + ;; + arch | manjaro | parch) + pacman -Sy --noconfirm socat >/dev/null 2>&1 + ;; + opensuse-tumbleweed | opensuse-leap) + zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1 + ;; + alpine) + apk add socat curl openssl >/dev/null 2>&1 + ;; + *) + LOGW "Unsupported OS for automatic socat installation" + ;; + esac + if [ $? -ne 0 ]; then + LOGE "install socat failed, please check logs" + exit 1 + else + LOGI "install socat succeed..." + fi + + # get the domain here, and we need to verify it + local domain="" + while true; do + read -rp "Please enter your domain name: " domain + domain="${domain// /}" # Trim whitespace + + if [[ -z "$domain" ]]; then + LOGE "Domain name cannot be empty. Please try again." + continue + fi + + if ! is_domain "$domain"; then + LOGE "Invalid domain format: ${domain}. Please enter a valid domain name." + continue + fi + + break + done + LOGD "Your domain is: ${domain}, checking it..." + + # check if there already exists a certificate + local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') + if [ "${currentCert}" == "${domain}" ]; then + local certInfo=$(~/.acme.sh/acme.sh --list) + LOGE "System already has certificates for this domain. Cannot issue again. Current certificate details:" + LOGI "$certInfo" + exit 1 + else + LOGI "Your domain is ready for issuing certificates now..." + fi + + # create a directory for the certificate + certPath="/root/cert/${domain}" + if [ ! -d "$certPath" ]; then + mkdir -p "$certPath" + else + rm -rf "$certPath" + mkdir -p "$certPath" + fi + + # get the port number for the standalone server + local WebPort=80 + read -rp "Please choose which port to use (default is 80): " WebPort + if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then + LOGE "Your input ${WebPort} is invalid, will use default port 80." + WebPort=80 + fi + LOGI "Will use port: ${WebPort} to issue certificates. Please make sure this port is open." + + # issue the certificate + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt + ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force + if [ $? -ne 0 ]; then + LOGE "Issuing certificate failed, please check logs." + rm -rf ~/.acme.sh/${domain} + exit 1 + else + LOGE "Issuing certificate succeeded, installing certificates..." + fi + + reloadCmd="x-ui restart" + + LOGI "Default --reloadcmd for ACME is: ${yellow}x-ui restart" + LOGI "This command will run on every certificate issue and renew." + read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd + if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then + echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; x-ui restart" + echo -e "${green}\t2.${plain} Input your own command" + echo -e "${green}\t0.${plain} Keep default reloadcmd" + read -rp "Choose an option: " choice + case "$choice" in + 1) + LOGI "Reloadcmd is: systemctl reload nginx ; x-ui restart" + reloadCmd="systemctl reload nginx ; x-ui restart" + ;; + 2) + LOGD "It's recommended to put x-ui restart at the end, so it won't raise an error if other services fails" + read -rp "Please enter your reloadcmd (example: systemctl reload nginx ; x-ui restart): " reloadCmd + LOGI "Your reloadcmd is: ${reloadCmd}" + ;; + *) + LOGI "Keep default reloadcmd" + ;; + esac + fi + + # install the certificate + ~/.acme.sh/acme.sh --installcert -d ${domain} \ + --key-file /root/cert/${domain}/privkey.pem \ + --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" + + if [ $? -ne 0 ]; then + LOGE "Installing certificate failed, exiting." + rm -rf ~/.acme.sh/${domain} + exit 1 + else + LOGI "Installing certificate succeeded, enabling auto renew..." + fi + + # enable auto-renew + ~/.acme.sh/acme.sh --upgrade --auto-upgrade + if [ $? -ne 0 ]; then + LOGE "Auto renew failed, certificate details:" + ls -lah cert/* + chmod 600 $certPath/privkey.pem + chmod 644 $certPath/fullchain.pem + exit 1 + else + LOGI "Auto renew succeeded, certificate details:" + ls -lah cert/* + chmod 600 $certPath/privkey.pem + chmod 644 $certPath/fullchain.pem + fi + + # Prompt user to set panel paths after successful certificate installation + read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel + if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then + local webCertFile="/root/cert/${domain}/fullchain.pem" + local webKeyFile="/root/cert/${domain}/privkey.pem" + + if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then + ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + LOGI "Panel paths set for domain: $domain" + LOGI " - Certificate File: $webCertFile" + LOGI " - Private Key File: $webKeyFile" + echo -e "${green}Access URL: https://${domain}:${existing_port}${existing_webBasePath}${plain}" + restart + else + LOGE "Error: Certificate or private key file not found for domain: $domain." + fi + else + LOGI "Skipping panel path setting." + fi +} + +ssl_cert_issue_CF() { + local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') + local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') + LOGI "****** Instructions for Use ******" + LOGI "Follow the steps below to complete the process:" + LOGI "1. Cloudflare Registered E-mail." + LOGI "2. Cloudflare Global API Key." + LOGI "3. The Domain Name." + LOGI "4. Once the certificate is issued, you will be prompted to set the certificate for the panel (optional)." + LOGI "5. The script also supports automatic renewal of the SSL certificate after installation." + + confirm "Do you confirm the information and wish to proceed? [y/n]" "y" + + if [ $? -eq 0 ]; then + # Check for acme.sh first + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + echo "acme.sh could not be found. We will install it." + install_acme + if [ $? -ne 0 ]; then + LOGE "Install acme failed, please check logs." + exit 1 + fi + fi + + CF_Domain="" + + LOGD "Please set a domain name:" + read -rp "Input your domain here: " CF_Domain + LOGD "Your domain name is set to: ${CF_Domain}" + + # Set up Cloudflare API details + CF_GlobalKey="" + CF_AccountEmail="" + LOGD "Please set the API key:" + read -rp "Input your key here: " CF_GlobalKey + LOGD "Your API key is: ${CF_GlobalKey}" + + LOGD "Please set up registered email:" + read -rp "Input your email here: " CF_AccountEmail + LOGD "Your registered email address is: ${CF_AccountEmail}" + + # Set the default CA to Let's Encrypt + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt + if [ $? -ne 0 ]; then + LOGE "Default CA, Let'sEncrypt fail, script exiting..." + exit 1 + fi + + export CF_Key="${CF_GlobalKey}" + export CF_Email="${CF_AccountEmail}" + + # Issue the certificate using Cloudflare DNS + ~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log --force + if [ $? -ne 0 ]; then + LOGE "Certificate issuance failed, script exiting..." + exit 1 + else + LOGI "Certificate issued successfully, Installing..." + fi + + # Install the certificate + certPath="/root/cert/${CF_Domain}" + if [ -d "$certPath" ]; then + rm -rf ${certPath} + fi + + mkdir -p ${certPath} + if [ $? -ne 0 ]; then + LOGE "Failed to create directory: ${certPath}" + exit 1 + fi + + reloadCmd="x-ui restart" + + LOGI "Default --reloadcmd for ACME is: ${yellow}x-ui restart" + LOGI "This command will run on every certificate issue and renew." + read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd + if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then + echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; x-ui restart" + echo -e "${green}\t2.${plain} Input your own command" + echo -e "${green}\t0.${plain} Keep default reloadcmd" + read -rp "Choose an option: " choice + case "$choice" in + 1) + LOGI "Reloadcmd is: systemctl reload nginx ; x-ui restart" + reloadCmd="systemctl reload nginx ; x-ui restart" + ;; + 2) + LOGD "It's recommended to put x-ui restart at the end, so it won't raise an error if other services fails" + read -rp "Please enter your reloadcmd (example: systemctl reload nginx ; x-ui restart): " reloadCmd + LOGI "Your reloadcmd is: ${reloadCmd}" + ;; + *) + LOGI "Keep default reloadcmd" + ;; + esac + fi + ~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \ + --key-file ${certPath}/privkey.pem \ + --fullchain-file ${certPath}/fullchain.pem --reloadcmd "${reloadCmd}" + + if [ $? -ne 0 ]; then + LOGE "Certificate installation failed, script exiting..." + exit 1 + else + LOGI "Certificate installed successfully, Turning on automatic updates..." + fi + + # Enable auto-update + ~/.acme.sh/acme.sh --upgrade --auto-upgrade + if [ $? -ne 0 ]; then + LOGE "Auto update setup failed, script exiting..." + exit 1 + else + LOGI "The certificate is installed and auto-renewal is turned on. Specific information is as follows:" + ls -lah ${certPath}/* + chmod 600 ${certPath}/privkey.pem + chmod 644 ${certPath}/fullchain.pem + fi + + # Prompt user to set panel paths after successful certificate installation + read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel + if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then + local webCertFile="${certPath}/fullchain.pem" + local webKeyFile="${certPath}/privkey.pem" + + if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then + ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + LOGI "Panel paths set for domain: $CF_Domain" + LOGI " - Certificate File: $webCertFile" + LOGI " - Private Key File: $webKeyFile" + echo -e "${green}Access URL: https://${CF_Domain}:${existing_port}${existing_webBasePath}${plain}" + restart + else + LOGE "Error: Certificate or private key file not found for domain: $CF_Domain." + fi + else + LOGI "Skipping panel path setting." + fi + else + show_menu + fi +} diff --git a/x-ui.sh b/x-ui.sh index 07aaddc6..340d456a 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -1,97 +1,37 @@ #!/bin/bash +# x-ui.sh - 3X-UI Panel Management Script (Entrypoint) +# This is the main entrypoint that sources modular library files -red='\033[0;31m' -green='\033[0;32m' -blue='\033[0;34m' -yellow='\033[0;33m' -plain='\033[0m' +# Resolve the actual script location (handles symlinks) +#SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")" +#SCRIPT_DIR="$(dirname "$SCRIPT_PATH")" +#LIB_DIR="${SCRIPT_DIR}/lib" +# +## Fallback for installed location +#[[ ! -d "$LIB_DIR" ]] && LIB_DIR="/usr/local/x-ui/lib" -#Add some basic function here -function LOGD() { - echo -e "${yellow}[DEG] $* ${plain}" -} +LIB_DIR="${LIB_DIR:=/usr/local/x-ui/lib}" +# Export LIB_DIR for use by library files +export LIB_DIR -function LOGE() { - echo -e "${red}[ERR] $* ${plain}" -} +# Source all library files +source "${LIB_DIR}/common.sh" +source "${LIB_DIR}/service.sh" +source "${LIB_DIR}/ssl.sh" +source "${LIB_DIR}/settings.sh" +source "${LIB_DIR}/firewall.sh" +source "${LIB_DIR}/iplimit.sh" +source "${LIB_DIR}/bbr.sh" +source "${LIB_DIR}/geo.sh" +source "${LIB_DIR}/install.sh" +source "${LIB_DIR}/extras.sh" -function LOGI() { - echo -e "${green}[INF] $* ${plain}" -} - -# Port helpers: detect listener and owning process (best effort) -is_port_in_use() { - local port="$1" - if command -v ss >/dev/null 2>&1; then - ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}' - return - fi - if command -v netstat >/dev/null 2>&1; then - netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}' - return - fi - if command -v lsof >/dev/null 2>&1; then - lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0 - fi - return 1 -} - -# Simple helpers for domain/IP validation -is_ipv4() { - [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1 -} -is_ipv6() { - [[ "$1" =~ : ]] && return 0 || return 1 -} -is_ip() { - is_ipv4 "$1" || is_ipv6 "$1" -} -is_domain() { - [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1 -} - -# check root -[[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \n" && exit 1 - -# Check OS and set release variable -if [[ -f /etc/os-release ]]; then - source /etc/os-release - release=$ID -elif [[ -f /usr/lib/os-release ]]; then - source /usr/lib/os-release - release=$ID -else - echo "Failed to check the system OS, please contact the author!" >&2 - exit 1 -fi +# Print OS info echo "The OS release is: $release" -os_version="" -os_version=$(grep "^VERSION_ID" /etc/os-release | cut -d '=' -f2 | tr -d '"' | tr -d '.') - -# Declare Variables -xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}" -xui_service="${XUI_SERVICE:=/etc/systemd/system}" -log_folder="${XUI_LOG_FOLDER:=/var/log/x-ui}" -mkdir -p "${log_folder}" -iplimit_log_path="${log_folder}/3xipl.log" -iplimit_banned_log_path="${log_folder}/3xipl-banned.log" - -confirm() { - if [[ $# > 1 ]]; then - echo && read -rp "$1 [Default $2]: " temp - if [[ "${temp}" == "" ]]; then - temp=$2 - fi - else - read -rp "$1 [y/n]: " temp - fi - if [[ "${temp}" == "y" || "${temp}" == "Y" ]]; then - return 0 - else - return 1 - fi -} +#============================================================================= +# Menu Functions (kept in entrypoint to avoid circular dependencies) +#============================================================================= confirm_restart() { confirm "Restart the panel, Attention: Restarting the panel will also restart xray" "y" @@ -107,2020 +47,6 @@ before_show_menu() { show_menu } -install() { - bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/install.sh) - if [[ $? == 0 ]]; then - if [[ $# == 0 ]]; then - start - else - start 0 - fi - fi -} - -update() { - confirm "This function will update all x-ui components to the latest version, and the data will not be lost. Do you want to continue?" "y" - if [[ $? != 0 ]]; then - LOGE "Cancelled" - if [[ $# == 0 ]]; then - before_show_menu - fi - return 0 - fi - bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/update.sh) - if [[ $? == 0 ]]; then - LOGI "Update is complete, Panel has automatically restarted " - before_show_menu - fi -} - -update_menu() { - echo -e "${yellow}Updating Menu${plain}" - confirm "This function will update the menu to the latest changes." "y" - if [[ $? != 0 ]]; then - LOGE "Cancelled" - if [[ $# == 0 ]]; then - before_show_menu - fi - return 0 - fi - - curl -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh - chmod +x ${xui_folder}/x-ui.sh - chmod +x /usr/bin/x-ui - - if [[ $? == 0 ]]; then - echo -e "${green}Update successful. The panel has automatically restarted.${plain}" - exit 0 - else - echo -e "${red}Failed to update the menu.${plain}" - return 1 - fi -} - -legacy_version() { - echo -n "Enter the panel version (like 2.4.0):" - read -r tag_version - - if [ -z "$tag_version" ]; then - echo "Panel version cannot be empty. Exiting." - exit 1 - fi - # Use the entered panel version in the download link - install_command="bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/v$tag_version/install.sh") v$tag_version" - - echo "Downloading and installing panel version $tag_version..." - eval $install_command -} - -# Function to handle the deletion of the script file -delete_script() { - rm "$0" # Remove the script file itself - exit 1 -} - -uninstall() { - confirm "Are you sure you want to uninstall the panel? xray will also uninstalled!" "n" - if [[ $? != 0 ]]; then - if [[ $# == 0 ]]; then - show_menu - fi - return 0 - fi - - if [[ $release == "alpine" ]]; then - rc-service x-ui stop - rc-update del x-ui - rm /etc/init.d/x-ui -f - else - systemctl stop x-ui - systemctl disable x-ui - rm ${xui_service}/x-ui.service -f - systemctl daemon-reload - systemctl reset-failed - fi - - rm /etc/x-ui/ -rf - rm ${xui_folder}/ -rf - - echo "" - echo -e "Uninstalled Successfully.\n" - echo "If you need to install this panel again, you can use below command:" - echo -e "${green}bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)${plain}" - echo "" - # Trap the SIGTERM signal - trap delete_script SIGTERM - delete_script -} - -reset_user() { - confirm "Are you sure to reset the username and password of the panel?" "n" - if [[ $? != 0 ]]; then - if [[ $# == 0 ]]; then - show_menu - fi - return 0 - fi - - read -rp "Please set the login username [default is a random username]: " config_account - [[ -z $config_account ]] && config_account=$(gen_random_string 10) - read -rp "Please set the login password [default is a random password]: " config_password - [[ -z $config_password ]] && config_password=$(gen_random_string 18) - - read -rp "Do you want to disable currently configured two-factor authentication? (y/n): " twoFactorConfirm - if [[ $twoFactorConfirm != "y" && $twoFactorConfirm != "Y" ]]; then - ${xui_folder}/x-ui setting -username ${config_account} -password ${config_password} -resetTwoFactor false >/dev/null 2>&1 - else - ${xui_folder}/x-ui setting -username ${config_account} -password ${config_password} -resetTwoFactor true >/dev/null 2>&1 - echo -e "Two factor authentication has been disabled." - fi - - echo -e "Panel login username has been reset to: ${green} ${config_account} ${plain}" - echo -e "Panel login password has been reset to: ${green} ${config_password} ${plain}" - echo -e "${green} Please use the new login username and password to access the X-UI panel. Also remember them! ${plain}" - confirm_restart -} - -gen_random_string() { - local length="$1" - local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' /dev/null 2>&1 - - echo -e "Web base path has been reset to: ${green}${config_webBasePath}${plain}" - echo -e "${green}Please use the new web base path to access the panel.${plain}" - restart -} - -reset_config() { - confirm "Are you sure you want to reset all panel settings, Account data will not be lost, Username and password will not change" "n" - if [[ $? != 0 ]]; then - if [[ $# == 0 ]]; then - show_menu - fi - return 0 - fi - ${xui_folder}/x-ui setting -reset - echo -e "All panel settings have been reset to default." - restart -} - -check_config() { - local info=$(${xui_folder}/x-ui setting -show true) - if [[ $? != 0 ]]; then - LOGE "get current settings error, please check logs" - show_menu - return - fi - LOGI "${info}" - - local existing_webBasePath=$(echo "$info" | grep -Eo 'webBasePath: .+' | awk '{print $2}') - local existing_port=$(echo "$info" | grep -Eo 'port: .+' | awk '{print $2}') - local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') - local server_ip=$(curl -s --max-time 3 https://api.ipify.org) - if [ -z "$server_ip" ]; then - server_ip=$(curl -s --max-time 3 https://4.ident.me) - fi - - if [[ -n "$existing_cert" ]]; then - local domain=$(basename "$(dirname "$existing_cert")") - - if [[ "$domain" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then - echo -e "${green}Access URL: https://${domain}:${existing_port}${existing_webBasePath}${plain}" - else - echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" - fi - else - echo -e "${red}⚠ WARNING: No SSL certificate configured!${plain}" - echo -e "${yellow}You can get a Let's Encrypt certificate for your IP address (valid ~6 days, auto-renews).${plain}" - read -rp "Generate SSL certificate for IP now? [y/N]: " gen_ssl - if [[ "$gen_ssl" == "y" || "$gen_ssl" == "Y" ]]; then - stop >/dev/null 2>&1 - ssl_cert_issue_for_ip - if [[ $? -eq 0 ]]; then - echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" - # ssl_cert_issue_for_ip already restarts the panel, but ensure it's running - start >/dev/null 2>&1 - else - LOGE "IP certificate setup failed." - echo -e "${yellow}You can try again via option 18 (SSL Certificate Management).${plain}" - start >/dev/null 2>&1 - fi - else - echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}" - echo -e "${yellow}For security, please configure SSL certificate using option 18 (SSL Certificate Management)${plain}" - fi - fi -} - -set_port() { - echo -n "Enter port number[1-65535]: " - read -r port - if [[ -z "${port}" ]]; then - LOGD "Cancelled" - before_show_menu - else - ${xui_folder}/x-ui setting -port ${port} - echo -e "The port is set, Please restart the panel now, and use the new port ${green}${port}${plain} to access web panel" - confirm_restart - fi -} - -start() { - check_status - if [[ $? == 0 ]]; then - echo "" - LOGI "Panel is running, No need to start again, If you need to restart, please select restart" - else - if [[ $release == "alpine" ]]; then - rc-service x-ui start - else - systemctl start x-ui - fi - sleep 2 - check_status - if [[ $? == 0 ]]; then - LOGI "x-ui Started Successfully" - else - LOGE "panel Failed to start, Probably because it takes longer than two seconds to start, Please check the log information later" - fi - fi - - if [[ $# == 0 ]]; then - before_show_menu - fi -} - -stop() { - check_status - if [[ $? == 1 ]]; then - echo "" - LOGI "Panel stopped, No need to stop again!" - else - if [[ $release == "alpine" ]]; then - rc-service x-ui stop - else - systemctl stop x-ui - fi - sleep 2 - check_status - if [[ $? == 1 ]]; then - LOGI "x-ui and xray stopped successfully" - else - LOGE "Panel stop failed, Probably because the stop time exceeds two seconds, Please check the log information later" - fi - fi - - if [[ $# == 0 ]]; then - before_show_menu - fi -} - -restart() { - if [[ $release == "alpine" ]]; then - rc-service x-ui restart - else - systemctl restart x-ui - fi - sleep 2 - check_status - if [[ $? == 0 ]]; then - LOGI "x-ui and xray Restarted successfully" - else - LOGE "Panel restart failed, Probably because it takes longer than two seconds to start, Please check the log information later" - fi - if [[ $# == 0 ]]; then - before_show_menu - fi -} - -status() { - if [[ $release == "alpine" ]]; then - rc-service x-ui status - else - systemctl status x-ui -l - fi - if [[ $# == 0 ]]; then - before_show_menu - fi -} - -enable() { - if [[ $release == "alpine" ]]; then - rc-update add x-ui - else - systemctl enable x-ui - fi - if [[ $? == 0 ]]; then - LOGI "x-ui Set to boot automatically on startup successfully" - else - LOGE "x-ui Failed to set Autostart" - fi - - if [[ $# == 0 ]]; then - before_show_menu - fi -} - -disable() { - if [[ $release == "alpine" ]]; then - rc-update del x-ui - else - systemctl disable x-ui - fi - if [[ $? == 0 ]]; then - LOGI "x-ui Autostart Cancelled successfully" - else - LOGE "x-ui Failed to cancel autostart" - fi - - if [[ $# == 0 ]]; then - before_show_menu - fi -} - -show_log() { - if [[ $release == "alpine" ]]; then - echo -e "${green}\t1.${plain} Debug Log" - echo -e "${green}\t0.${plain} Back to Main Menu" - read -rp "Choose an option: " choice - - case "$choice" in - 0) - show_menu - ;; - 1) - grep -F 'x-ui[' /var/log/messages - if [[ $# == 0 ]]; then - before_show_menu - fi - ;; - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - show_log - ;; - esac - else - echo -e "${green}\t1.${plain} Debug Log" - echo -e "${green}\t2.${plain} Clear All logs" - echo -e "${green}\t0.${plain} Back to Main Menu" - read -rp "Choose an option: " choice - - case "$choice" in - 0) - show_menu - ;; - 1) - journalctl -u x-ui -e --no-pager -f -p debug - if [[ $# == 0 ]]; then - before_show_menu - fi - ;; - 2) - sudo journalctl --rotate - sudo journalctl --vacuum-time=1s - echo "All Logs cleared." - restart - ;; - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - show_log - ;; - esac - fi -} - -bbr_menu() { - echo -e "${green}\t1.${plain} Enable BBR" - echo -e "${green}\t2.${plain} Disable BBR" - echo -e "${green}\t0.${plain} Back to Main Menu" - read -rp "Choose an option: " choice - case "$choice" in - 0) - show_menu - ;; - 1) - enable_bbr - bbr_menu - ;; - 2) - disable_bbr - bbr_menu - ;; - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - bbr_menu - ;; - esac -} - -disable_bbr() { - - if ! grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf || ! grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then - echo -e "${yellow}BBR is not currently enabled.${plain}" - before_show_menu - fi - - # Replace BBR with CUBIC configurations - sed -i 's/net.core.default_qdisc=fq/net.core.default_qdisc=pfifo_fast/' /etc/sysctl.conf - sed -i 's/net.ipv4.tcp_congestion_control=bbr/net.ipv4.tcp_congestion_control=cubic/' /etc/sysctl.conf - - # Apply changes - sysctl -p - - # Verify that BBR is replaced with CUBIC - if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "cubic" ]]; then - echo -e "${green}BBR has been replaced with CUBIC successfully.${plain}" - else - echo -e "${red}Failed to replace BBR with CUBIC. Please check your system configuration.${plain}" - fi -} - -enable_bbr() { - if grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf && grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then - echo -e "${green}BBR is already enabled!${plain}" - before_show_menu - fi - - # Enable BBR - echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf - echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf - - # Apply changes - sysctl -p - - # Verify that BBR is enabled - if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then - echo -e "${green}BBR has been enabled successfully.${plain}" - else - echo -e "${red}Failed to enable BBR. Please check your system configuration.${plain}" - fi -} - -update_shell() { - curl -fLRo /usr/bin/x-ui -z /usr/bin/x-ui https://github.com/MHSanaei/3x-ui/raw/main/x-ui.sh - if [[ $? != 0 ]]; then - echo "" - LOGE "Failed to download script, Please check whether the machine can connect Github" - before_show_menu - else - chmod +x /usr/bin/x-ui - LOGI "Upgrade script succeeded, Please rerun the script" - before_show_menu - fi -} - -# 0: running, 1: not running, 2: not installed -check_status() { - if [[ $release == "alpine" ]]; then - if [[ ! -f /etc/init.d/x-ui ]]; then - return 2 - fi - if [[ $(rc-service x-ui status | grep -F 'status: started' -c) == 1 ]]; then - return 0 - else - return 1 - fi - else - if [[ ! -f ${xui_service}/x-ui.service ]]; then - return 2 - fi - temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1) - if [[ "${temp}" == "running" ]]; then - return 0 - else - return 1 - fi - fi -} - -check_enabled() { - if [[ $release == "alpine" ]]; then - if [[ $(rc-update show | grep -F 'x-ui' | grep default -c) == 1 ]]; then - return 0 - else - return 1 - fi - else - temp=$(systemctl is-enabled x-ui) - if [[ "${temp}" == "enabled" ]]; then - return 0 - else - return 1 - fi - fi -} - -check_uninstall() { - check_status - if [[ $? != 2 ]]; then - echo "" - LOGE "Panel installed, Please do not reinstall" - if [[ $# == 0 ]]; then - before_show_menu - fi - return 1 - else - return 0 - fi -} - -check_install() { - check_status - if [[ $? == 2 ]]; then - echo "" - LOGE "Please install the panel first" - if [[ $# == 0 ]]; then - before_show_menu - fi - return 1 - else - return 0 - fi -} - -show_status() { - check_status - case $? in - 0) - echo -e "Panel state: ${green}Running${plain}" - show_enable_status - ;; - 1) - echo -e "Panel state: ${yellow}Not Running${plain}" - show_enable_status - ;; - 2) - echo -e "Panel state: ${red}Not Installed${plain}" - ;; - esac - show_xray_status -} - -show_enable_status() { - check_enabled - if [[ $? == 0 ]]; then - echo -e "Start automatically: ${green}Yes${plain}" - else - echo -e "Start automatically: ${red}No${plain}" - fi -} - -check_xray_status() { - count=$(ps -ef | grep "xray-linux" | grep -v "grep" | wc -l) - if [[ count -ne 0 ]]; then - return 0 - else - return 1 - fi -} - -show_xray_status() { - check_xray_status - if [[ $? == 0 ]]; then - echo -e "xray state: ${green}Running${plain}" - else - echo -e "xray state: ${red}Not Running${plain}" - fi -} - -firewall_menu() { - echo -e "${green}\t1.${plain} ${green}Install${plain} Firewall" - echo -e "${green}\t2.${plain} Port List [numbered]" - echo -e "${green}\t3.${plain} ${green}Open${plain} Ports" - echo -e "${green}\t4.${plain} ${red}Delete${plain} Ports from List" - echo -e "${green}\t5.${plain} ${green}Enable${plain} Firewall" - echo -e "${green}\t6.${plain} ${red}Disable${plain} Firewall" - echo -e "${green}\t7.${plain} Firewall Status" - echo -e "${green}\t0.${plain} Back to Main Menu" - read -rp "Choose an option: " choice - case "$choice" in - 0) - show_menu - ;; - 1) - install_firewall - firewall_menu - ;; - 2) - ufw status numbered - firewall_menu - ;; - 3) - open_ports - firewall_menu - ;; - 4) - delete_ports - firewall_menu - ;; - 5) - ufw enable - firewall_menu - ;; - 6) - ufw disable - firewall_menu - ;; - 7) - ufw status verbose - firewall_menu - ;; - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - firewall_menu - ;; - esac -} - -install_firewall() { - if ! command -v ufw &>/dev/null; then - echo "ufw firewall is not installed. Installing now..." - apt-get update - apt-get install -y ufw - else - echo "ufw firewall is already installed" - fi - - # Check if the firewall is inactive - if ufw status | grep -q "Status: active"; then - echo "Firewall is already active" - else - echo "Activating firewall..." - # Open the necessary ports - ufw allow ssh - ufw allow http - ufw allow https - ufw allow 2053/tcp #webPort - ufw allow 2096/tcp #subport - - # Enable the firewall - ufw --force enable - fi -} - -open_ports() { - # Prompt the user to enter the ports they want to open - read -rp "Enter the ports you want to open (e.g. 80,443,2053 or range 400-500): " ports - - # Check if the input is valid - if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then - echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2 - exit 1 - fi - - # Open the specified ports using ufw - IFS=',' read -ra PORT_LIST <<<"$ports" - for port in "${PORT_LIST[@]}"; do - if [[ $port == *-* ]]; then - # Split the range into start and end ports - start_port=$(echo $port | cut -d'-' -f1) - end_port=$(echo $port | cut -d'-' -f2) - # Open the port range - ufw allow $start_port:$end_port/tcp - ufw allow $start_port:$end_port/udp - else - # Open the single port - ufw allow "$port" - fi - done - - # Confirm that the ports are opened - echo "Opened the specified ports:" - for port in "${PORT_LIST[@]}"; do - if [[ $port == *-* ]]; then - start_port=$(echo $port | cut -d'-' -f1) - end_port=$(echo $port | cut -d'-' -f2) - # Check if the port range has been successfully opened - (ufw status | grep -q "$start_port:$end_port") && echo "$start_port-$end_port" - else - # Check if the individual port has been successfully opened - (ufw status | grep -q "$port") && echo "$port" - fi - done -} - -delete_ports() { - # Display current rules with numbers - echo "Current UFW rules:" - ufw status numbered - - # Ask the user how they want to delete rules - echo "Do you want to delete rules by:" - echo "1) Rule numbers" - echo "2) Ports" - read -rp "Enter your choice (1 or 2): " choice - - if [[ $choice -eq 1 ]]; then - # Deleting by rule numbers - read -rp "Enter the rule numbers you want to delete (1, 2, etc.): " rule_numbers - - # Validate the input - if ! [[ $rule_numbers =~ ^([0-9]+)(,[0-9]+)*$ ]]; then - echo "Error: Invalid input. Please enter a comma-separated list of rule numbers." >&2 - exit 1 - fi - - # Split numbers into an array - IFS=',' read -ra RULE_NUMBERS <<<"$rule_numbers" - for rule_number in "${RULE_NUMBERS[@]}"; do - # Delete the rule by number - ufw delete "$rule_number" || echo "Failed to delete rule number $rule_number" - done - - echo "Selected rules have been deleted." - - elif [[ $choice -eq 2 ]]; then - # Deleting by ports - read -rp "Enter the ports you want to delete (e.g. 80,443,2053 or range 400-500): " ports - - # Validate the input - if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then - echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2 - exit 1 - fi - - # Split ports into an array - IFS=',' read -ra PORT_LIST <<<"$ports" - for port in "${PORT_LIST[@]}"; do - if [[ $port == *-* ]]; then - # Split the port range - start_port=$(echo $port | cut -d'-' -f1) - end_port=$(echo $port | cut -d'-' -f2) - # Delete the port range - ufw delete allow $start_port:$end_port/tcp - ufw delete allow $start_port:$end_port/udp - else - # Delete a single port - ufw delete allow "$port" - fi - done - - # Confirmation of deletion - echo "Deleted the specified ports:" - for port in "${PORT_LIST[@]}"; do - if [[ $port == *-* ]]; then - start_port=$(echo $port | cut -d'-' -f1) - end_port=$(echo $port | cut -d'-' -f2) - # Check if the port range has been deleted - (ufw status | grep -q "$start_port:$end_port") || echo "$start_port-$end_port" - else - # Check if the individual port has been deleted - (ufw status | grep -q "$port") || echo "$port" - fi - done - else - echo "${red}Error:${plain} Invalid choice. Please enter 1 or 2." >&2 - exit 1 - fi -} - -update_all_geofiles() { - update_geofiles "main" - update_geofiles "IR" - update_geofiles "RU" -} - -update_geofiles() { - case "${1}" in - "main") dat_files=(geoip geosite); dat_source="Loyalsoldier/v2ray-rules-dat";; - "IR") dat_files=(geoip_IR geosite_IR); dat_source="chocolate4u/Iran-v2ray-rules" ;; - "RU") dat_files=(geoip_RU geosite_RU); dat_source="runetfreedom/russia-v2ray-rules-dat";; - esac - for dat in "${dat_files[@]}"; do - # Remove suffix for remote filename (e.g., geoip_IR -> geoip) - remote_file="${dat%%_*}" - curl -fLRo ${xui_folder}/bin/${dat}.dat -z ${xui_folder}/bin/${dat}.dat \ - https://github.com/${dat_source}/releases/latest/download/${remote_file}.dat - done -} - -update_geo() { - echo -e "${green}\t1.${plain} Loyalsoldier (geoip.dat, geosite.dat)" - echo -e "${green}\t2.${plain} chocolate4u (geoip_IR.dat, geosite_IR.dat)" - echo -e "${green}\t3.${plain} runetfreedom (geoip_RU.dat, geosite_RU.dat)" - echo -e "${green}\t4.${plain} All" - echo -e "${green}\t0.${plain} Back to Main Menu" - read -rp "Choose an option: " choice - - case "$choice" in - 0) - show_menu - ;; - 1) - update_geofiles "main" - echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}" - restart - ;; - 2) - update_geofiles "IR" - echo -e "${green}chocolate4u datasets have been updated successfully!${plain}" - restart - ;; - 3) - update_geofiles "RU" - echo -e "${green}runetfreedom datasets have been updated successfully!${plain}" - restart - ;; - 4) - update_all_geofiles - echo -e "${green}All geo files have been updated successfully!${plain}" - restart - ;; - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - update_geo - ;; - esac - - before_show_menu -} - -install_acme() { - # Check if acme.sh is already installed - if command -v ~/.acme.sh/acme.sh &>/dev/null; then - LOGI "acme.sh is already installed." - return 0 - fi - - LOGI "Installing acme.sh..." - cd ~ || return 1 # Ensure you can change to the home directory - - curl -s https://get.acme.sh | sh - if [ $? -ne 0 ]; then - LOGE "Installation of acme.sh failed." - return 1 - else - LOGI "Installation of acme.sh succeeded." - fi - - return 0 -} - -ssl_cert_issue_main() { - echo -e "${green}\t1.${plain} Get SSL (Domain)" - echo -e "${green}\t2.${plain} Revoke" - echo -e "${green}\t3.${plain} Force Renew" - echo -e "${green}\t4.${plain} Show Existing Domains" - echo -e "${green}\t5.${plain} Set Cert paths for the panel" - echo -e "${green}\t6.${plain} Get SSL for IP Address (6-day cert, auto-renews)" - echo -e "${green}\t0.${plain} Back to Main Menu" - - read -rp "Choose an option: " choice - case "$choice" in - 0) - show_menu - ;; - 1) - ssl_cert_issue - ssl_cert_issue_main - ;; - 2) - local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) - if [ -z "$domains" ]; then - echo "No certificates found to revoke." - else - echo "Existing domains:" - echo "$domains" - read -rp "Please enter a domain from the list to revoke the certificate: " domain - if echo "$domains" | grep -qw "$domain"; then - ~/.acme.sh/acme.sh --revoke -d ${domain} - LOGI "Certificate revoked for domain: $domain" - else - echo "Invalid domain entered." - fi - fi - ssl_cert_issue_main - ;; - 3) - local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) - if [ -z "$domains" ]; then - echo "No certificates found to renew." - else - echo "Existing domains:" - echo "$domains" - read -rp "Please enter a domain from the list to renew the SSL certificate: " domain - if echo "$domains" | grep -qw "$domain"; then - ~/.acme.sh/acme.sh --renew -d ${domain} --force - LOGI "Certificate forcefully renewed for domain: $domain" - else - echo "Invalid domain entered." - fi - fi - ssl_cert_issue_main - ;; - 4) - local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) - if [ -z "$domains" ]; then - echo "No certificates found." - else - echo "Existing domains and their paths:" - for domain in $domains; do - local cert_path="/root/cert/${domain}/fullchain.pem" - local key_path="/root/cert/${domain}/privkey.pem" - if [[ -f "${cert_path}" && -f "${key_path}" ]]; then - echo -e "Domain: ${domain}" - echo -e "\tCertificate Path: ${cert_path}" - echo -e "\tPrivate Key Path: ${key_path}" - else - echo -e "Domain: ${domain} - Certificate or Key missing." - fi - done - fi - ssl_cert_issue_main - ;; - 5) - local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) - if [ -z "$domains" ]; then - echo "No certificates found." - else - echo "Available domains:" - echo "$domains" - read -rp "Please choose a domain to set the panel paths: " domain - - if echo "$domains" | grep -qw "$domain"; then - local webCertFile="/root/cert/${domain}/fullchain.pem" - local webKeyFile="/root/cert/${domain}/privkey.pem" - - if [[ -f "${webCertFile}" && -f "${webKeyFile}" ]]; then - ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" - echo "Panel paths set for domain: $domain" - echo " - Certificate File: $webCertFile" - echo " - Private Key File: $webKeyFile" - restart - else - echo "Certificate or private key not found for domain: $domain." - fi - else - echo "Invalid domain entered." - fi - fi - ssl_cert_issue_main - ;; - 6) - echo -e "${yellow}Let's Encrypt SSL Certificate for IP Address${plain}" - echo -e "This will obtain a certificate for your server's IP using the shortlived profile." - echo -e "${yellow}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}" - echo -e "${yellow}Port 80 must be open and accessible from the internet.${plain}" - confirm "Do you want to proceed?" "y" - if [[ $? == 0 ]]; then - ssl_cert_issue_for_ip - fi - ssl_cert_issue_main - ;; - - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - ssl_cert_issue_main - ;; - esac -} - -ssl_cert_issue_for_ip() { - LOGI "Starting automatic SSL certificate generation for server IP..." - LOGI "Using Let's Encrypt shortlived profile (~6 days validity, auto-renews)" - - local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') - local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') - - # Get server IP - local server_ip=$(curl -s --max-time 3 https://api.ipify.org) - if [ -z "$server_ip" ]; then - server_ip=$(curl -s --max-time 3 https://4.ident.me) - fi - - if [ -z "$server_ip" ]; then - LOGE "Failed to get server IP address" - return 1 - fi - - LOGI "Server IP detected: ${server_ip}" - - # Ask for optional IPv6 - local ipv6_addr="" - read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr - ipv6_addr="${ipv6_addr// /}" # Trim whitespace - - # check for acme.sh first - if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then - LOGI "acme.sh not found, installing..." - install_acme - if [ $? -ne 0 ]; then - LOGE "Failed to install acme.sh" - return 1 - fi - fi - - # install socat - case "${release}" in - ubuntu | debian | armbian) - apt-get update >/dev/null 2>&1 && apt-get install socat -y >/dev/null 2>&1 - ;; - fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 - ;; - centos) - if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1 - else - dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 - fi - ;; - arch | manjaro | parch) - pacman -Sy --noconfirm socat >/dev/null 2>&1 - ;; - opensuse-tumbleweed | opensuse-leap) - zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1 - ;; - alpine) - apk add socat curl openssl >/dev/null 2>&1 - ;; - *) - LOGW "Unsupported OS for automatic socat installation" - ;; - esac - - # Create certificate directory - certPath="/root/cert/ip" - mkdir -p "$certPath" - - # Build domain arguments - local domain_args="-d ${server_ip}" - if [[ -n "$ipv6_addr" ]] && is_ipv6 "$ipv6_addr"; then - domain_args="${domain_args} -d ${ipv6_addr}" - LOGI "Including IPv6 address: ${ipv6_addr}" - fi - - # Choose port for HTTP-01 listener (default 80, allow override) - local WebPort="" - read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort - WebPort="${WebPort:-80}" - if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then - LOGE "Invalid port provided. Falling back to 80." - WebPort=80 - fi - LOGI "Using port ${WebPort} to issue certificate for IP: ${server_ip}" - if [[ "${WebPort}" -ne 80 ]]; then - LOGI "Reminder: Let's Encrypt still reaches port 80; forward external port 80 to ${WebPort} for validation." - fi - - while true; do - if is_port_in_use "${WebPort}"; then - LOGI "Port ${WebPort} is currently in use." - - local alt_port="" - read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port - alt_port="${alt_port// /}" - if [[ -z "${alt_port}" ]]; then - LOGE "Port ${WebPort} is busy; cannot proceed with issuance." - return 1 - fi - if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then - LOGE "Invalid port provided." - return 1 - fi - WebPort="${alt_port}" - continue - else - LOGI "Port ${WebPort} is free and ready for standalone validation." - break - fi - done - - # Reload command - restarts panel after renewal - local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null" - - # issue the certificate for IP with shortlived profile - ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt - ~/.acme.sh/acme.sh --issue \ - ${domain_args} \ - --standalone \ - --server letsencrypt \ - --certificate-profile shortlived \ - --days 6 \ - --httpport ${WebPort} \ - --force - - if [ $? -ne 0 ]; then - LOGE "Failed to issue certificate for IP: ${server_ip}" - LOGE "Make sure port ${WebPort} is open and the server is accessible from the internet" - # Cleanup acme.sh data for both IPv4 and IPv6 if specified - rm -rf ~/.acme.sh/${server_ip} 2>/dev/null - [[ -n "$ipv6_addr" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null - rm -rf ${certPath} 2>/dev/null - return 1 - else - LOGI "Certificate issued successfully for IP: ${server_ip}" - fi - - # Install the certificate - # Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails, - # but the cert files are still installed. We check for files instead of exit code. - ~/.acme.sh/acme.sh --installcert -d ${server_ip} \ - --key-file "${certPath}/privkey.pem" \ - --fullchain-file "${certPath}/fullchain.pem" \ - --reloadcmd "${reloadCmd}" 2>&1 || true - - # Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero) - if [[ ! -f "${certPath}/fullchain.pem" || ! -f "${certPath}/privkey.pem" ]]; then - LOGE "Certificate files not found after installation" - # Cleanup acme.sh data for both IPv4 and IPv6 if specified - rm -rf ~/.acme.sh/${server_ip} 2>/dev/null - [[ -n "$ipv6_addr" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null - rm -rf ${certPath} 2>/dev/null - return 1 - fi - - LOGI "Certificate files installed successfully" - - # enable auto-renew - ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 - chmod 600 $certPath/privkey.pem 2>/dev/null - chmod 644 $certPath/fullchain.pem 2>/dev/null - - # Set certificate paths for the panel - local webCertFile="${certPath}/fullchain.pem" - local webKeyFile="${certPath}/privkey.pem" - - if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then - ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" - LOGI "Certificate configured for panel" - LOGI " - Certificate File: $webCertFile" - LOGI " - Private Key File: $webKeyFile" - LOGI " - Validity: ~6 days (auto-renews via acme.sh cron)" - echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" - LOGI "Panel will restart to apply SSL certificate..." - restart - return 0 - else - LOGE "Certificate files not found after installation" - return 1 - fi -} - -ssl_cert_issue() { - local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') - local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') - # check for acme.sh first - if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then - echo "acme.sh could not be found. we will install it" - install_acme - if [ $? -ne 0 ]; then - LOGE "install acme failed, please check logs" - exit 1 - fi - fi - - # install socat - case "${release}" in - ubuntu | debian | armbian) - apt-get update >/dev/null 2>&1 && apt-get install socat -y >/dev/null 2>&1 - ;; - fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 - ;; - centos) - if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1 - else - dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 - fi - ;; - arch | manjaro | parch) - pacman -Sy --noconfirm socat >/dev/null 2>&1 - ;; - opensuse-tumbleweed | opensuse-leap) - zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1 - ;; - alpine) - apk add socat curl openssl >/dev/null 2>&1 - ;; - *) - LOGW "Unsupported OS for automatic socat installation" - ;; - esac - if [ $? -ne 0 ]; then - LOGE "install socat failed, please check logs" - exit 1 - else - LOGI "install socat succeed..." - fi - - # get the domain here, and we need to verify it - local domain="" - while true; do - read -rp "Please enter your domain name: " domain - domain="${domain// /}" # Trim whitespace - - if [[ -z "$domain" ]]; then - LOGE "Domain name cannot be empty. Please try again." - continue - fi - - if ! is_domain "$domain"; then - LOGE "Invalid domain format: ${domain}. Please enter a valid domain name." - continue - fi - - break - done - LOGD "Your domain is: ${domain}, checking it..." - - # check if there already exists a certificate - local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') - if [ "${currentCert}" == "${domain}" ]; then - local certInfo=$(~/.acme.sh/acme.sh --list) - LOGE "System already has certificates for this domain. Cannot issue again. Current certificate details:" - LOGI "$certInfo" - exit 1 - else - LOGI "Your domain is ready for issuing certificates now..." - fi - - # create a directory for the certificate - certPath="/root/cert/${domain}" - if [ ! -d "$certPath" ]; then - mkdir -p "$certPath" - else - rm -rf "$certPath" - mkdir -p "$certPath" - fi - - # get the port number for the standalone server - local WebPort=80 - read -rp "Please choose which port to use (default is 80): " WebPort - if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then - LOGE "Your input ${WebPort} is invalid, will use default port 80." - WebPort=80 - fi - LOGI "Will use port: ${WebPort} to issue certificates. Please make sure this port is open." - - # issue the certificate - ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt - ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force - if [ $? -ne 0 ]; then - LOGE "Issuing certificate failed, please check logs." - rm -rf ~/.acme.sh/${domain} - exit 1 - else - LOGE "Issuing certificate succeeded, installing certificates..." - fi - - reloadCmd="x-ui restart" - - LOGI "Default --reloadcmd for ACME is: ${yellow}x-ui restart" - LOGI "This command will run on every certificate issue and renew." - read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd - if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then - echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; x-ui restart" - echo -e "${green}\t2.${plain} Input your own command" - echo -e "${green}\t0.${plain} Keep default reloadcmd" - read -rp "Choose an option: " choice - case "$choice" in - 1) - LOGI "Reloadcmd is: systemctl reload nginx ; x-ui restart" - reloadCmd="systemctl reload nginx ; x-ui restart" - ;; - 2) - LOGD "It's recommended to put x-ui restart at the end, so it won't raise an error if other services fails" - read -rp "Please enter your reloadcmd (example: systemctl reload nginx ; x-ui restart): " reloadCmd - LOGI "Your reloadcmd is: ${reloadCmd}" - ;; - *) - LOGI "Keep default reloadcmd" - ;; - esac - fi - - # install the certificate - ~/.acme.sh/acme.sh --installcert -d ${domain} \ - --key-file /root/cert/${domain}/privkey.pem \ - --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" - - if [ $? -ne 0 ]; then - LOGE "Installing certificate failed, exiting." - rm -rf ~/.acme.sh/${domain} - exit 1 - else - LOGI "Installing certificate succeeded, enabling auto renew..." - fi - - # enable auto-renew - ~/.acme.sh/acme.sh --upgrade --auto-upgrade - if [ $? -ne 0 ]; then - LOGE "Auto renew failed, certificate details:" - ls -lah cert/* - chmod 600 $certPath/privkey.pem - chmod 644 $certPath/fullchain.pem - exit 1 - else - LOGI "Auto renew succeeded, certificate details:" - ls -lah cert/* - chmod 600 $certPath/privkey.pem - chmod 644 $certPath/fullchain.pem - fi - - # Prompt user to set panel paths after successful certificate installation - read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel - if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then - local webCertFile="/root/cert/${domain}/fullchain.pem" - local webKeyFile="/root/cert/${domain}/privkey.pem" - - if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then - ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" - LOGI "Panel paths set for domain: $domain" - LOGI " - Certificate File: $webCertFile" - LOGI " - Private Key File: $webKeyFile" - echo -e "${green}Access URL: https://${domain}:${existing_port}${existing_webBasePath}${plain}" - restart - else - LOGE "Error: Certificate or private key file not found for domain: $domain." - fi - else - LOGI "Skipping panel path setting." - fi -} - -ssl_cert_issue_CF() { - local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') - local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') - LOGI "****** Instructions for Use ******" - LOGI "Follow the steps below to complete the process:" - LOGI "1. Cloudflare Registered E-mail." - LOGI "2. Cloudflare Global API Key." - LOGI "3. The Domain Name." - LOGI "4. Once the certificate is issued, you will be prompted to set the certificate for the panel (optional)." - LOGI "5. The script also supports automatic renewal of the SSL certificate after installation." - - confirm "Do you confirm the information and wish to proceed? [y/n]" "y" - - if [ $? -eq 0 ]; then - # Check for acme.sh first - if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then - echo "acme.sh could not be found. We will install it." - install_acme - if [ $? -ne 0 ]; then - LOGE "Install acme failed, please check logs." - exit 1 - fi - fi - - CF_Domain="" - - LOGD "Please set a domain name:" - read -rp "Input your domain here: " CF_Domain - LOGD "Your domain name is set to: ${CF_Domain}" - - # Set up Cloudflare API details - CF_GlobalKey="" - CF_AccountEmail="" - LOGD "Please set the API key:" - read -rp "Input your key here: " CF_GlobalKey - LOGD "Your API key is: ${CF_GlobalKey}" - - LOGD "Please set up registered email:" - read -rp "Input your email here: " CF_AccountEmail - LOGD "Your registered email address is: ${CF_AccountEmail}" - - # Set the default CA to Let's Encrypt - ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt - if [ $? -ne 0 ]; then - LOGE "Default CA, Let'sEncrypt fail, script exiting..." - exit 1 - fi - - export CF_Key="${CF_GlobalKey}" - export CF_Email="${CF_AccountEmail}" - - # Issue the certificate using Cloudflare DNS - ~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log --force - if [ $? -ne 0 ]; then - LOGE "Certificate issuance failed, script exiting..." - exit 1 - else - LOGI "Certificate issued successfully, Installing..." - fi - - # Install the certificate - certPath="/root/cert/${CF_Domain}" - if [ -d "$certPath" ]; then - rm -rf ${certPath} - fi - - mkdir -p ${certPath} - if [ $? -ne 0 ]; then - LOGE "Failed to create directory: ${certPath}" - exit 1 - fi - - reloadCmd="x-ui restart" - - LOGI "Default --reloadcmd for ACME is: ${yellow}x-ui restart" - LOGI "This command will run on every certificate issue and renew." - read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd - if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then - echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; x-ui restart" - echo -e "${green}\t2.${plain} Input your own command" - echo -e "${green}\t0.${plain} Keep default reloadcmd" - read -rp "Choose an option: " choice - case "$choice" in - 1) - LOGI "Reloadcmd is: systemctl reload nginx ; x-ui restart" - reloadCmd="systemctl reload nginx ; x-ui restart" - ;; - 2) - LOGD "It's recommended to put x-ui restart at the end, so it won't raise an error if other services fails" - read -rp "Please enter your reloadcmd (example: systemctl reload nginx ; x-ui restart): " reloadCmd - LOGI "Your reloadcmd is: ${reloadCmd}" - ;; - *) - LOGI "Keep default reloadcmd" - ;; - esac - fi - ~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \ - --key-file ${certPath}/privkey.pem \ - --fullchain-file ${certPath}/fullchain.pem --reloadcmd "${reloadCmd}" - - if [ $? -ne 0 ]; then - LOGE "Certificate installation failed, script exiting..." - exit 1 - else - LOGI "Certificate installed successfully, Turning on automatic updates..." - fi - - # Enable auto-update - ~/.acme.sh/acme.sh --upgrade --auto-upgrade - if [ $? -ne 0 ]; then - LOGE "Auto update setup failed, script exiting..." - exit 1 - else - LOGI "The certificate is installed and auto-renewal is turned on. Specific information is as follows:" - ls -lah ${certPath}/* - chmod 600 ${certPath}/privkey.pem - chmod 644 ${certPath}/fullchain.pem - fi - - # Prompt user to set panel paths after successful certificate installation - read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel - if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then - local webCertFile="${certPath}/fullchain.pem" - local webKeyFile="${certPath}/privkey.pem" - - if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then - ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" - LOGI "Panel paths set for domain: $CF_Domain" - LOGI " - Certificate File: $webCertFile" - LOGI " - Private Key File: $webKeyFile" - echo -e "${green}Access URL: https://${CF_Domain}:${existing_port}${existing_webBasePath}${plain}" - restart - else - LOGE "Error: Certificate or private key file not found for domain: $CF_Domain." - fi - else - LOGI "Skipping panel path setting." - fi - else - show_menu - fi -} - -run_speedtest() { - # Check if Speedtest is already installed - if ! command -v speedtest &>/dev/null; then - # If not installed, determine installation method - if command -v snap &>/dev/null; then - # Use snap to install Speedtest - echo "Installing Speedtest using snap..." - snap install speedtest - else - # Fallback to using package managers - local pkg_manager="" - local speedtest_install_script="" - - if command -v dnf &>/dev/null; then - pkg_manager="dnf" - speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh" - elif command -v yum &>/dev/null; then - pkg_manager="yum" - speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh" - elif command -v apt-get &>/dev/null; then - pkg_manager="apt-get" - speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh" - elif command -v apt &>/dev/null; then - pkg_manager="apt" - speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh" - fi - - if [[ -z $pkg_manager ]]; then - echo "Error: Package manager not found. You may need to install Speedtest manually." - return 1 - else - echo "Installing Speedtest using $pkg_manager..." - curl -s $speedtest_install_script | bash - $pkg_manager install -y speedtest - fi - fi - fi - - speedtest -} - - - -ip_validation() { - ipv6_regex="^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$" - ipv4_regex="^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)$" -} - -iplimit_main() { - echo -e "\n${green}\t1.${plain} Install Fail2ban and configure IP Limit" - echo -e "${green}\t2.${plain} Change Ban Duration" - echo -e "${green}\t3.${plain} Unban Everyone" - echo -e "${green}\t4.${plain} Ban Logs" - echo -e "${green}\t5.${plain} Ban an IP Address" - echo -e "${green}\t6.${plain} Unban an IP Address" - echo -e "${green}\t7.${plain} Real-Time Logs" - echo -e "${green}\t8.${plain} Service Status" - echo -e "${green}\t9.${plain} Service Restart" - echo -e "${green}\t10.${plain} Uninstall Fail2ban and IP Limit" - echo -e "${green}\t0.${plain} Back to Main Menu" - read -rp "Choose an option: " choice - case "$choice" in - 0) - show_menu - ;; - 1) - confirm "Proceed with installation of Fail2ban & IP Limit?" "y" - if [[ $? == 0 ]]; then - install_iplimit - else - iplimit_main - fi - ;; - 2) - read -rp "Please enter new Ban Duration in Minutes [default 30]: " NUM - if [[ $NUM =~ ^[0-9]+$ ]]; then - create_iplimit_jails ${NUM} - if [[ $release == "alpine" ]]; then - rc-service fail2ban restart - else - systemctl restart fail2ban - fi - else - echo -e "${red}${NUM} is not a number! Please, try again.${plain}" - fi - iplimit_main - ;; - 3) - confirm "Proceed with Unbanning everyone from IP Limit jail?" "y" - if [[ $? == 0 ]]; then - fail2ban-client reload --restart --unban 3x-ipl - truncate -s 0 "${iplimit_banned_log_path}" - echo -e "${green}All users Unbanned successfully.${plain}" - iplimit_main - else - echo -e "${yellow}Cancelled.${plain}" - fi - iplimit_main - ;; - 4) - show_banlog - iplimit_main - ;; - 5) - read -rp "Enter the IP address you want to ban: " ban_ip - ip_validation - if [[ $ban_ip =~ $ipv4_regex || $ban_ip =~ $ipv6_regex ]]; then - fail2ban-client set 3x-ipl banip "$ban_ip" - echo -e "${green}IP Address ${ban_ip} has been banned successfully.${plain}" - else - echo -e "${red}Invalid IP address format! Please try again.${plain}" - fi - iplimit_main - ;; - 6) - read -rp "Enter the IP address you want to unban: " unban_ip - ip_validation - if [[ $unban_ip =~ $ipv4_regex || $unban_ip =~ $ipv6_regex ]]; then - fail2ban-client set 3x-ipl unbanip "$unban_ip" - echo -e "${green}IP Address ${unban_ip} has been unbanned successfully.${plain}" - else - echo -e "${red}Invalid IP address format! Please try again.${plain}" - fi - iplimit_main - ;; - 7) - tail -f /var/log/fail2ban.log - iplimit_main - ;; - 8) - service fail2ban status - iplimit_main - ;; - 9) - if [[ $release == "alpine" ]]; then - rc-service fail2ban restart - else - systemctl restart fail2ban - fi - iplimit_main - ;; - 10) - remove_iplimit - iplimit_main - ;; - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - iplimit_main - ;; - esac -} - -install_iplimit() { - if ! command -v fail2ban-client &>/dev/null; then - echo -e "${green}Fail2ban is not installed. Installing now...!${plain}\n" - - # Check the OS and install necessary packages - case "${release}" in - ubuntu) - apt-get update - if [[ "${os_version}" -ge 24 ]]; then - apt-get install python3-pip -y - python3 -m pip install pyasynchat --break-system-packages - fi - apt-get install fail2ban -y - ;; - debian) - apt-get update - if [ "$os_version" -ge 12 ]; then - apt-get install -y python3-systemd - fi - apt-get install -y fail2ban - ;; - armbian) - apt-get update && apt-get install fail2ban -y - ;; - fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update && dnf -y install fail2ban - ;; - centos) - if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum update -y && yum install epel-release -y - yum -y install fail2ban - else - dnf -y update && dnf -y install fail2ban - fi - ;; - arch | manjaro | parch) - pacman -Syu --noconfirm fail2ban - ;; - alpine) - apk add fail2ban - ;; - *) - echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" - exit 1 - ;; - esac - - if ! command -v fail2ban-client &>/dev/null; then - echo -e "${red}Fail2ban installation failed.${plain}\n" - exit 1 - fi - - echo -e "${green}Fail2ban installed successfully!${plain}\n" - else - echo -e "${yellow}Fail2ban is already installed.${plain}\n" - fi - - echo -e "${green}Configuring IP Limit...${plain}\n" - - # make sure there's no conflict for jail files - iplimit_remove_conflicts - - # Check if log file exists - if ! test -f "${iplimit_banned_log_path}"; then - touch ${iplimit_banned_log_path} - fi - - # Check if service log file exists so fail2ban won't return error - if ! test -f "${iplimit_log_path}"; then - touch ${iplimit_log_path} - fi - - # Create the iplimit jail files - # we didn't pass the bantime here to use the default value - create_iplimit_jails - - # Launching fail2ban - if [[ $release == "alpine" ]]; then - if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then - rc-service fail2ban start - else - rc-service fail2ban restart - fi - rc-update add fail2ban - else - if ! systemctl is-active --quiet fail2ban; then - systemctl start fail2ban - else - systemctl restart fail2ban - fi - systemctl enable fail2ban - fi - - echo -e "${green}IP Limit installed and configured successfully!${plain}\n" - before_show_menu -} - -remove_iplimit() { - echo -e "${green}\t1.${plain} Only remove IP Limit configurations" - echo -e "${green}\t2.${plain} Uninstall Fail2ban and IP Limit" - echo -e "${green}\t0.${plain} Back to Main Menu" - read -rp "Choose an option: " num - case "$num" in - 1) - rm -f /etc/fail2ban/filter.d/3x-ipl.conf - rm -f /etc/fail2ban/action.d/3x-ipl.conf - rm -f /etc/fail2ban/jail.d/3x-ipl.conf - if [[ $release == "alpine" ]]; then - rc-service fail2ban restart - else - systemctl restart fail2ban - fi - echo -e "${green}IP Limit removed successfully!${plain}\n" - before_show_menu - ;; - 2) - rm -rf /etc/fail2ban - if [[ $release == "alpine" ]]; then - rc-service fail2ban stop - else - systemctl stop fail2ban - fi - case "${release}" in - ubuntu | debian | armbian) - apt-get remove -y fail2ban - apt-get purge -y fail2ban -y - apt-get autoremove -y - ;; - fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf remove fail2ban -y - dnf autoremove -y - ;; - centos) - if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum remove fail2ban -y - yum autoremove -y - else - dnf remove fail2ban -y - dnf autoremove -y - fi - ;; - arch | manjaro | parch) - pacman -Rns --noconfirm fail2ban - ;; - alpine) - apk del fail2ban - ;; - *) - echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n" - exit 1 - ;; - esac - echo -e "${green}Fail2ban and IP Limit removed successfully!${plain}\n" - before_show_menu - ;; - 0) - show_menu - ;; - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - remove_iplimit - ;; - esac -} - -show_banlog() { - local system_log="/var/log/fail2ban.log" - - echo -e "${green}Checking ban logs...${plain}\n" - - if [[ $release == "alpine" ]]; then - if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then - echo -e "${red}Fail2ban service is not running!${plain}\n" - return 1 - fi - else - if ! systemctl is-active --quiet fail2ban; then - echo -e "${red}Fail2ban service is not running!${plain}\n" - return 1 - fi - fi - - if [[ -f "$system_log" ]]; then - echo -e "${green}Recent system ban activities from fail2ban.log:${plain}" - grep "3x-ipl" "$system_log" | grep -E "Ban|Unban" | tail -n 10 || echo -e "${yellow}No recent system ban activities found${plain}" - echo "" - fi - - if [[ -f "${iplimit_banned_log_path}" ]]; then - echo -e "${green}3X-IPL ban log entries:${plain}" - if [[ -s "${iplimit_banned_log_path}" ]]; then - grep -v "INIT" "${iplimit_banned_log_path}" | tail -n 10 || echo -e "${yellow}No ban entries found${plain}" - else - echo -e "${yellow}Ban log file is empty${plain}" - fi - else - echo -e "${red}Ban log file not found at: ${iplimit_banned_log_path}${plain}" - fi - - echo -e "\n${green}Current jail status:${plain}" - fail2ban-client status 3x-ipl || echo -e "${yellow}Unable to get jail status${plain}" -} - -create_iplimit_jails() { - # Use default bantime if not passed => 30 minutes - local bantime="${1:-30}" - - # Uncomment 'allowipv6 = auto' in fail2ban.conf - sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf - - # On Debian 12+ fail2ban's default backend should be changed to systemd - if [[ "${release}" == "debian" && ${os_version} -ge 12 ]]; then - sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf - fi - - cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf -[3x-ipl] -enabled=true -backend=auto -filter=3x-ipl -action=3x-ipl -logpath=${iplimit_log_path} -maxretry=2 -findtime=32 -bantime=${bantime}m -EOF - - cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf -[Definition] -datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S -failregex = \[LIMIT_IP\]\s*Email\s*=\s*.+\s*\|\|\s*SRC\s*=\s* -ignoreregex = -EOF - - cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf -[INCLUDES] -before = iptables-allports.conf - -[Definition] -actionstart = -N f2b- - -A f2b- -j - -I -p -j f2b- - -actionstop = -D -p -j f2b- - - -X f2b- - -actioncheck = -n -L | grep -q 'f2b-[ \t]' - -actionban = -I f2b- 1 -s -j - echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = [IP] = banned for seconds." >> ${iplimit_banned_log_path} - -actionunban = -D f2b- -s -j - echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = [IP] = unbanned." >> ${iplimit_banned_log_path} - -[Init] -name = default -protocol = tcp -chain = INPUT -EOF - - echo -e "${green}Ip Limit jail files created with a bantime of ${bantime} minutes.${plain}" -} - -iplimit_remove_conflicts() { - local jail_files=( - /etc/fail2ban/jail.conf - /etc/fail2ban/jail.local - ) - - for file in "${jail_files[@]}"; do - # Check for [3x-ipl] config in jail file then remove it - if test -f "${file}" && grep -qw '3x-ipl' ${file}; then - sed -i "/\[3x-ipl\]/,/^$/d" ${file} - echo -e "${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\n" - fi - done -} - -SSH_port_forwarding() { - local URL_lists=( - "https://api4.ipify.org" - "https://ipv4.icanhazip.com" - "https://v4.api.ipinfo.io/ip" - "https://ipv4.myexternalip.com/raw" - "https://4.ident.me" - "https://check-host.net/ip" - ) - local server_ip="" - for ip_address in "${URL_lists[@]}"; do - server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]') - if [[ -n "${server_ip}" ]]; then - break - fi - done - local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') - local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') - local existing_listenIP=$(${xui_folder}/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}') - local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep -Eo 'cert: .+' | awk '{print $2}') - local existing_key=$(${xui_folder}/x-ui setting -getCert true | grep -Eo 'key: .+' | awk '{print $2}') - - local config_listenIP="" - local listen_choice="" - - if [[ -n "$existing_cert" && -n "$existing_key" ]]; then - echo -e "${green}Panel is secure with SSL.${plain}" - before_show_menu - fi - if [[ -z "$existing_cert" && -z "$existing_key" && (-z "$existing_listenIP" || "$existing_listenIP" == "0.0.0.0") ]]; then - echo -e "\n${red}Warning: No Cert and Key found! The panel is not secure.${plain}" - echo "Please obtain a certificate or set up SSH port forwarding." - fi - - if [[ -n "$existing_listenIP" && "$existing_listenIP" != "0.0.0.0" && (-z "$existing_cert" && -z "$existing_key") ]]; then - echo -e "\n${green}Current SSH Port Forwarding Configuration:${plain}" - echo -e "Standard SSH command:" - echo -e "${yellow}ssh -L 2222:${existing_listenIP}:${existing_port} root@${server_ip}${plain}" - echo -e "\nIf using SSH key:" - echo -e "${yellow}ssh -i -L 2222:${existing_listenIP}:${existing_port} root@${server_ip}${plain}" - echo -e "\nAfter connecting, access the panel at:" - echo -e "${yellow}http://localhost:2222${existing_webBasePath}${plain}" - fi - - echo -e "\nChoose an option:" - echo -e "${green}1.${plain} Set listen IP" - echo -e "${green}2.${plain} Clear listen IP" - echo -e "${green}0.${plain} Back to Main Menu" - read -rp "Choose an option: " num - - case "$num" in - 1) - if [[ -z "$existing_listenIP" || "$existing_listenIP" == "0.0.0.0" ]]; then - echo -e "\nNo listenIP configured. Choose an option:" - echo -e "1. Use default IP (127.0.0.1)" - echo -e "2. Set a custom IP" - read -rp "Select an option (1 or 2): " listen_choice - - config_listenIP="127.0.0.1" - [[ "$listen_choice" == "2" ]] && read -rp "Enter custom IP to listen on: " config_listenIP - - ${xui_folder}/x-ui setting -listenIP "${config_listenIP}" >/dev/null 2>&1 - echo -e "${green}listen IP has been set to ${config_listenIP}.${plain}" - echo -e "\n${green}SSH Port Forwarding Configuration:${plain}" - echo -e "Standard SSH command:" - echo -e "${yellow}ssh -L 2222:${config_listenIP}:${existing_port} root@${server_ip}${plain}" - echo -e "\nIf using SSH key:" - echo -e "${yellow}ssh -i -L 2222:${config_listenIP}:${existing_port} root@${server_ip}${plain}" - echo -e "\nAfter connecting, access the panel at:" - echo -e "${yellow}http://localhost:2222${existing_webBasePath}${plain}" - restart - else - config_listenIP="${existing_listenIP}" - echo -e "${green}Current listen IP is already set to ${config_listenIP}.${plain}" - fi - ;; - 2) - ${xui_folder}/x-ui setting -listenIP 0.0.0.0 >/dev/null 2>&1 - echo -e "${green}Listen IP has been cleared.${plain}" - restart - ;; - 0) - show_menu - ;; - *) - echo -e "${red}Invalid option. Please select a valid number.${plain}\n" - SSH_port_forwarding - ;; - esac -} - show_usage() { echo -e "┌────────────────────────────────────────────────────────────────┐ │ ${blue}x-ui control menu usages (subcommands):${plain} │ @@ -2269,7 +195,11 @@ show_menu() { esac } -if [[ $# > 0 ]]; then +#============================================================================= +# CLI Argument Handling +#============================================================================= + +if [[ $# -gt 0 ]]; then case $1 in "start") check_install 0 && start 0