1
0
Эх сурвалжийг харах

fix(fail2ban): fix banning regression and Docker zero-jail issue

- DockerEntrypoint.sh: create jail.d/filter.d/action.d config files
  before starting fail2ban so Docker containers no longer start with
  0 active jails (fixes #4134)

- x-ui.sh create_iplimit_jails: lower maxretry from 2 to 1 so
  fail2ban bans on the first log entry; with maxretry=2 and the
  partitionLiveIps logic the second occurrence could arrive after the
  32 s findtime window, silently preventing any ban (fixes #4163)

- x-ui.sh: fix datepattern (%%Y -> %Y) so fail2ban parses the Go
  log timestamp correctly instead of looking for a literal %%Y string

- x-ui.sh / DockerEntrypoint.sh: fix date command in actionban /
  actionunban echo (%%Y -> %Y) so the ban log records actual dates

- check_client_ip_job.go: replace log.SetOutput / log.SetFlags on
  the global standard-library logger with a local log.New instance,
  eliminating the dangling closed-file-handle between calls and
  stopping unrelated stdlib log output from polluting 3xipl.log

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
MHSanaei 2 өдөр өмнө
parent
commit
3349dcbc13

+ 1 - 0
.gitignore

@@ -1,6 +1,7 @@
 # Ignore editor and IDE settings
 .idea/
 .vscode/
+.claude/
 .cache/
 .sync*
 

+ 56 - 2
DockerEntrypoint.sh

@@ -1,7 +1,61 @@
 #!/bin/sh
 
-# Start fail2ban
-[ $XUI_ENABLE_FAIL2BAN == "true" ] && fail2ban-client -x start
+# Start fail2ban with the 3x-ipl jail
+if [ "$XUI_ENABLE_FAIL2BAN" = "true" ]; then
+    LOG_FOLDER="${XUI_LOG_FOLDER:-/var/log/x-ui}"
+    mkdir -p "$LOG_FOLDER"
+    touch "$LOG_FOLDER/3xipl.log" "$LOG_FOLDER/3xipl-banned.log"
+
+    mkdir -p /etc/fail2ban/jail.d /etc/fail2ban/filter.d /etc/fail2ban/action.d
+
+    cat > /etc/fail2ban/jail.d/3x-ipl.conf << EOF
+[3x-ipl]
+enabled=true
+backend=auto
+filter=3x-ipl
+action=3x-ipl
+logpath=$LOG_FOLDER/3xipl.log
+maxretry=1
+findtime=32
+bantime=30m
+EOF
+
+    cat > /etc/fail2ban/filter.d/3x-ipl.conf << 'EOF'
+[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 > /etc/fail2ban/action.d/3x-ipl.conf << EOF
+[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." >> $LOG_FOLDER/3xipl-banned.log
+
+actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
+              echo "\$(date +"%Y/%m/%d %H:%M:%S")   UNBAN   [Email] = <F-USER> [IP] = <ip> unbanned." >> $LOG_FOLDER/3xipl-banned.log
+
+[Init]
+name = default
+protocol = tcp
+chain = INPUT
+EOF
+
+    fail2ban-client -x start
+fi
 
 # Run x-ui
 exec /app/x-ui

+ 13 - 11
web/job/check_client_ip_job.go

@@ -403,16 +403,6 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
 	shouldCleanLog := false
 	j.disAllowedIps = []string{}
 
-	// Open log file
-	logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
-	if err != nil {
-		logger.Errorf("failed to open IP limit log file: %s", err)
-		return false
-	}
-	defer logIpFile.Close()
-	log.SetOutput(logIpFile)
-	log.SetFlags(log.LstdFlags)
-
 	// historical db-only ips are excluded from this count on purpose.
 	var keptLive []IPWithTimestamp
 	if len(liveIps) > limitIp {
@@ -422,13 +412,25 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
 		keptLive = liveIps[:limitIp]
 		bannedLive := liveIps[limitIp:]
 
+		// Open log file only when a ban entry needs to be written.
+		// Use a local logger to avoid mutating the global log.* state,
+		// which would redirect all standard-library logging to this file
+		// and leave a dangling closed-file handle after the defer fires.
+		logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
+		if err != nil {
+			logger.Errorf("failed to open IP limit log file: %s", err)
+			return false
+		}
+		defer logIpFile.Close()
+		ipLogger := log.New(logIpFile, "", log.LstdFlags)
+
 		// log format is load-bearing: x-ui.sh create_iplimit_jails builds
 		// filter.d/3x-ipl.conf with
 		//   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+
 		// don't change the wording.
 		for _, ipTime := range bannedLive {
 			j.disAllowedIps = append(j.disAllowedIps, ipTime.IP)
-			log.Printf("[LIMIT_IP] Email = %s || Disconnecting OLD IP = %s || Timestamp = %d", clientEmail, ipTime.IP, ipTime.Timestamp)
+			ipLogger.Printf("[LIMIT_IP] Email = %s || Disconnecting OLD IP = %s || Timestamp = %d", clientEmail, ipTime.IP, ipTime.Timestamp)
 		}
 
 		// force xray to drop existing connections from banned ips

+ 4 - 4
x-ui.sh

@@ -2034,14 +2034,14 @@ backend=auto
 filter=3x-ipl
 action=3x-ipl
 logpath=${iplimit_log_path}
-maxretry=2
+maxretry=1
 findtime=32
 bantime=${bantime}m
 EOF
 
     cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf
 [Definition]
-datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
+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
@@ -2062,10 +2062,10 @@ actionstop = <iptables> -D <chain> -p <protocol> -j 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}
+            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}
+              echo "\$(date +"%Y/%m/%d %H:%M:%S")   UNBAN   [Email] = <F-USER> [IP] = <ip> unbanned." >> ${iplimit_banned_log_path}
 
 [Init]
 name = default