mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
fix(job): skip fail2ban IP limit when disabled (#4581)
Honor XUI_ENABLE_FAIL2BAN before running fail2ban-dependent IP-limit work. This avoids spawning fail2ban-client on disabled Docker installs while preserving the default enabled behavior when the env var is unset. Co-authored-by: Mayurifag <Mayurifag@users.noreply.github.com>
This commit is contained in:
parent
01d2ec5061
commit
8fa248c621
3 changed files with 134 additions and 5 deletions
|
|
@ -66,8 +66,12 @@ func (j *CheckClientIpJob) Run() {
|
|||
}
|
||||
|
||||
shouldClearAccessLog := false
|
||||
iplimitActive := j.hasLimitIp()
|
||||
f2bInstalled := j.checkFail2BanInstalled()
|
||||
fail2BanEnabled := isFail2BanEnabled()
|
||||
iplimitActive := fail2BanEnabled && j.hasLimitIp()
|
||||
f2bInstalled := false
|
||||
if iplimitActive {
|
||||
f2bInstalled = j.checkFail2BanInstalled()
|
||||
}
|
||||
isAccessLogAvailable := j.checkAccessLogAvailable(iplimitActive)
|
||||
|
||||
if isAccessLogAvailable {
|
||||
|
|
@ -80,9 +84,7 @@ func (j *CheckClientIpJob) Run() {
|
|||
if f2bInstalled {
|
||||
shouldClearAccessLog = j.processLogFile()
|
||||
} else {
|
||||
if !f2bInstalled {
|
||||
logger.Warning("[LimitIP] Fail2Ban is not installed, Please install Fail2Ban from the x-ui bash menu.")
|
||||
}
|
||||
logger.Warning("[LimitIP] Fail2Ban is not installed, Please install Fail2Ban from the x-ui bash menu.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -279,12 +281,21 @@ func partitionLiveIps(ipMap map[string]int64, observedThisScan map[string]bool)
|
|||
}
|
||||
|
||||
func (j *CheckClientIpJob) checkFail2BanInstalled() bool {
|
||||
if !isFail2BanEnabled() {
|
||||
return false
|
||||
}
|
||||
|
||||
cmd := "fail2ban-client"
|
||||
args := []string{"-h"}
|
||||
err := exec.Command(cmd, args...).Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func isFail2BanEnabled() bool {
|
||||
value, ok := os.LookupEnv("XUI_ENABLE_FAIL2BAN")
|
||||
return !ok || value == "true"
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) checkAccessLogAvailable(iplimitActive bool) bool {
|
||||
accessLogPath, err := xray.GetAccessLogPath()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -128,6 +128,49 @@ func ipSet(entries []IPWithTimestamp) map[string]int64 {
|
|||
return out
|
||||
}
|
||||
|
||||
func TestRun_DisabledFail2BanSkipsProbeAndBanLog(t *testing.T) {
|
||||
setupIntegrationDB(t)
|
||||
t.Setenv("XUI_ENABLE_FAIL2BAN", "false")
|
||||
marker := fakeFail2BanClient(t)
|
||||
|
||||
const email = "disabled-fail2ban"
|
||||
seedInboundWithClient(t, "inbound-disabled-fail2ban", email, 1)
|
||||
|
||||
binDir := t.TempDir()
|
||||
accessLog := filepath.Join(t.TempDir(), "access.log")
|
||||
t.Setenv("XUI_BIN_FOLDER", binDir)
|
||||
configData, err := json.Marshal(map[string]any{
|
||||
"log": map[string]any{"access": accessLog},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal xray config: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(binDir, "config.json"), configData, 0644); err != nil {
|
||||
t.Fatalf("write xray config: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(accessLog, []byte("2026/05/26 12:00:00 from tcp:203.0.113.10:443 accepted tcp:example.com:443 email: disabled-fail2ban\n"), 0644); err != nil {
|
||||
t.Fatalf("write access log: %v", err)
|
||||
}
|
||||
|
||||
j := NewCheckClientIpJob()
|
||||
j.Run()
|
||||
|
||||
if _, err := os.Stat(marker); !os.IsNotExist(err) {
|
||||
t.Fatalf("fail2ban-client should not have been executed, stat error: %v", err)
|
||||
}
|
||||
if info, err := os.Stat(readIpLimitLogPath()); err == nil && info.Size() > 0 {
|
||||
body, _ := os.ReadFile(readIpLimitLogPath())
|
||||
t.Fatalf("3xipl.log should be empty when fail2ban is disabled, got:\n%s", body)
|
||||
}
|
||||
var count int64
|
||||
if err := database.GetDB().Model(&model.InboundClientIps{}).Where("client_email = ?", email).Count(&count).Error; err != nil {
|
||||
t.Fatalf("count InboundClientIps: %v", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatalf("disabled fail2ban should not persist IP-limit rows, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
// #4091 repro: client has limit=3, db still holds 3 idle ips from a
|
||||
// few minutes ago, only one live ip is actually connecting. pre-fix:
|
||||
// live ip got banned every tick and never appeared in the panel.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
@ -145,3 +148,75 @@ func TestPartitionLiveIps_EmptyScanLeavesDbIntact(t *testing.T) {
|
|||
t.Fatalf("all merged entries should flow to historical\ngot: %v\nwant: [A B]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckFail2BanInstalled_DisabledEnvSkipsClientProbe(t *testing.T) {
|
||||
t.Setenv("XUI_ENABLE_FAIL2BAN", "false")
|
||||
marker := fakeFail2BanClient(t)
|
||||
|
||||
if (&CheckClientIpJob{}).checkFail2BanInstalled() {
|
||||
t.Fatal("fail2ban should be unavailable when XUI_ENABLE_FAIL2BAN=false")
|
||||
}
|
||||
if _, err := os.Stat(marker); !os.IsNotExist(err) {
|
||||
t.Fatalf("fail2ban-client should not have been executed, stat error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckFail2BanInstalled_EmptyEnvSkipsClientProbe(t *testing.T) {
|
||||
t.Setenv("XUI_ENABLE_FAIL2BAN", "")
|
||||
marker := fakeFail2BanClient(t)
|
||||
|
||||
if (&CheckClientIpJob{}).checkFail2BanInstalled() {
|
||||
t.Fatal("fail2ban should be unavailable when XUI_ENABLE_FAIL2BAN is empty")
|
||||
}
|
||||
if _, err := os.Stat(marker); !os.IsNotExist(err) {
|
||||
t.Fatalf("fail2ban-client should not have been executed, stat error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsFail2BanEnabled_DefaultsToEnabledWhenUnset(t *testing.T) {
|
||||
value, ok := os.LookupEnv("XUI_ENABLE_FAIL2BAN")
|
||||
os.Unsetenv("XUI_ENABLE_FAIL2BAN")
|
||||
t.Cleanup(func() {
|
||||
if ok {
|
||||
os.Setenv("XUI_ENABLE_FAIL2BAN", value)
|
||||
} else {
|
||||
os.Unsetenv("XUI_ENABLE_FAIL2BAN")
|
||||
}
|
||||
})
|
||||
|
||||
if !isFail2BanEnabled() {
|
||||
t.Fatal("fail2ban should default to enabled when XUI_ENABLE_FAIL2BAN is unset")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckFail2BanInstalled_EnabledEnvProbesClient(t *testing.T) {
|
||||
t.Setenv("XUI_ENABLE_FAIL2BAN", "true")
|
||||
marker := fakeFail2BanClient(t)
|
||||
|
||||
if !(&CheckClientIpJob{}).checkFail2BanInstalled() {
|
||||
t.Fatal("fail2ban should be available when the client probe succeeds")
|
||||
}
|
||||
if _, err := os.Stat(marker); err != nil {
|
||||
t.Fatalf("fail2ban-client should have been executed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func fakeFail2BanClient(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
dir := t.TempDir()
|
||||
marker := filepath.Join(dir, "probe-called")
|
||||
fakeClient := filepath.Join(dir, "fail2ban-client")
|
||||
script := "#!/bin/sh\n: > \"$FAIL2BAN_PROBE_MARKER\"\nexit 0\n"
|
||||
if runtime.GOOS == "windows" {
|
||||
fakeClient += ".bat"
|
||||
script = "@echo off\ntype nul > \"%FAIL2BAN_PROBE_MARKER%\"\nexit /b 0\n"
|
||||
}
|
||||
if err := os.WriteFile(fakeClient, []byte(script), 0o755); err != nil {
|
||||
t.Fatalf("write fake fail2ban-client: %v", err)
|
||||
}
|
||||
|
||||
t.Setenv("FAIL2BAN_PROBE_MARKER", marker)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
return marker
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue