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.
-
+
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