3x-ui/x-ui.sh

2680 lines
88 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
red='\033[0;31m'
green='\033[0;32m'
blue='\033[0;34m'
yellow='\033[0;33m'
plain='\033[0m'
#添加一些基础函数
function LOGD() {
echo -e "${yellow}[调试] $* ${plain}"
}
function LOGE() {
echo -e "${red}[错误] $* ${plain}"
}
function LOGI() {
echo -e "${green}[信息] $* ${plain}"
}
# 端口辅助函数:检测端口监听及所属进程(尽力而为)
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
}
# 域名/IP 验证简单辅助函数
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
}
# 检查 root 权限
[[ $EUID -ne 0 ]] && LOGE "错误:必须使用 root 权限运行此脚本!\n" && exit 1
# 检查操作系统并设置发行版变量
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 "无法识别操作系统,请联系作者!" >&2
exit 1
fi
echo "操作系统版本:$release"
os_version=""
os_version=$(grep "^VERSION_ID" /etc/os-release | cut -d '=' -f2 | tr -d '"' | tr -d '.')
# 声明变量
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 [默认 $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
}
confirm_restart() {
confirm "重启面板,注意:重启面板也会重启 xray" "y"
if [[ $? == 0 ]]; then
restart
else
show_menu
fi
}
before_show_menu() {
echo && echo -n -e "${yellow}按回车键返回主菜单:${plain}" && read -r temp
show_menu
}
install() {
bash <(curl -Ls https://raw.githubusercontent.com/Sora39831/3x-ui/main/install.sh)
if [[ $? == 0 ]]; then
if [[ $# == 0 ]]; then
start
else
start 0
fi
fi
}
update() {
confirm "此功能将更新所有 x-ui 组件到最新版本,数据不会丢失。是否继续?" "y"
if [[ $? != 0 ]]; then
LOGE "已取消"
if [[ $# == 0 ]]; then
before_show_menu
fi
return 0
fi
bash <(curl -Ls https://raw.githubusercontent.com/Sora39831/3x-ui/main/update.sh)
if [[ $? == 0 ]]; then
LOGI "更新完成,面板已自动重启"
before_show_menu
fi
}
update_menu() {
echo -e "${yellow}正在更新菜单${plain}"
confirm "此功能将更新菜单到最新版本。" "y"
if [[ $? != 0 ]]; then
LOGE "已取消"
if [[ $# == 0 ]]; then
before_show_menu
fi
return 0
fi
curl -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/Sora39831/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}更新成功。面板已自动重启。${plain}"
exit 0
else
echo -e "${red}更新菜单失败。${plain}"
return 1
fi
}
legacy_version() {
echo -n "输入面板版本(例如 2.4.0"
read -r tag_version
if [ -z "$tag_version" ]; then
echo "面板版本不能为空,退出。"
exit 1
fi
# 使用输入的面板版本号下载
install_command="bash <(curl -Ls "https://raw.githubusercontent.com/Sora39831/3x-ui/v$tag_version/install.sh") v$tag_version"
echo "正在下载并安装面板版本 $tag_version..."
eval $install_command
}
# 处理脚本文件删除的函数
delete_script() {
rm "$0" # 删除脚本自身
exit 1
}
uninstall() {
confirm "确定要卸载面板吗xray 也会被卸载!" "n"
if [[ $? != 0 ]]; then
if [[ $# == 0 ]]; then
show_menu
fi
return 0
fi
# 询问是否吊销证书
local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2>/dev/null)
if [[ -n "$domains" ]]; then
echo ""
echo "检测到以下证书:"
echo "$domains"
confirm "是否要吊销所有证书?" "n"
if [[ $? == 0 ]]; then
for domain in $domains; do
~/.acme.sh/acme.sh --revoke -d "${domain}" 2>/dev/null
LOGI "域名 $domain 的证书已吊销"
done
rm -rf /root/cert/
fi
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 "卸载成功。\n"
echo "如需再次安装面板,可使用以下命令:"
echo -e "${green}bash <(curl -Ls https://raw.githubusercontent.com/Sora39831/3x-ui/master/install.sh)${plain}"
echo ""
# 捕获 SIGTERM 信号
trap delete_script SIGTERM
delete_script
}
reset_user() {
confirm "确定要重置面板的用户名和密码吗?" "n"
if [[ $? != 0 ]]; then
if [[ $# == 0 ]]; then
show_menu
fi
return 0
fi
read -rp "请设置登录用户名 [默认随机生成]" config_account
[[ -z $config_account ]] && config_account=$(gen_random_string 10)
read -rp "请设置登录密码 [默认随机生成]" config_password
[[ -z $config_password ]] && config_password=$(gen_random_string 18)
read -rp "是否要禁用当前配置的双因素认证?(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 "双因素认证已禁用。"
fi
echo -e "面板登录用户名已重置为:${green} ${config_account} ${plain}"
echo -e "面板登录密码已重置为:${green} ${config_password} ${plain}"
echo -e "${green} 请使用新的登录用户名和密码访问 X-UI 面板。请牢记!${plain}"
confirm_restart
}
gen_random_string() {
local length="$1"
openssl rand -base64 $(( length * 2 )) \
| tr -dc 'a-zA-Z0-9' \
| head -c "$length"
}
reset_webbasepath() {
echo -e "${yellow}正在重置 Web 路径${plain}"
read -rp "确定要重置 Web 路径吗?(y/n)" confirm
if [[ $confirm != "y" && $confirm != "Y" ]]; then
echo -e "${yellow}操作已取消。${plain}"
return
fi
config_webBasePath=$(gen_random_string 18)
# 应用新的 Web 路径设置
${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}" >/dev/null 2>&1
echo -e "Web 路径已重置为:${green}${config_webBasePath}${plain}"
echo -e "${green}请使用新的 Web 路径访问面板。${plain}"
restart
}
reset_config() {
confirm "确定要重置所有面板设置吗?这将清除面板的端口、路径、证书等配置,但不会删除账户数据和流量数据。" "n"
if [[ $? != 0 ]]; then
if [[ $# == 0 ]]; then
show_menu
fi
return 0
fi
# 重置面板证书配置
${xui_folder}/x-ui cert -reset 2>/dev/null
# 重置面板设置(端口、路径等)
${xui_folder}/x-ui setting -reset
echo -e "所有面板设置已重置为默认值。"
echo -e "${yellow}面板将使用默认端口 2053 和随机用户名/密码重新启动。${plain}"
restart
}
check_config() {
local info=$(${xui_folder}/x-ui setting -show true)
if [[ $? != 0 ]]; then
LOGE "获取当前设置出错,请查看日志"
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}访问地址https://${domain}:${existing_port}${existing_webBasePath}${plain}"
else
echo -e "${green}访问地址https://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
fi
else
echo -e "${red}⚠ 警告:未配置 SSL 证书!${plain}"
echo -e "${yellow}您可以为 IP 地址获取 Let's Encrypt 证书(有效期约 6 天,自动续期)。${plain}"
read -rp "现在为 IP 生成 SSL 证书?[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}访问地址https://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
# ssl_cert_issue_for_ip 已经重启面板,但确保其正在运行
start >/dev/null 2>&1
else
LOGE "IP 证书配置失败。"
echo -e "${yellow}您可以通过选项 19SSL 证书管理)重试。${plain}"
start >/dev/null 2>&1
fi
else
echo -e "${yellow}访问地址http://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
echo -e "${yellow}出于安全考虑,请使用选项 19SSL 证书管理)配置 SSL 证书${plain}"
fi
fi
}
set_port() {
echo -n "输入端口号[1-65535]"
read -r port
if [[ -z "${port}" ]]; then
LOGD "已取消"
before_show_menu
else
${xui_folder}/x-ui setting -port ${port}
echo -e "端口已设置,请立即重启面板,并使用新端口 ${green}${port}${plain} 访问 Web 面板"
confirm_restart
fi
}
start() {
check_status
if [[ $? == 0 ]]; then
echo ""
LOGI "面板正在运行,无需重复启动,如需重启请选择重启"
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 启动成功"
else
LOGE "面板启动失败,可能是因为启动时间超过两秒,请稍后查看日志信息"
fi
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
}
stop() {
check_status
if [[ $? == 1 ]]; then
echo ""
LOGI "面板已停止,无需重复停止!"
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 和 xray 已停止"
else
LOGE "面板停止失败,可能是因为停止时间超过两秒,请稍后查看日志信息"
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 和 xray 重启成功"
else
LOGE "面板重启失败,可能是因为启动时间超过两秒,请稍后查看日志信息"
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
}
restart_xray() {
systemctl reload x-ui
LOGI "xray-core 重启信号已发送,请查看日志信息确认 xray 是否重启成功"
sleep 2
show_xray_status
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 default
else
systemctl enable x-ui
fi
if [[ $? == 0 ]]; then
LOGI "x-ui 设置开机自启成功"
else
LOGE "x-ui 设置开机自启失败"
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 已取消开机自启"
else
LOGE "x-ui 取消开机自启失败"
fi
if [[ $# == 0 ]]; then
before_show_menu
fi
}
show_log() {
if [[ $release == "alpine" ]]; then
echo -e "${green}\t1.${plain} 调试日志"
echo -e "${green}\t0.${plain} 返回主菜单"
read -rp "请选择:" 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}无效选项,请选择有效数字。${plain}\n"
show_log
;;
esac
else
echo -e "${green}\t1.${plain} 调试日志"
echo -e "${green}\t2.${plain} 清除所有日志"
echo -e "${green}\t0.${plain} 返回主菜单"
read -rp "请选择:" 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 "所有日志已清除。"
restart
;;
*)
echo -e "${red}无效选项,请选择有效数字。${plain}\n"
show_log
;;
esac
fi
}
bbr_menu() {
echo -e "${green}\t1.${plain} 启用 BBR"
echo -e "${green}\t2.${plain} 禁用 BBR"
echo -e "${green}\t0.${plain} 返回主菜单"
read -rp "请选择:" choice
case "$choice" in
0)
show_menu
;;
1)
enable_bbr
bbr_menu
;;
2)
disable_bbr
bbr_menu
;;
*)
echo -e "${red}无效选项,请选择有效数字。${plain}\n"
bbr_menu
;;
esac
}
disable_bbr() {
if [[ $(sysctl -n net.ipv4.tcp_congestion_control) != "bbr" ]] || [[ ! $(sysctl -n net.core.default_qdisc) =~ ^(fq|cake)$ ]]; then
echo -e "${yellow}BBR 当前未启用。${plain}"
before_show_menu
fi
if [ -f "/etc/sysctl.d/99-bbr-x-ui.conf" ]; then
old_settings=$(head -1 /etc/sysctl.d/99-bbr-x-ui.conf | tr -d '#')
sysctl -w net.core.default_qdisc="${old_settings%:*}"
sysctl -w net.ipv4.tcp_congestion_control="${old_settings#*:}"
rm /etc/sysctl.d/99-bbr-x-ui.conf
sysctl --system
else
# 用 CUBIC 配置替换 BBR
if [ -f "/etc/sysctl.conf" ]; then
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
sysctl -p
fi
fi
if [[ $(sysctl -n net.ipv4.tcp_congestion_control) != "bbr" ]]; then
echo -e "${green}BBR 已成功替换为 CUBIC。${plain}"
else
echo -e "${red}替换 BBR 为 CUBIC 失败。请检查系统配置。${plain}"
fi
}
enable_bbr() {
if [[ $(sysctl -n net.ipv4.tcp_congestion_control) == "bbr" ]] && [[ $(sysctl -n net.core.default_qdisc) =~ ^(fq|cake)$ ]]; then
echo -e "${green}BBR 已启用!${plain}"
before_show_menu
fi
# 启用 BBR
if [ -d "/etc/sysctl.d/" ]; then
{
echo "#$(sysctl -n net.core.default_qdisc):$(sysctl -n net.ipv4.tcp_congestion_control)"
echo "net.core.default_qdisc = fq"
echo "net.ipv4.tcp_congestion_control = bbr"
} > "/etc/sysctl.d/99-bbr-x-ui.conf"
if [ -f "/etc/sysctl.conf" ]; then
# 备份 sysctl.conf 中的旧设置(如果有)
sed -i 's/^net.core.default_qdisc/# &/' /etc/sysctl.conf
sed -i 's/^net.ipv4.tcp_congestion_control/# &/' /etc/sysctl.conf
fi
sysctl --system
else
sed -i '/net.core.default_qdisc/d' /etc/sysctl.conf
sed -i '/net.ipv4.tcp_congestion_control/d' /etc/sysctl.conf
echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf
sysctl -p
fi
# 验证 BBR 是否已启用
if [[ $(sysctl -n net.ipv4.tcp_congestion_control) == "bbr" ]]; then
echo -e "${green}BBR 已成功启用。${plain}"
else
echo -e "${red}启用 BBR 失败。请检查系统配置。${plain}"
fi
}
update_shell() {
curl -fLRo /usr/bin/x-ui -z /usr/bin/x-ui https://github.com/Sora39831/3x-ui/raw/main/x-ui.sh
if [[ $? != 0 ]]; then
echo ""
LOGE "下载脚本失败,请检查机器是否能连接 GitHub"
before_show_menu
else
chmod +x /usr/bin/x-ui
LOGI "升级脚本成功,请重新运行脚本"
before_show_menu
fi
}
# 0: 运中, 1: 未运行, 2: 未安装
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 "面板已安装,请勿重复安装"
if [[ $# == 0 ]]; then
before_show_menu
fi
return 1
else
return 0
fi
}
check_install() {
check_status
if [[ $? == 2 ]]; then
echo ""
LOGE "请先安装面板"
if [[ $# == 0 ]]; then
before_show_menu
fi
return 1
else
return 0
fi
}
show_status() {
check_status
case $? in
0)
echo -e "面板状态:${green}运行中${plain}"
show_enable_status
;;
1)
echo -e "面板状态:${yellow}未运行${plain}"
show_enable_status
;;
2)
echo -e "面板状态:${red}未安装${plain}"
;;
esac
show_xray_status
}
show_enable_status() {
check_enabled
if [[ $? == 0 ]]; then
echo -e "开机自启:${green}${plain}"
else
echo -e "开机自启:${red}${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 状态:${green}运行中${plain}"
else
echo -e "xray 状态:${red}未运行${plain}"
fi
}
firewall_menu() {
echo -e "${green}\t1.${plain} ${green}安装${plain} 防火墙"
echo -e "${green}\t2.${plain} 端口列表 [带编号]"
echo -e "${green}\t3.${plain} ${green}开放${plain} 端口"
echo -e "${green}\t4.${plain} ${red}删除${plain} 列表中的端口"
echo -e "${green}\t5.${plain} ${green}启用${plain} 防火墙"
echo -e "${green}\t6.${plain} ${red}禁用${plain} 防火墙"
echo -e "${green}\t7.${plain} 防火墙状态"
echo -e "${green}\t0.${plain} 返回主菜单"
read -rp "请选择:" 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_wall_menu
;;
5)
ufw enable
firewall_menu
;;
6)
ufw disable
firewall_menu
;;
7)
ufw status verbose
firewall_menu
;;
*)
echo -e "${red}无效选项,请选择有效数字。${plain}\n"
firewall_menu
;;
esac
}
install_firewall() {
if ! command -v ufw &>/dev/null; then
echo "ufw 防火墙未安装,正在安装..."
apt-get update
apt-get install -y ufw
else
echo "ufw 防火墙已安装"
fi
# 检查防火墙是否处于非活动状态
if ufw status | grep -q "Status: active"; then
echo "防火墙已激活"
else
echo "正在激活防火墙..."
# 开放必要端口
ufw allow ssh
ufw allow http
ufw allow https
ufw allow 2053/tcp #webPort
ufw allow 2096/tcp #subport
# 启用防火墙
ufw --force enable
fi
}
open_ports() {
# 提示用户输入要开放的端口
read -rp "输入要开放的端口(例如 80,443,2053 或范围 400-500" ports
# 检查输入是否有效
if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then
echo "错误:无效输入。请输入逗号分隔的端口列表或端口范围(例如 80,443,2053 或 400-500。" >&2
exit 1
fi
# 使用 ufw 开放指定端口
IFS=',' read -ra PORT_LIST <<<"$ports"
for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then
# 将范围拆分为起始和结束端口
start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2)
# 开放端口范围
ufw allow $start_port:$end_port/tcp
ufw allow $start_port:$end_port/udp
else
# 开放单个端口
ufw allow "$port"
fi
done
# 确认端口已开放
echo "已开放指定端口:"
for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then
start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2)
# 检查端口范围是否已成功开放
(ufw status | grep -q "$start_port:$end_port") && echo "$start_port-$end_port"
else
# 检查单个端口是否已成功开放
(ufw status | grep -q "$port") && echo "$port"
fi
done
}
delete_ports() {
# 显示当前带编号的规则
echo "当前 UFW 规则:"
ufw status numbered
# 询问用户删除方式
echo "您想通过以下哪种方式删除规则:"
echo "1) 规则编号"
echo "2) 端口号"
read -rp "请输入选择1 或 2" choice
if [[ $choice -eq 1 ]]; then
# 按规则编号删除
read -rp "输入要删除的规则编号1, 2 等):" rule_numbers
# 验证输入
if ! [[ $rule_numbers =~ ^([0-9]+)(,[0-9]+)*$ ]]; then
echo "错误:无效输入。请输入逗号分隔的规则编号列表。" >&2
exit 1
fi
# 将编号拆分为数组
IFS=',' read -ra RULE_NUMBERS <<<"$rule_numbers"
for rule_number in "${RULE_NUMBERS[@]}"; do
# 按编号删除规则
ufw delete "$rule_number" || echo "删除规则编号 $rule_number 失败"
done
echo "已删除所选规则。"
elif [[ $choice -eq 2 ]]; then
# 按端口删除
read -rp "输入要删除的端口(例如 80,443,2053 或范围 400-500" ports
# 验证输入
if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then
echo "错误:无效输入。请输入逗号分隔的端口列表或端口范围(例如 80,443,2053 或 400-500。" >&2
exit 1
fi
# 将端口拆分为数组
IFS=',' read -ra PORT_LIST <<<"$ports"
for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then
# 拆分端口范围
start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2)
# 删除端口范围
ufw delete allow $start_port:$end_port/tcp
ufw delete allow $start_port:$end_port/udp
else
# 删除单个端口
ufw delete allow "$port"
fi
done
# 确认删除
echo "已删除指定端口:"
for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then
start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2)
# 检查端口范围是否已删除
(ufw status | grep -q "$start_port:$end_port") || echo "$start_port-$end_port"
else
# 检查单个端口是否已删除
(ufw status | grep -q "$port") || echo "$port"
fi
done
else
echo "${red}错误:${plain} 无效选择。请输入 1 或 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
# 移除后缀获取远程文件名(例如 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} 全部更新"
echo -e "${green}\t0.${plain} 返回主菜单"
read -rp "请选择:" choice
case "$choice" in
0)
show_menu
;;
1)
update_geofiles "main"
echo -e "${green}Loyalsoldier 数据集更新成功!${plain}"
restart
;;
2)
update_geofiles "IR"
echo -e "${green}chocolate4u 数据集更新成功!${plain}"
restart
;;
3)
update_geofiles "RU"
echo -e "${green}runetfreedom 数据集更新成功!${plain}"
restart
;;
4)
update_all_geofiles
echo -e "${green}所有 geo 文件更新成功!${plain}"
restart
;;
*)
echo -e "${red}无效选项,请选择有效数字。${plain}\n"
update_geo
;;
esac
before_show_menu
}
install_acme() {
# 检查 acme.sh 是否已安装
if command -v ~/.acme.sh/acme.sh &>/dev/null; then
LOGI "acme.sh 已安装。"
return 0
fi
LOGI "正在安装 acme.sh..."
cd ~ || return 1
curl -s https://get.acme.sh | sh
if [ $? -ne 0 ]; then
LOGE "安装 acme.sh 失败。"
return 1
else
LOGI "安装 acme.sh 成功。"
fi
return 0
}
ssl_cert_issue_main() {
echo -e "${green}\t1.${plain} 获取 SSL域名"
echo -e "${green}\t2.${plain} 吊销证书"
echo -e "${green}\t3.${plain} 强制续期"
echo -e "${green}\t4.${plain} 查看已有域名"
echo -e "${green}\t5.${plain} 为面板设置证书路径"
echo -e "${green}\t6.${plain} 为 IP 地址获取 SSL6 天证书,自动续期)"
echo -e "${green}\t0.${plain} 返回主菜单"
read -rp "请选择:" 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 "未找到可吊销的证书。"
else
echo "已有域名:"
echo "$domains"
read -rp "请输入要吊销证书的域名:" domain
if echo "$domains" | grep -qw "$domain"; then
~/.acme.sh/acme.sh --revoke -d ${domain}
LOGI "域名 $domain 的证书已吊销"
else
echo "输入的域名无效。"
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 "未找到可续期的证书。"
else
echo "已有域名:"
echo "$domains"
read -rp "请输入要续期 SSL 证书的域名:" domain
if echo "$domains" | grep -qw "$domain"; then
~/.acme.sh/acme.sh --renew -d ${domain} --force
LOGI "域名 $domain 的证书已强制续期"
else
echo "输入的域名无效。"
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 "未找到证书。"
else
echo "已有域名及其路径:"
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}"
echo -e "\t证书路径${cert_path}"
echo -e "\t私钥路径${key_path}"
else
echo -e "域名:${domain} - 证书或密钥缺失。"
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 "未找到证书。"
else
echo "可选域名:"
echo "$domains"
read -rp "请选择一个域名设置面板路径:" 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 "域名 $domain 的面板路径已设置"
echo " - 证书文件:$webCertFile"
echo " - 私钥文件:$webKeyFile"
restart
else
echo "未找到域名 $domain 的证书或私钥。"
fi
else
echo "输入的域名无效。"
fi
fi
ssl_cert_issue_main
;;
6)
echo -e "${yellow}Let's Encrypt IP 地址 SSL 证书${plain}"
echo -e "将使用短期配置文件为服务器 IP 获取证书。"
echo -e "${yellow}证书有效期约 6 天,通过 acme.sh cron 自动续期。${plain}"
echo -e "${yellow}80 端口必须开放且可从外网访问。${plain}"
confirm "是否继续?" "y"
if [[ $? == 0 ]]; then
ssl_cert_issue_for_ip
fi
ssl_cert_issue_main
;;
*)
echo -e "${red}无效选项,请选择有效数字。${plain}\n"
ssl_cert_issue_main
;;
esac
}
ssl_cert_issue_for_ip() {
LOGI "开始为服务器 IP 自动生成 SSL 证书..."
LOGI "使用 Let's Encrypt 短期配置文件(约 6 天有效期,自动续期)"
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}')
# 获取服务器 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 "获取服务器 IP 地址失败"
return 1
fi
LOGI "检测到服务器 IP${server_ip}"
# 询问可选的 IPv6
local ipv6_addr=""
read -rp "是否包含 IPv6 地址?(留空跳过):" ipv6_addr
ipv6_addr="${ipv6_addr// /}" # 去除空格
# 先检查 acme.sh
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
LOGI "未找到 acme.sh正在安装..."
install_acme
if [ $? -ne 0 ]; then
LOGE "安装 acme.sh 失败"
return 1
fi
fi
# 安装 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 "不支持的系统,无法自动安装 socat"
;;
esac
# 创建证书目录
certPath="/root/cert/ip"
mkdir -p "$certPath"
# 构建域名参数
local domain_args="-d ${server_ip}"
if [[ -n "$ipv6_addr" ]] && is_ipv6 "$ipv6_addr"; then
domain_args="${domain_args} -d ${ipv6_addr}"
LOGI "包含 IPv6 地址:${ipv6_addr}"
fi
# 选择 HTTP-01 监听端口(默认 80允许自定义
local WebPort=""
read -rp "用于 ACME HTTP-01 监听的端口(默认 80" WebPort
WebPort="${WebPort:-80}"
if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then
LOGE "无效端口,回退到 80。"
WebPort=80
fi
LOGI "使用端口 ${WebPort} 为 IP 签发证书:${server_ip}"
if [[ "${WebPort}" -ne 80 ]]; then
LOGI "提醒Let's Encrypt 仍然连接 80 端口;请将外部 80 端口转发到 ${WebPort}"
fi
while true; do
if is_port_in_use "${WebPort}"; then
LOGI "端口 ${WebPort} 当前被占用。"
local alt_port=""
read -rp "请输入另一个端口供 acme.sh 独立监听(留空取消):" alt_port
alt_port="${alt_port// /}"
if [[ -z "${alt_port}" ]]; then
LOGE "端口 ${WebPort} 被占用,无法继续。"
return 1
fi
if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then
LOGE "无效端口。"
return 1
fi
WebPort="${alt_port}"
continue
else
LOGI "端口 ${WebPort} 空闲,可以进行独立验证。"
break
fi
done
# 重载命令 - 续期后重启面板
local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null"
# 使用短期配置文件为 IP 签发证书
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
~/.acme.sh/acme.sh --issue \
${domain_args} \
--standalone \
--server letsencrypt \
--certificate-profile shortlived \
--days 6 \
--httpport ${WebPort} \
--force
if [ $? -ne 0 ]; then
LOGE "为 IP 签发证书失败:${server_ip}"
LOGE "请确保端口 ${WebPort} 已开放且服务器可从外网访问"
# 清理 acme.sh 数据IPv4 和 IPv6
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 "IP 证书签发成功:${server_ip}"
fi
# 安装证书
# 注意acme.sh 可能在 reloadcmd 失败时报告 "Reload error" 并返回非零退出码,
# 但证书文件仍然已安装。我们通过检查文件而非退出码来判断。
~/.acme.sh/acme.sh --installcert -d ${server_ip} \
--key-file "${certPath}/privkey.pem" \
--fullchain-file "${certPath}/fullchain.pem" \
--reloadcmd "${reloadCmd}" 2>&1 || true
# 验证证书文件存在(不依赖退出码 - reloadcmd 失败会导致非零)
if [[ ! -f "${certPath}/fullchain.pem" || ! -f "${certPath}/privkey.pem" ]]; then
LOGE "安装后未找到证书文件"
# 清理 acme.sh 数据IPv4 和 IPv6
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 "证书文件安装成功"
# 启用自动续期
~/.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
# 为面板设置证书路径
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 "面板证书已配置"
LOGI " - 证书文件:$webCertFile"
LOGI " - 私钥文件:$webKeyFile"
LOGI " - 有效期:约 6 天(通过 acme.sh cron 自动续期)"
echo -e "${green}访问地址https://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
LOGI "面板将重启以应用 SSL 证书..."
restart
return 0
else
LOGE "安装后未找到证书文件"
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}')
# 先检查 acme.sh
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
echo "未找到 acme.sh将安装它"
install_acme
if [ $? -ne 0 ]; then
LOGE "安装 acme.sh 失败,请查看日志"
exit 1
fi
fi
# 安装 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 "不支持的系统,无法自动安装 socat"
;;
esac
if [ $? -ne 0 ]; then
LOGE "安装 socat 失败,请查看日志"
exit 1
else
LOGI "安装 socat 成功..."
fi
# 获取域名并验证
local domain=""
while true; do
read -rp "请输入您的域名:" domain
domain="${domain// /}" # 去除空格
if [[ -z "$domain" ]]; then
LOGE "域名不能为空,请重试。"
continue
fi
if ! is_domain "$domain"; then
LOGE "无效的域名格式:${domain},请输入有效的域名。"
continue
fi
break
done
LOGD "您的域名是:${domain},正在检查..."
# 检查是否已存在证书
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
if [ "${currentCert}" == "${domain}" ]; then
local certInfo=$(~/.acme.sh/acme.sh --list)
LOGE "系统已有该域名的证书,无法重复签发。当前证书详情:"
LOGI "$certInfo"
exit 1
else
LOGI "您的域名已准备好签发证书..."
fi
# 创建证书目录
certPath="/root/cert/${domain}"
if [ ! -d "$certPath" ]; then
mkdir -p "$certPath"
else
rm -rf "$certPath"
mkdir -p "$certPath"
fi
# 获取独立服务器端口号
local WebPort=80
read -rp "请选择要使用的端口(默认 80" WebPort
if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then
LOGE "输入 ${WebPort} 无效,将使用默认端口 80。"
WebPort=80
fi
LOGI "将使用端口:${WebPort} 签发证书。请确保此端口已开放。"
# 签发证书
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force
if [ $? -ne 0 ]; then
LOGE "签发证书失败,请查看日志。"
rm -rf ~/.acme.sh/${domain}
exit 1
else
LOGE "证书签发成功,正在安装证书..."
fi
reloadCmd="x-ui restart"
LOGI "ACME 默认 --reloadcmd 为:${yellow}x-ui restart"
LOGI "此命令将在每次签发和续期证书时运行。"
read -rp "是否要修改 ACME 的 --reloadcmd(y/n)" setReloadcmd
if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then
echo -e "\n${green}\t1.${plain} 预设systemctl reload nginx ; x-ui restart"
echo -e "${green}\t2.${plain} 输入自定义命令"
echo -e "${green}\t0.${plain} 保持默认 reloadcmd"
read -rp "请选择:" choice
case "$choice" in
1)
LOGI "Reloadcmd 设为systemctl reload nginx ; x-ui restart"
reloadCmd="systemctl reload nginx ; x-ui restart"
;;
2)
LOGD "建议将 x-ui restart 放在最后,这样即使其他服务失败也不会报错"
read -rp "请输入自定义的 reloadcmd例如systemctl reload nginx ; x-ui restart" reloadCmd
LOGI "您的 reloadcmd 为:${reloadCmd}"
;;
*)
LOGI "保持默认 reloadcmd"
;;
esac
fi
# 安装证书
~/.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 "安装证书失败,退出。"
rm -rf ~/.acme.sh/${domain}
exit 1
else
LOGI "安装证书成功,正在启用自动续期..."
fi
# 启用自动续期
~/.acme.sh/acme.sh --upgrade --auto-upgrade
if [ $? -ne 0 ]; then
LOGE "自动续期设置失败,证书详情:"
ls -lah cert/*
chmod 600 $certPath/privkey.pem
chmod 644 $certPath/fullchain.pem
exit 1
else
LOGI "自动续期设置成功,证书详情:"
ls -lah cert/*
chmod 600 $certPath/privkey.pem
chmod 644 $certPath/fullchain.pem
fi
# 证书安装成功后提示用户为面板设置证书路径
read -rp "是否要为面板设置此证书?(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 "域名 $domain 的面板路径已设置"
LOGI " - 证书文件:$webCertFile"
LOGI " - 私钥文件:$webKeyFile"
echo -e "${green}访问地址https://${domain}:${existing_port}${existing_webBasePath}${plain}"
restart
else
LOGE "错误:未找到域名 $domain 的证书或私钥文件。"
fi
else
LOGI "跳过面板路径设置。"
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 "****** 使用说明 ******"
LOGI "请按照以下步骤完成操作:"
LOGI "1. Cloudflare 注册邮箱。"
LOGI "2. Cloudflare 全局 API 密钥。"
LOGI "3. 域名。"
LOGI "4. 证书签发后,将提示您为面板设置证书(可选)。"
LOGI "5. 脚本还支持安装后自动续期 SSL 证书。"
confirm "请确认信息并继续?[y/n]" "y"
if [ $? -eq 0 ]; then
# 检查 acme.sh
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
echo "未找到 acme.sh将安装它。"
install_acme
if [ $? -ne 0 ]; then
LOGE "安装 acme.sh 失败,请查看日志。"
exit 1
fi
fi
CF_Domain=""
LOGD "请设置域名:"
read -rp "在此输入您的域名:" CF_Domain
LOGD "您的域名设置为:${CF_Domain}"
# 设置 Cloudflare API 信息
CF_GlobalKey=""
CF_AccountEmail=""
LOGD "请设置 API 密钥:"
read -rp "在此输入您的密钥:" CF_GlobalKey
LOGD "您的 API 密钥为:${CF_GlobalKey}"
LOGD "请设置注册邮箱:"
read -rp "在此输入您的邮箱:" CF_AccountEmail
LOGD "您的注册邮箱为:${CF_AccountEmail}"
# 设置默认 CA 为 Let's Encrypt
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force
if [ $? -ne 0 ]; then
LOGE "设置默认 CALet's Encrypt失败脚本退出..."
exit 1
fi
export CF_Key="${CF_GlobalKey}"
export CF_Email="${CF_AccountEmail}"
# 使用 Cloudflare DNS 签发证书
~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log --force
if [ $? -ne 0 ]; then
LOGE "证书签发失败,脚本退出..."
exit 1
else
LOGI "证书签发成功,正在安装..."
fi
# 安装证书
certPath="/root/cert/${CF_Domain}"
if [ -d "$certPath" ]; then
rm -rf ${certPath}
fi
mkdir -p ${certPath}
if [ $? -ne 0 ]; then
LOGE "创建目录失败:${certPath}"
exit 1
fi
reloadCmd="x-ui restart"
LOGI "ACME 默认 --reloadcmd 为:${yellow}x-ui restart"
LOGI "此命令将在每次签发和续期证书时运行。"
read -rp "是否要修改 ACME 的 --reloadcmd(y/n)" setReloadcmd
if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then
echo -e "\n${green}\t1.${plain} 预设systemctl reload nginx ; x-ui restart"
echo -e "${green}\t2.${plain} 输入自定义命令"
echo -e "${green}\t0.${plain} 保持默认 reloadcmd"
read -rp "请选择:" choice
case "$choice" in
1)
LOGI "Reloadcmd 设为systemctl reload nginx ; x-ui restart"
reloadCmd="systemctl reload nginx ; x-ui restart"
;;
2)
LOGD "建议将 x-ui restart 放在最后,这样即使其他服务失败也不会报错"
read -rp "请输入自定义的 reloadcmd例如systemctl reload nginx ; x-ui restart" reloadCmd
LOGI "您的 reloadcmd 为:${reloadCmd}"
;;
*)
LOGI "保持默认 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 "证书安装失败,脚本退出..."
exit 1
else
LOGI "证书安装成功,正在启用自动更新..."
fi
# 启用自动更新
~/.acme.sh/acme.sh --upgrade --auto-upgrade
if [ $? -ne 0 ]; then
LOGE "自动更新设置失败,脚本退出..."
exit 1
else
LOGI "证书已安装并开启自动续期。详情如下:"
ls -lah ${certPath}/*
chmod 600 ${certPath}/privkey.pem
chmod 644 ${certPath}/fullchain.pem
fi
# 证书安装成功后提示用户为面板设置证书路径
read -rp "是否要为面板设置此证书?(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 "域名 $CF_Domain 的面板路径已设置"
LOGI " - 证书文件:$webCertFile"
LOGI " - 私钥文件:$webKeyFile"
echo -e "${green}访问地址https://${CF_Domain}:${existing_port}${existing_webBasePath}${plain}"
restart
else
LOGE "错误:未找到域名 $CF_Domain 的证书或私钥文件。"
fi
else
LOGI "跳过面板路径设置。"
fi
else
show_menu
fi
}
run_speedtest() {
# 检查 Speedtest 是否已安装
if ! command -v speedtest &>/dev/null; then
# 如未安装,确定安装方式
if command -v snap &>/dev/null; then
# 使用 snap 安装 Speedtest
echo "正在使用 snap 安装 Speedtest..."
snap install speedtest
else
# 回退到使用包管理器
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 "错误:未找到包管理器。您可能需要手动安装 Speedtest。"
return 1
else
echo "正在使用 $pkg_manager 安装 Speedtest..."
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} 安装 Fail2ban 并配置 IP 限制"
echo -e "${green}\t2.${plain} 修改封禁时长"
echo -e "${green}\t3.${plain} 解封所有人"
echo -e "${green}\t4.${plain} 封禁日志"
echo -e "${green}\t5.${plain} 封禁指定 IP 地址"
echo -e "${green}\t6.${plain} 解封指定 IP 地址"
echo -e "${green}\t7.${plain} 实时日志"
echo -e "${green}\t8.${plain} 服务状态"
echo -e "${green}\t9.${plain} 重启服务"
echo -e "${green}\t10.${plain} 卸载 Fail2ban 和 IP 限制"
echo -e "${green}\t0.${plain} 返回主菜单"
read -rp "请选择:" choice
case "$choice" in
0)
show_menu
;;
1)
confirm "是否继续安装 Fail2ban 和 IP 限制?" "y"
if [[ $? == 0 ]]; then
install_iplimit
else
iplimit_main
fi
;;
2)
read -rp "请输入新的封禁时长(分钟)[默认 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} 不是数字!请重试。${plain}"
fi
iplimit_main
;;
3)
confirm "是否继续解封 IP 限制中的所有人?" "y"
if [[ $? == 0 ]]; then
fail2ban-client reload --restart --unban 3x-ipl
truncate -s 0 "${iplimit_banned_log_path}"
echo -e "${green}所有用户已成功解封。${plain}"
iplimit_main
else
echo -e "${yellow}已取消。${plain}"
fi
iplimit_main
;;
4)
show_banlog
iplimit_main
;;
5)
read -rp "输入要封禁的 IP 地址:" 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 地址 ${ban_ip} 已成功封禁。${plain}"
else
echo -e "${red}IP 地址格式无效!请重试。${plain}"
fi
iplimit_main
;;
6)
read -rp "输入要解封的 IP 地址:" 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 地址 ${unban_ip} 已成功解封。${plain}"
else
echo -e "${red}IP 地址格式无效!请重试。${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}无效选项,请选择有效数字。${plain}\n"
iplimit_main
;;
esac
}
install_iplimit() {
if ! command -v fail2ban-client &>/dev/null; then
echo -e "${green}Fail2ban 未安装,正在安装...${plain}\n"
# 检查操作系统并安装必要的软件包
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}不支持的操作系统。请检查脚本并手动安装必要的软件包。${plain}\n"
exit 1
;;
esac
if ! command -v fail2ban-client &>/dev/null; then
echo -e "${red}Fail2ban 安装失败。${plain}\n"
exit 1
fi
echo -e "${green}Fail2ban 安装成功!${plain}\n"
else
echo -e "${yellow}Fail2ban 已安装。${plain}\n"
fi
echo -e "${green}正在配置 IP 限制...${plain}\n"
# 确保 jail 文件没有冲突
iplimit_remove_conflicts
# 检查日志文件是否存在
if ! test -f "${iplimit_banned_log_path}"; then
touch ${iplimit_banned_log_path}
fi
# 检查服务日志文件是否存在,以免 fail2ban 报错
if ! test -f "${iplimit_log_path}"; then
touch ${iplimit_log_path}
fi
# 创建 iplimit jail 文件
# 此处不传递 bantime 以使用默认值
create_iplimit_jails
# 启动 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 限制安装并配置成功!${plain}\n"
before_show_menu
}
remove_iplimit() {
echo -e "${green}\t1.${plain} 仅移除 IP 限制配置"
echo -e "${green}\t2.${plain} 卸载 Fail2ban 和 IP 限制"
echo -e "${green}\t0.${plain} 返回主菜单"
read -rp "请选择:" 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 限制已成功移除!${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}不支持的操作系统。请手动卸载 Fail2ban。${plain}\n"
exit 1
;;
esac
echo -e "${green}Fail2ban 和 IP 限制已成功移除!${plain}\n"
before_show_menu
;;
0)
show_menu
;;
*)
echo -e "${red}无效选项,请选择有效数字。${plain}\n"
remove_iplimit
;;
esac
}
show_banlog() {
local system_log="/var/log/fail2ban.log"
echo -e "${green}正在检查封禁日志...${plain}\n"
if [[ $release == "alpine" ]]; then
if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then
echo -e "${red}Fail2ban 服务未运行!${plain}\n"
return 1
fi
else
if ! systemctl is-active --quiet fail2ban; then
echo -e "${red}Fail2ban 服务未运行!${plain}\n"
return 1
fi
fi
if [[ -f "$system_log" ]]; then
echo -e "${green}来自 fail2ban.log 的最近系统封禁活动:${plain}"
grep "3x-ipl" "$system_log" | grep -E "Ban|Unban" | tail -n 10 || echo -e "${yellow}未找到最近的系统封禁活动${plain}"
echo ""
fi
if [[ -f "${iplimit_banned_log_path}" ]]; then
echo -e "${green}3X-IPL 封禁日志条目:${plain}"
if [[ -s "${iplimit_banned_log_path}" ]]; then
grep -v "INIT" "${iplimit_banned_log_path}" | tail -n 10 || echo -e "${yellow}未找到封禁记录${plain}"
else
echo -e "${yellow}封禁日志文件为空${plain}"
fi
else
echo -e "${red}未找到封禁日志文件:${iplimit_banned_log_path}${plain}"
fi
echo -e "\n${green}当前 jail 状态:${plain}"
fail2ban-client status 3x-ipl || echo -e "${yellow}无法获取 jail 状态${plain}"
}
create_iplimit_jails() {
# 如未传递则使用默认封禁时长 => 30 分钟
local bantime="${1:-30}"
# 取消 fail2ban.conf 中 'allowipv6 = auto' 的注释
sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf
# 在 Debian 12+ 上fail2ban 的默认后端应更改为 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*<F-USER>.+</F-USER>\s*\|\|\s*Disconnecting OLD IP\s*=\s*<ADDR>\s*\|\|\s*Timestamp\s*=\s*\d+
ignoreregex =
EOF
cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
[INCLUDES]
before = iptables-allports.conf
[Definition]
actionstart = <iptables> -N f2b-<name>
<iptables> -A f2b-<name> -j <returntype>
<iptables> -I <chain> -p <protocol> -j f2b-<name>
actionstop = <iptables> -D <chain> -p <protocol> -j f2b-<name>
<actionflush>
<iptables> -X f2b-<name>
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]'
actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = <F-USER> [IP] = <ip> banned for <bantime> seconds." >> ${iplimit_banned_log_path}
actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = <F-USER> [IP] = <ip> unbanned." >> ${iplimit_banned_log_path}
[Init]
name = default
protocol = tcp
chain = INPUT
EOF
echo -e "${green}IP 限制 jail 文件已创建,封禁时长为 ${bantime} 分钟。${plain}"
}
iplimit_remove_conflicts() {
local jail_files=(
/etc/fail2ban/jail.conf
/etc/fail2ban/jail.local
)
for file in "${jail_files[@]}"; do
# 检查 jail 文件中是否存在 [3x-ipl] 配置,如存在则移除
if test -f "${file}" && grep -qw '3x-ipl' ${file}; then
sed -i "/\[3x-ipl\]/,/^$/d" ${file}
echo -e "${yellow}正在移除 jail (${file}) 中 [3x-ipl] 的冲突配置!${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
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
local http_code=$(echo "$response" | tail -n1)
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
server_ip="${ip_result}"
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}面板已配置 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}警告:未找到证书和密钥!面板不安全。${plain}"
echo "请获取证书或设置 SSH 端口转发。"
fi
if [[ -n "$existing_listenIP" && "$existing_listenIP" != "0.0.0.0" && (-z "$existing_cert" && -z "$existing_key") ]]; then
echo -e "\n${green}当前 SSH 端口转发配置:${plain}"
echo -e "标准 SSH 命令:"
echo -e "${yellow}ssh -L 2222:${existing_listenIP}:${existing_port} root@${server_ip}${plain}"
echo -e "\n如果使用 SSH 密钥:"
echo -e "${yellow}ssh -i <sshkeypath> -L 2222:${existing_listenIP}:${existing_port} root@${server_ip}${plain}"
echo -e "\n连接后通过以下地址访问面板"
echo -e "${yellow}http://localhost:2222${existing_webBasePath}${plain}"
fi
echo -e "\n请选择"
echo -e "${green}1.${plain} 设置监听 IP"
echo -e "${green}2.${plain} 清除监听 IP"
echo -e "${green}0.${plain} 返回主菜单"
read -rp "请选择:" num
case "$num" in
1)
if [[ -z "$existing_listenIP" || "$existing_listenIP" == "0.0.0.0" ]]; then
echo -e "\n未配置 listenIP。请选择"
echo -e "1. 使用默认 IP (127.0.0.1)"
echo -e "2. 设置自定义 IP"
read -rp "请选择1 或 2" listen_choice
config_listenIP="127.0.0.1"
[[ "$listen_choice" == "2" ]] && read -rp "输入自定义监听 IP" config_listenIP
${xui_folder}/x-ui setting -listenIP "${config_listenIP}" >/dev/null 2>&1
echo -e "${green}监听 IP 已设置为 ${config_listenIP}${plain}"
echo -e "\n${green}SSH 端口转发配置:${plain}"
echo -e "标准 SSH 命令:"
echo -e "${yellow}ssh -L 2222:${config_listenIP}:${existing_port} root@${server_ip}${plain}"
echo -e "\n如果使用 SSH 密钥:"
echo -e "${yellow}ssh -i <sshkeypath> -L 2222:${config_listenIP}:${existing_port} root@${server_ip}${plain}"
echo -e "\n连接后通过以下地址访问面板"
echo -e "${yellow}http://localhost:2222${existing_webBasePath}${plain}"
restart
else
config_listenIP="${existing_listenIP}"
echo -e "${green}当前监听 IP 已设置为 ${config_listenIP}${plain}"
fi
;;
2)
${xui_folder}/x-ui setting -listenIP 0.0.0.0 >/dev/null 2>&1
echo -e "${green}监听 IP 已清除。${plain}"
restart
;;
0)
show_menu
;;
*)
echo -e "${red}无效选项,请选择有效数字。${plain}\n"
SSH_port_forwarding
;;
esac
}
show_usage() {
echo -e "┌────────────────────────────────────────────────────────────────┐
${blue}x-ui 管理菜单用法(子命令):${plain}
│ │
${blue}x-ui${plain} - 管理脚本 │
${blue}x-ui start${plain} - 启动 │
${blue}x-ui stop${plain} - 停止 │
${blue}x-ui restart${plain} - 重启 │
| ${blue}x-ui restart-xray${plain} - 重启 Xray │
${blue}x-ui status${plain} - 查看当前状态 │
${blue}x-ui settings${plain} - 查看当前设置 │
${blue}x-ui enable${plain} - 设置开机自启 │
${blue}x-ui disable${plain} - 取消开机自启 │
${blue}x-ui log${plain} - 查看日志 │
${blue}x-ui banlog${plain} - 查看 Fail2ban 封禁日志 │
${blue}x-ui update${plain} - 更新 │
${blue}x-ui update-all-geofiles${plain} - 更新所有 geo 文件 │
${blue}x-ui legacy${plain} - 安装旧版本 │
${blue}x-ui install${plain} - 安装 │
${blue}x-ui uninstall${plain} - 卸载 │
└────────────────────────────────────────────────────────────────┘"
}
# Read dbType from /etc/x-ui/x-ui.json using the Go binary
read_json_dbtype() {
local db_type
db_type=$(${xui_folder}/x-ui setting -showDbType 2>/dev/null)
if [ -z "$db_type" ]; then
echo "sqlite"
else
echo "$db_type"
fi
}
# Show current database status
db_show_status() {
local current_type=$(read_json_dbtype)
echo -e "${green}当前数据库类型: ${current_type}${plain}"
if [ "$current_type" = "mariadb" ]; then
local json_path="/etc/x-ui/x-ui.json"
if command -v jq >/dev/null 2>&1; then
local host=$(jq -r '.other.dbHost // "127.0.0.1"' "$json_path" 2>/dev/null)
local port=$(jq -r '.other.dbPort // "3306"' "$json_path" 2>/dev/null)
local dbname=$(jq -r '.other.dbName // "3xui"' "$json_path" 2>/dev/null)
else
local host=$(grep -o '"dbHost"[[:space:]]*:[[:space:]]*"[^"]*"' "$json_path" 2>/dev/null | tail -1 | sed 's/.*"\([^"]*\)"$/\1/')
local port=$(grep -o '"dbPort"[[:space:]]*:[[:space:]]*"[^"]*"' "$json_path" 2>/dev/null | tail -1 | sed 's/.*"\([^"]*\)"$/\1/')
local dbname=$(grep -o '"dbName"[[:space:]]*:[[:space:]]*"[^"]*"' "$json_path" 2>/dev/null | tail -1 | sed 's/.*"\([^"]*\)"$/\1/')
fi
echo -e "${green}MariaDB 主机: ${host:-127.0.0.1}:${port:-3306}${plain}"
echo -e "${green}数据库名: ${dbname:-3xui}${plain}"
fi
}
# Check if MariaDB is installed (server or client)
check_mariadb_installed() {
if command -v mariadb >/dev/null 2>&1 || command -v mysql >/dev/null 2>&1; then
return 0
fi
if systemctl is-active --quiet mariadb 2>/dev/null || systemctl is-active --quiet mysql 2>/dev/null; then
return 0
fi
return 1
}
# Install MariaDB server based on distro
install_mariadb() {
echo -e "${green}正在安装 MariaDB...${plain}"
case "${release}" in
ubuntu | debian | linuxmint)
apt-get update -y && apt-get install -y mariadb-server mariadb-client
;;
centos | rhel | almalinux | rocky | ol | alinux | amzn)
if command -v dnf >/dev/null 2>&1; then
dnf install -y mariadb-server mariadb
else
yum install -y mariadb-server mariadb
fi
;;
fedora)
dnf install -y mariadb-server mariadb
;;
arch | manjaro)
pacman -Sy --noconfirm mariadb
mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql >/dev/null 2>&1
;;
opensuse* | sles)
zypper install -y mariadb-server mariadb-client
;;
alpine)
apk add mariadb mariadb-client
mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql >/dev/null 2>&1
;;
*)
echo -e "${red}不支持的发行版: ${release},请手动安装 MariaDB${plain}"
return 1
;;
esac
local ret=$?
if [ $ret -eq 0 ]; then
echo -e "${green}MariaDB 安装成功${plain}"
else
echo -e "${red}MariaDB 安装失败${plain}"
fi
return $ret
}
# Start and enable MariaDB service
start_mariadb_service() {
local svc_name=""
if systemctl list-unit-files | grep -q "^mariadb.service"; then
svc_name="mariadb"
elif systemctl list-unit-files | grep -q "^mysql.service"; then
svc_name="mysql"
fi
if [ -n "$svc_name" ]; then
systemctl start "$svc_name" 2>/dev/null
systemctl enable "$svc_name" 2>/dev/null
else
# alpine / no systemd
rc-service mariadb start 2>/dev/null
rc-update add mariadb 2>/dev/null
fi
}
# Test MariaDB connection with given credentials
# Args: host port user pass
test_mariadb_connection() {
local host="$1" port="$2" user="$3" pass="$4"
mariadb -h "$host" -P "$port" -u "$user" -p"$pass" -e "SELECT 1;" >/dev/null 2>&1
return $?
}
# Test connection to a specific database, create if not exists
# Args: host port user pass dbname
ensure_mariadb_database() {
local host="$1" port="$2" user="$3" pass="$4" dbname="$5"
# Check if database exists
local exists
exists=$(mariadb -h "$host" -P "$port" -u "$user" -p"$pass" -N -B -e \
"SELECT COUNT(*) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='${dbname}';" 2>/dev/null)
if [ "$exists" = "1" ]; then
echo -e "${green}数据库 '${dbname}' 已存在${plain}"
return 0
fi
# Create database
echo -e "${green}正在创建数据库 '${dbname}'...${plain}"
mariadb -h "$host" -P "$port" -u "$user" -p"$pass" -e \
"CREATE DATABASE \`${dbname}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 2>/dev/null
if [ $? -eq 0 ]; then
echo -e "${green}数据库 '${dbname}' 创建成功${plain}"
return 0
else
echo -e "${red}数据库 '${dbname}' 创建失败${plain}"
return 1
fi
}
# Switch to MariaDB
db_switch_to_mariadb() {
local current_type=$(read_json_dbtype)
if [ "$current_type" = "mariadb" ]; then
echo -e "${yellow}当前已经是 MariaDB${plain}"
db_menu
return
fi
# Step 1: Check MariaDB installation
if ! check_mariadb_installed; then
echo -e "${yellow}未检测到 MariaDB${plain}"
confirm "是否安装 MariaDB" "y"
if [ $? -ne 0 ]; then
echo -e "${yellow}已取消安装,返回数据库菜单${plain}"
db_menu
return
fi
install_mariadb
if [ $? -ne 0 ]; then
echo -e "${red}MariaDB 安装失败,返回数据库菜单${plain}"
db_menu
return
fi
start_mariadb_service
if ! check_mariadb_installed; then
echo -e "${red}MariaDB 安装后仍无法检测到,请手动检查${plain}"
db_menu
return
fi
echo -e "${green}MariaDB 已安装并启动${plain}"
else
echo -e "${green}MariaDB 已安装${plain}"
# Ensure service is running
start_mariadb_service
fi
# Step 2: Collect connection info
echo -e "${green}请输入 MariaDB 连接信息(直接回车使用默认值):${plain}"
read -rp "MariaDB IP默认 127.0.0.1: " db_host
db_host=${db_host:-127.0.0.1}
read -rp "MariaDB 端口(默认 3306: " db_port
db_port=${db_port:-3306}
read -rp "MariaDB 用户名: " db_user
if [ -z "$db_user" ]; then
echo -e "${red}用户名不能为空${plain}"
db_menu
return
fi
read -rsp "MariaDB 密码: " db_pass
echo
if [ -z "$db_pass" ]; then
echo -e "${red}密码不能为空${plain}"
db_menu
return
fi
read -rp "数据库名(默认 3xui: " db_name
db_name=${db_name:-3xui}
# Step 3: Test connection
echo -e "${green}正在测试数据库连接...${plain}"
if ! test_mariadb_connection "$db_host" "$db_port" "$db_user" "$db_pass"; then
echo -e "${red}无法连接到 MariaDB请检查用户名、密码及主机信息${plain}"
db_menu
return
fi
echo -e "${green}数据库连接成功${plain}"
# Step 4: Ensure database exists
if ! ensure_mariadb_database "$db_host" "$db_port" "$db_user" "$db_pass" "$db_name"; then
db_menu
return
fi
# Step 5: Save config and migrate
echo -e "${green}正在配置 MariaDB 连接...${plain}"
XUI_DB_PASSWORD="$db_pass" ${xui_folder}/x-ui setting -dbHost "$db_host" -dbPort "$db_port" -dbUser "$db_user" -dbName "$db_name" >/dev/null 2>&1
echo -e "${green}正在迁移数据从 SQLite 到 MariaDB...${plain}"
${xui_folder}/x-ui migrate-db -direction sqlite-to-mariadb
if [ $? -eq 0 ]; then
echo -e "${green}数据库切换成功,正在重启面板...${plain}"
${xui_folder}/x-ui setting -dbType mariadb >/dev/null 2>&1
restart
else
echo -e "${red}数据迁移失败,保持 SQLite 不变${plain}"
restart
fi
}
# Switch to SQLite
db_switch_to_sqlite() {
local current_type=$(read_json_dbtype)
if [ "$current_type" = "sqlite" ]; then
echo -e "${yellow}当前已经是 SQLite${plain}"
db_menu
return
fi
echo -e "${green}正在迁移数据从 MariaDB 到 SQLite...${plain}"
${xui_folder}/x-ui migrate-db -direction mariadb-to-sqlite
if [ $? -eq 0 ]; then
echo -e "${green}数据库切换成功,正在重启面板...${plain}"
${xui_folder}/x-ui setting -dbType sqlite >/dev/null 2>&1
restart
else
echo -e "${red}数据迁移失败,保持 MariaDB 不变${plain}"
db_menu
fi
}
# Database management menu
db_menu() {
local current_type=$(read_json_dbtype)
echo -e "
╔────────────────────────────────────────────────╗
${green}数据库管理${plain}
│────────────────────────────────────────────────│
${green}0.${plain} 返回主菜单 │
${green}1.${plain} 查看当前数据库类型(当前: ${current_type}
${green}2.${plain} 切换到 MariaDB │
${green}3.${plain} 切换到 SQLite │
╚════════════════════════════════════════════════╝
"
read -rp "请输入选择 [0-3]" num
case "${num}" in
0)
show_menu
;;
1)
db_show_status
db_menu
;;
2)
db_switch_to_mariadb
;;
3)
db_switch_to_sqlite
;;
*)
echo -e "${red}无效选项,请选择有效数字。${plain}\n"
db_menu
;;
esac
}
show_menu() {
echo -e "
╔────────────────────────────────────────────────╗
${green}3X-UI 面板管理脚本${plain}
${green}0.${plain} 退出脚本 │
│────────────────────────────────────────────────│
${green}1.${plain} 安装 │
${green}2.${plain} 更新 │
${green}3.${plain} 更新菜单 │
${green}4.${plain} 安装旧版本 │
${green}5.${plain} 卸载 │
│────────────────────────────────────────────────│
${green}6.${plain} 重置用户名和密码 │
${green}7.${plain} 重置 Web 路径 │
${green}8.${plain} 重置设置 │
${green}9.${plain} 修改端口 │
${green}10.${plain} 查看当前设置 │
│────────────────────────────────────────────────│
${green}11.${plain} 启动 │
${green}12.${plain} 停止 │
${green}13.${plain} 重启 │
| ${green}14.${plain} 重启 Xray │
${green}15.${plain} 查看状态 │
${green}16.${plain} 日志管理 │
│────────────────────────────────────────────────│
${green}17.${plain} 设置开机自启 │
${green}18.${plain} 取消开机自启 │
│────────────────────────────────────────────────│
${green}19.${plain} SSL 证书管理 │
${green}20.${plain} Cloudflare SSL 证书 │
${green}21.${plain} IP 限制管理 │
${green}22.${plain} 防火墙管理 │
${green}23.${plain} SSH 端口转发管理 │
│────────────────────────────────────────────────│
${green}24.${plain} BBR 管理 │
${green}25.${plain} 更新 Geo 文件 │
${green}26.${plain} 网速测试 (Speedtest) │
│────────────────────────────────────────────────│
${green}27.${plain} 数据库管理 │
╚────────────────────────────────────────────────╝
"
show_status
echo && read -rp "请输入选择 [0-27]" num
case "${num}" in
0)
exit 0
;;
1)
check_uninstall && install
;;
2)
check_install && update
;;
3)
check_install && update_menu
;;
4)
check_install && legacy_version
;;
5)
check_install && uninstall
;;
6)
check_install && reset_user
;;
7)
check_install && reset_webbasepath
;;
8)
check_install && reset_config
;;
9)
check_install && set_port
;;
10)
check_install && check_config
;;
11)
check_install && start
;;
12)
check_install && stop
;;
13)
check_install && restart
;;
14)
check_install && restart_xray
;;
15)
check_install && status
;;
16)
check_install && show_log
;;
17)
check_install && enable
;;
18)
check_install && disable
;;
19)
ssl_cert_issue_main
;;
20)
ssl_cert_issue_CF
;;
21)
iplimit_main
;;
22)
firewall_menu
;;
23)
SSH_port_forwarding
;;
24)
bbr_menu
;;
25)
update_geo
;;
26)
run_speedtest
;;
27)
check_install && db_menu
;;
*)
LOGE "请输入正确的数字 [0-27]"
;;
esac
}
if [[ $# > 0 ]]; then
case $1 in
"start")
check_install 0 && start 0
;;
"stop")
check_install 0 && stop 0
;;
"restart")
check_install 0 && restart 0
;;
"restart-xray")
check_install 0 && restart_xray 0
;;
"status")
check_install 0 && status 0
;;
"settings")
check_install 0 && check_config 0
;;
"enable")
check_install 0 && enable 0
;;
"disable")
check_install 0 && disable 0
;;
"log")
check_install 0 && show_log 0
;;
"banlog")
check_install 0 && show_banlog 0
;;
"update")
check_install 0 && update 0
;;
"legacy")
check_install 0 && legacy_version 0
;;
"install")
check_uninstall 0 && install 0
;;
"uninstall")
check_install 0 && uninstall 0
;;
"update-all-geofiles")
check_install 0 && update_all_geofiles 0 && restart 0
;;
*) show_usage ;;
esac
else
show_menu
fi