From 8fa97e52fb1d8ca1926abd9d2fdccb98d8a6895d Mon Sep 17 00:00:00 2001
From: Turtle hermit <160160283+neonixandrein@users.noreply.github.com>
Date: Sat, 11 Jul 2026 23:45:03 +0400
Subject: [PATCH 1/4] Update install.sh
---
install.sh | 51 ++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 44 insertions(+), 7 deletions(-)
diff --git a/install.sh b/install.sh
index ffa0479e07..4b63ec9ae7 100644
--- a/install.sh
+++ b/install.sh
@@ -1333,15 +1333,52 @@ 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}"}
}
-# 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
@@ -1356,7 +1393,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
}
From 848998e73857ce6a8c6855654314b50d9a01780d Mon Sep 17 00:00:00 2001
From: Turtle hermit <160160283+neonixandrein@users.noreply.github.com>
Date: Sat, 11 Jul 2026 23:58:50 +0400
Subject: [PATCH 2/4] Update x-ui.sh
---
x-ui.sh | 167 +++++++++++++++++++++++---------------------------------
1 file changed, 67 insertions(+), 100 deletions(-)
diff --git a/x-ui.sh b/x-ui.sh
index 4125198c47..2730ebc959 100644
--- a/x-ui.sh
+++ b/x-ui.sh
@@ -2249,119 +2249,81 @@ 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
- apt-get install -y fail2ban nftables
- ;;
- armbian)
- apt-get update && apt-get install fail2ban nftables -y
- ;;
- 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}"
+ if [[ "$backend" == "nftables" ]]; then
+ apt-get install -y fail2ban nftables
+ else
+ apt-get install -y fail2ban iptables
fi
- dnf makecache -y && dnf -y install fail2ban nftables
;;
- centos)
- if [[ "${VERSION_ID}" =~ ^7 ]]; then
- yum makecache -y && yum install epel-release -y
- yum -y install fail2ban nftables
+ fedora|amzn|virtuozzo|rhel|almalinux|rocky|ol|centos)
+ if [[ "$backend" == "nftables" ]]; then
+ dnf install -y fail2ban nftables
else
- dnf makecache -y && dnf -y install fail2ban nftables
+ dnf install -y fail2ban iptables
fi
;;
- arch | manjaro | parch)
- pacman -Sy --noconfirm fail2ban nftables
+ arch|manjaro|parch)
+ pacman -Sy --noconfirm fail2ban ${backend}
;;
alpine)
- apk add fail2ban nftables
+ apk add fail2ban ${backend}
;;
*)
- 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"
-
- # make sure there's no conflict for jail files
+ # Remove old conflicting 3x-ipl configs (safe on reinstall)
+ echo -e "${yellow}Preparing fresh IP Limit configuration...${plain}"
iplimit_remove_conflicts
+ rm -f /etc/fail2ban/action.d/3x-ipl.conf
- # Check if log file exists
- if ! test -f "${iplimit_banned_log_path}"; then
- touch ${iplimit_banned_log_path}
- fi
+ # Ensure log files exist
+ touch "${iplimit_banned_log_path}" "${iplimit_log_path}" 2>/dev/null || true
- # 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 configuration with selected backend
+ create_iplimit_jails 30 "$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
}
@@ -2482,15 +2444,18 @@ 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}"
- # 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
@@ -2503,6 +2468,7 @@ 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
@@ -2510,10 +2476,7 @@ failregex = \[LIMIT_IP\]\s*Email\s*=\s*.+\s*\|\|\s*Disconnect
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"
@@ -2522,7 +2485,25 @@ 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-multiport.conf
+
+[Definition]
+actionstart = add chain ip f2b- { type filter hook input priority 0 \; }
+actionstop = delete chain ip f2b-
+
+actionban = add rule ip f2b- ip saddr tcp dport != { } reject
+ add rule ip f2b- ip saddr udp dport != { } reject
+ echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = [IP] = ..." >> ${iplimit_banned_log_path}
+
+actionunban = delete rule ip f2b- ip saddr ...
+EOF
+ else
+ # iptables — original behavior preserved
+ cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
[INCLUDES]
before = iptables-allports.conf
@@ -2550,23 +2531,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() {
From 6f0c649e4425479ea7591c31179eac58fea6639a Mon Sep 17 00:00:00 2001
From: Turtle hermit <160160283+neonixandrein@users.noreply.github.com>
Date: Sun, 12 Jul 2026 00:05:24 +0400
Subject: [PATCH 3/4] Update install.sh
---
install.sh | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/install.sh b/install.sh
index 4b63ec9ae7..4f3fcce146 100644
--- a/install.sh
+++ b/install.sh
@@ -1359,7 +1359,8 @@ EOF
*) export XUI_FAIL2BAN_BACKEND="iptables" ;;
esac
- echo -e "${green}Selected backend: ${XUI_FAIL2BAN_BACKEND}${plain}"}
+ echo -e "${green}Selected backend: ${XUI_FAIL2BAN_BACKEND}${plain}"
+
}
# setup_fail2ban auto-installs and configures fail2ban + IP Limit during installation.
From 0dbc06fb6c4ca50ab9e6a2104a78139bbbe0b5d7 Mon Sep 17 00:00:00 2001
From: Turtle hermit <160160283+neonixandrein@users.noreply.github.com>
Date: Sun, 12 Jul 2026 00:05:24 +0400
Subject: [PATCH 4/4] Update install.sh
---
install.sh | 10 ++++++--
x-ui.sh | 73 ++++++++++++++++++++++++++++++++++++------------------
2 files changed, 57 insertions(+), 26 deletions(-)
diff --git a/install.sh b/install.sh
index 4b63ec9ae7..9874029b07 100644
--- a/install.sh
+++ b/install.sh
@@ -1359,7 +1359,13 @@ EOF
*) export XUI_FAIL2BAN_BACKEND="iptables" ;;
esac
- echo -e "${green}Selected backend: ${XUI_FAIL2BAN_BACKEND}${plain}"}
+ 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 + IP Limit during installation.
@@ -1701,4 +1707,4 @@ install_x-ui() {
echo -e "${green}Running...${plain}"
install_base
-install_x-ui $1
+install_x-ui $1
\ No newline at end of file
diff --git a/x-ui.sh b/x-ui.sh
index 2730ebc959..1db7b1569e 100644
--- a/x-ui.sh
+++ b/x-ui.sh
@@ -2272,24 +2272,28 @@ setup_fail2ban_iplimit() {
case "${release}" in
ubuntu|debian|armbian)
apt-get update
- if [[ "$backend" == "nftables" ]]; then
- apt-get install -y fail2ban nftables
- else
- apt-get install -y fail2ban iptables
- fi
+ # Always install nftables (recent fail2ban defaults to nftables-multiport)
+ apt-get install -y fail2ban nftables
;;
- fedora|amzn|virtuozzo|rhel|almalinux|rocky|ol|centos)
- if [[ "$backend" == "nftables" ]]; then
- dnf install -y fail2ban nftables
- else
- dnf install -y fail2ban iptables
- fi
+ fedora)
+ # Fedora has fail2ban in main repos
+ dnf install -y 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)
+ # CentOS 7 doesn't have dnf, use yum and enable EPEL
+ yum install -y epel-release
+ yum install -y fail2ban nftables
;;
arch|manjaro|parch)
- pacman -Sy --noconfirm fail2ban ${backend}
+ pacman -Sy --noconfirm fail2ban nftables
;;
alpine)
- apk add fail2ban ${backend}
+ apk add fail2ban nftables
;;
*)
echo -e "${red}Unsupported OS. Please install fail2ban manually.${plain}"
@@ -2305,8 +2309,9 @@ setup_fail2ban_iplimit() {
# Remove old conflicting 3x-ipl configs (safe on reinstall)
echo -e "${yellow}Preparing fresh IP Limit configuration...${plain}"
- iplimit_remove_conflicts
- rm -f /etc/fail2ban/action.d/3x-ipl.conf
+ 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
# Ensure log files exist
touch "${iplimit_banned_log_path}" "${iplimit_log_path}" 2>/dev/null || true
@@ -2314,6 +2319,11 @@ setup_fail2ban_iplimit() {
# Create configuration with selected backend
create_iplimit_jails 30 "$backend"
+ # 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
+
# Restart service
if [[ $release == "alpine" ]]; then
rc-service fail2ban restart || rc-service fail2ban start
@@ -2447,6 +2457,11 @@ create_iplimit_jails() {
# 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
+
# Common configuration
sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf
@@ -2487,19 +2502,29 @@ EOF
# === Backend-specific action configuration ===
if [[ "$backend" == "nftables" ]]; then
- cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
+ cat << 'EOF' > /etc/fail2ban/action.d/3x-ipl.conf
[INCLUDES]
-before = nftables-multiport.conf
+before = nftables-common.conf
[Definition]
-actionstart = add chain ip f2b- { type filter hook input priority 0 \; }
-actionstop = delete chain ip f2b-
+actionban = nft add element ip f2b 3x-ipl { }
+ echo "$(date +"%Y/%m/%d %H:%M:%S") BAN [Email] = [IP] = banned." >> /var/log/fail2ban-iplimit.log
-actionban = add rule ip f2b- ip saddr tcp dport != { } reject
- add rule ip f2b- ip saddr udp dport != { } reject
- echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = [IP] = ..." >> ${iplimit_banned_log_path}
+actionunban = nft delete element ip f2b 3x-ipl { }
+ echo "$(date +"%Y/%m/%d %H:%M:%S") UNBAN [Email] = [IP] = unbanned." >> /var/log/fail2ban-iplimit.log
-actionunban = delete rule ip f2b- ip saddr ...
+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
@@ -3567,4 +3592,4 @@ if [[ $# > 0 ]]; then
esac
else
show_menu
-fi
+fi
\ No newline at end of file