Skip to content
Open
59 changes: 51 additions & 8 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1351,15 +1351,58 @@ EOF
fi

${xui_folder}/x-ui migrate
echo ""
echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "${green} Fail2ban / IP Limit Configuration ${plain}"
echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "1) iptables (legacy, recommended default)"
echo -e "2) nftables (modern)"
echo -e "3) Skip (IP Limit will be disabled)"

local fb_choice="1"
if [[ "$NONINTERACTIVE" == "1" ]]; then
case "${XUI_FAIL2BAN_BACKEND:-iptables}" in
nft|nftables) fb_choice="2" ;;
none|skip) fb_choice="3" ;;
*) fb_choice="1" ;;
esac
else
read -rp "Choose [1]: " fb_choice
fb_choice="${fb_choice:-1}"
fi

case "$fb_choice" in
2) export XUI_FAIL2BAN_BACKEND="nftables" ;;
3) export XUI_FAIL2BAN_BACKEND="none" ;;
*) export XUI_FAIL2BAN_BACKEND="iptables" ;;
esac

echo -e "${green}Selected backend: ${XUI_FAIL2BAN_BACKEND}${plain}"

# Persist backend choice for x-ui.sh menu operations
mkdir -p /etc/x-ui
echo "$XUI_FAIL2BAN_BACKEND" > /etc/x-ui/fail2ban_backend
chmod 600 /etc/x-ui/fail2ban_backend

}

# setup_fail2ban auto-installs and configures fail2ban for the IP Limit feature
# by invoking the freshly installed x-ui CLI. IP Limit is load-bearing on
# fail2ban (without it the panel disables the limitIp field and zeroes existing
# limits), so a fresh install should make it work out of the box, just like the
# Docker image already does. Non-fatal by design: a fail2ban failure must never
# abort the panel install.
# setup_fail2ban auto-installs and configures fail2ban + IP Limit during installation.
# It respects the XUI_FAIL2BAN_BACKEND environment variable (set in config_after_install):
# - "iptables" (default): uses legacy iptables backend
# - "nftables": uses modern nftables backend
# - "none": completely skips Fail2ban/IP Limit setup
#
# IP Limit feature depends on fail2ban. Without it the panel will disable the
# limitIp field and zero existing limits. This function is non-fatal by design —
# a failure here must never abort the overall panel installation.
#
# Called automatically after a fresh install (and via `x-ui setup-fail2ban`).
setup_fail2ban() {
if [[ "${XUI_FAIL2BAN_BACKEND}" == "none" ]]; then
echo -e "${yellow}Fail2ban/IP Limit skipped by user choice.${plain}"
return 0
fi

if [[ -n "${XUI_ENABLE_FAIL2BAN+x}" && "${XUI_ENABLE_FAIL2BAN}" != "true" ]]; then
echo -e "${yellow}XUI_ENABLE_FAIL2BAN=${XUI_ENABLE_FAIL2BAN}, skipping Fail2ban auto-setup.${plain}"
return 0
Expand All @@ -1374,7 +1417,7 @@ setup_fail2ban() {
if /usr/bin/x-ui setup-fail2ban; then
echo -e "${green}Fail2ban setup complete.${plain}"
else
echo -e "${yellow}Fail2ban setup did not finish; IP Limit stays disabled until you run 'x-ui' and open the IP Limit menu. Continuing.${plain}"
echo -e "${yellow}Fail2ban setup did not finish; IP Limit stays disabled until you run 'x-ui' menu.${plain}"
fi
return 0
}
Expand Down Expand Up @@ -1682,4 +1725,4 @@ install_x-ui() {

echo -e "${green}Running...${plain}"
install_base
install_x-ui $1
install_x-ui $1
13 changes: 13 additions & 0 deletions internal/web/service/xray.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,28 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {

var finalClients []any
var wgPeers []any
// Track processed emails to avoid duplicates in the final clients list.
processed := make(map[string]bool)

for i := range dbClients {
c := dbClients[i]

if processed[c.Email] {
continue
}

if enable, exists := enableMap[c.Email]; exists && !enable {
logger.Infof("Remove Inbound User %s due to expiration or traffic limit", c.Email)
processed[c.Email] = true
continue
}

if !c.Enable {
continue
}

processed[c.Email] = true

flow := c.Flow
if flow == "xtls-rprx-vision-udp443" {
flow = "xtls-rprx-vision"
Expand Down
188 changes: 90 additions & 98 deletions x-ui.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2255,119 +2255,91 @@ iplimit_main() {
}

setup_fail2ban_iplimit() {
# Honor the same toggle the panel uses (isFail2BanEnabled): enabled when the
# var is unset or exactly "true"; any other explicit value means the operator
# opted out, so do nothing rather than install a fail2ban the panel ignores.
# XUI_FAIL2BAN_BACKEND is set in install.sh during initial setup.
# Possible values: iptables (default), nftables, none
local backend="${XUI_FAIL2BAN_BACKEND:-iptables}"

if [[ "$backend" == "none" ]]; then
echo -e "${yellow}Fail2ban/IP Limit skipped by user choice.${plain}\n"
return 0
fi

if [[ -n "${XUI_ENABLE_FAIL2BAN+x}" && "${XUI_ENABLE_FAIL2BAN}" != "true" ]]; then
echo -e "${yellow}XUI_ENABLE_FAIL2BAN=${XUI_ENABLE_FAIL2BAN}, skipping Fail2ban setup.${plain}\n"
return 0
fi

echo -e "${green}Setting up Fail2ban (backend: ${backend})...${plain}\n"

# Install fail2ban if missing
if ! command -v fail2ban-client &> /dev/null; then
echo -e "${green}Fail2ban is not installed. Installing now...!${plain}\n"

# Install fail2ban together with nftables. Recent fail2ban packages
# default to `banaction = nftables-multiport` in /etc/fail2ban/jail.conf,
# but the `nftables` package isn't pulled in as a dependency on most
# minimal server images (Debian 12+, Ubuntu 24+, fresh RHEL-family).
# Without `nft` in PATH the default sshd jail fails to ban with
# stderr: '/bin/sh: 1: nft: not found'
# even though our own 3x-ipl jail uses iptables. Bundling the binary
# at install time prevents that confusing log spam for new installs.
echo -e "${green}Fail2ban is not installed. Installing now...${plain}"

case "${release}" in
ubuntu)
ubuntu|debian|armbian)
apt-get update
if [[ "${os_version}" -ge 2400 ]]; then
apt-get install python3-pip -y
python3 -m pip install pyasynchat --break-system-packages
fi
apt-get install fail2ban nftables -y
;;
debian)
apt-get update
if [ "$os_version" -ge 12 ]; then
apt-get install -y python3-systemd
fi
# Always install nftables (recent fail2ban defaults to nftables-multiport)
apt-get install -y fail2ban nftables
;;
armbian)
apt-get update && apt-get install fail2ban nftables -y
fedora)
# Fedora has fail2ban in main repos
dnf install -y fail2ban nftables
;;
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
if [[ "${release}" != "fedora" ]] && ! dnf repolist enabled 2> /dev/null | grep -qiw epel; then
dnf install -y epel-release \
|| dnf install -y "https://dl.fedoraproject.org/pub/epel/epel-release-latest-$(rpm -E %rhel).noarch.rpm" \
|| echo -e "${yellow}Could not enable the EPEL repository; fail2ban is only available from EPEL on this distro.${plain}"
fi
dnf makecache -y && dnf -y install fail2ban nftables
amzn|virtuozzo|rhel|almalinux|rocky|ol)
# RHEL-family: enable EPEL, use dnf (dnf available on all these)
dnf install -y epel-release
dnf install -y fail2ban nftables
;;
centos)
if [[ "${VERSION_ID}" =~ ^7 ]]; then
yum makecache -y && yum install epel-release -y
yum -y install fail2ban nftables
else
dnf makecache -y && dnf -y install fail2ban nftables
fi
# CentOS 7 doesn't have dnf, use yum and enable EPEL
yum install -y epel-release
yum install -y fail2ban nftables
;;
arch | manjaro | parch)
arch|manjaro|parch)
pacman -Sy --noconfirm fail2ban nftables
;;
alpine)
apk add fail2ban nftables
;;
*)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
echo -e "${red}Unsupported OS. Please install fail2ban manually.${plain}"
return 1
;;
esac

if ! command -v fail2ban-client &> /dev/null; then
echo -e "${red}Fail2ban installation failed.${plain}\n"
echo -e "${red}Fail2ban installation failed.${plain}"
return 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"
# Remove old conflicting 3x-ipl configs (safe on reinstall)
echo -e "${yellow}Preparing fresh IP Limit configuration...${plain}"
rm -f /etc/fail2ban/jail.d/3x-ipl.conf 2>/dev/null
rm -f /etc/fail2ban/action.d/3x-ipl.conf 2>/dev/null
rm -f /etc/fail2ban/filter.d/3x-ipl.conf 2>/dev/null

# make sure there's no conflict for jail files
iplimit_remove_conflicts
# Ensure log files exist
touch "${iplimit_banned_log_path}" "${iplimit_log_path}" 2>/dev/null || true

# Check if log file exists
if ! test -f "${iplimit_banned_log_path}"; then
touch ${iplimit_banned_log_path}
fi
# Create configuration with selected backend
create_iplimit_jails 30 "$backend"

# Check if service log file exists so fail2ban won't return error
if ! test -f "${iplimit_log_path}"; then
touch ${iplimit_log_path}
fi
# Persist chosen backend for future menu operations
mkdir -p /etc/x-ui
echo "$backend" > /etc/x-ui/fail2ban_backend
chmod 600 /etc/x-ui/fail2ban_backend

# Create the iplimit jail files
# we didn't pass the bantime here to use the default value
create_iplimit_jails

# Launching fail2ban
# Restart service
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-service fail2ban restart || rc-service fail2ban start
rc-update add fail2ban
else
if ! systemctl is-active --quiet fail2ban; then
systemctl start fail2ban
else
systemctl restart fail2ban
fi
systemctl restart fail2ban || systemctl start fail2ban
systemctl enable fail2ban
fi

echo -e "${green}IP Limit installed and configured successfully!${plain}\n"
echo -e "${green}IP Limit installed and configured successfully (backend: ${backend})!${plain}\n"
return 0
}

Expand Down Expand Up @@ -2488,15 +2460,23 @@ show_banlog() {
create_iplimit_jails() {
# Use default bantime if not passed => 30 minutes
local bantime="${1:-30}"
# backend can be "iptables" (default) or "nftables"
local backend="${2:-iptables}"

# Load persisted backend choice from prior install (if not passed as argument)
if [[ $# -lt 2 && -f /etc/x-ui/fail2ban_backend ]]; then
backend=$(cat /etc/x-ui/fail2ban_backend)
fi

# Uncomment 'allowipv6 = auto' in fail2ban.conf
# Common configuration
sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf

# On Debian 12+ and Ubuntu 22.04+ fail2ban's default backend should be changed to systemd
# On Debian 12+ and Ubuntu 22.04+ change default backend to systemd
if [[ ( "${release}" == "debian" && ${os_version} -ge 12 ) || ( "${release}" == "ubuntu" && ${os_version} -ge 2200 ) ]]; then
sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf
fi

# Jail configuration (same for both backends)
cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf
[3x-ipl]
enabled=true
Expand All @@ -2509,17 +2489,15 @@ findtime=32
bantime=${bantime}m
EOF

# Filter definition
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

# Ports to exempt from the ban so an over-limit proxy client can never lock
# the administrator out of SSH or the panel. The ban still covers every other
# TCP and UDP port (including all Xray inbounds, e.g. UDP-based Hysteria2), so
# IP-limit keeps working for inbounds added later without regenerating these files.
# Ports to exempt from the ban (SSH + panel port)
local ssh_ports
ssh_ports=$(grep -oP '^[[:space:]]*Port[[:space:]]+\K[0-9]+' /etc/ssh/sshd_config 2>/dev/null | paste -sd, -)
[[ -z "${ssh_ports}" ]] && ssh_ports="22"
Expand All @@ -2528,7 +2506,35 @@ EOF
local exempt_ports="${ssh_ports}"
[[ -n "${panel_port}" ]] && exempt_ports="${exempt_ports},${panel_port}"

cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
# === Backend-specific action configuration ===
if [[ "$backend" == "nftables" ]]; then
cat << 'EOF' > /etc/fail2ban/action.d/3x-ipl.conf
[INCLUDES]
before = nftables-common.conf

[Definition]
actionban = nft add element ip f2b 3x-ipl { <ip> }
echo "$(date +"%Y/%m/%d %H:%M:%S") BAN [Email] = <F-USER> [IP] = <ip> banned." >> /var/log/fail2ban-iplimit.log

actionunban = nft delete element ip f2b 3x-ipl { <ip> }
echo "$(date +"%Y/%m/%d %H:%M:%S") UNBAN [Email] = <F-USER> [IP] = <ip> unbanned." >> /var/log/fail2ban-iplimit.log

actionflush = nft flush set ip f2b 3x-ipl

actionstart =
nft add table ip f2b 2>/dev/null || true
nft add set ip f2b 3x-ipl { type ipv4_addr; } 2>/dev/null || true
nft add chain ip f2b 3x-ipl { type filter hook input priority 0; } 2>/dev/null || true
nft add rule ip f2b 3x-ipl ip saddr @3x-ipl drop

actionstop =
nft delete chain ip f2b 3x-ipl 2>/dev/null || true
nft delete set ip f2b 3x-ipl 2>/dev/null || true
nft delete table ip f2b 2>/dev/null || true
EOF
else
# iptables — original behavior preserved
cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
[INCLUDES]
before = iptables-allports.conf

Expand Down Expand Up @@ -2556,23 +2562,9 @@ name = default
chain = INPUT
exemptports = ${exempt_ports}
EOF
fi

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
echo -e "${green}Ip Limit jail files created with a bantime of ${bantime} minutes (backend: ${backend}).${plain}"
}

SSH_port_forwarding() {
Expand Down Expand Up @@ -3606,4 +3598,4 @@ if [[ $# > 0 ]]; then
esac
else
show_menu
fi
fi