diff --git a/.gitignore b/.gitignore index 8fa4eeb0..1761ece2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Ignore editor and IDE settings .idea/ .vscode/ +.claude/ .cache/ .sync* diff --git a/DockerEntrypoint.sh b/DockerEntrypoint.sh index 7511d2ea..4479a4c5 100644 --- a/DockerEntrypoint.sh +++ b/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*.+\s*\|\|\s*Disconnecting OLD IP\s*=\s*\s*\|\|\s*Timestamp\s*=\s*\d+ +ignoreregex = +EOF + + cat > /etc/fail2ban/action.d/3x-ipl.conf << EOF +[INCLUDES] +before = iptables-allports.conf + +[Definition] +actionstart = -N f2b- + -A f2b- -j + -I -p -j f2b- + +actionstop = -D -p -j f2b- + + -X f2b- + +actioncheck = -n -L | grep -q 'f2b-[ \t]' + +actionban = -I f2b- 1 -s -j + echo "\$(date +"%Y/%m/%d %H:%M:%S") BAN [Email] = [IP] = banned for seconds." >> $LOG_FOLDER/3xipl-banned.log + +actionunban = -D f2b- -s -j + echo "\$(date +"%Y/%m/%d %H:%M:%S") UNBAN [Email] = [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 diff --git a/web/job/check_client_ip_job.go b/web/job/check_client_ip_job.go index 7f0ac2cf..e16cced2 100644 --- a/web/job/check_client_ip_job.go +++ b/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*.+\s*\|\|\s*Disconnecting OLD IP\s*=\s*\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 diff --git a/x-ui.sh b/x-ui.sh index 73e99195..31ecc683 100644 --- a/x-ui.sh +++ b/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*.+\s*\|\|\s*Disconnecting OLD IP\s*=\s*\s*\|\|\s*Timestamp\s*=\s*\d+ ignoreregex = EOF @@ -2062,10 +2062,10 @@ actionstop = -D -p -j f2b- actioncheck = -n -L | grep -q 'f2b-[ \t]' actionban = -I f2b- 1 -s -j - echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = [IP] = banned for seconds." >> ${iplimit_banned_log_path} + echo "\$(date +"%Y/%m/%d %H:%M:%S") BAN [Email] = [IP] = banned for seconds." >> ${iplimit_banned_log_path} actionunban = -D f2b- -s -j - echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = [IP] = unbanned." >> ${iplimit_banned_log_path} + echo "\$(date +"%Y/%m/%d %H:%M:%S") UNBAN [Email] = [IP] = unbanned." >> ${iplimit_banned_log_path} [Init] name = default