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