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
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.
This commit is contained in:
parent
20edaee8ed
commit
cb2d7db665
3 changed files with 134 additions and 5 deletions
|
|
@ -66,8 +66,12 @@ func (j *CheckClientIpJob) Run() {
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldClearAccessLog := false
|
shouldClearAccessLog := false
|
||||||
iplimitActive := j.hasLimitIp()
|
fail2BanEnabled := isFail2BanEnabled()
|
||||||
f2bInstalled := j.checkFail2BanInstalled()
|
iplimitActive := fail2BanEnabled && j.hasLimitIp()
|
||||||
|
f2bInstalled := false
|
||||||
|
if iplimitActive {
|
||||||
|
f2bInstalled = j.checkFail2BanInstalled()
|
||||||
|
}
|
||||||
isAccessLogAvailable := j.checkAccessLogAvailable(iplimitActive)
|
isAccessLogAvailable := j.checkAccessLogAvailable(iplimitActive)
|
||||||
|
|
||||||
if isAccessLogAvailable {
|
if isAccessLogAvailable {
|
||||||
|
|
@ -80,9 +84,7 @@ func (j *CheckClientIpJob) Run() {
|
||||||
if f2bInstalled {
|
if f2bInstalled {
|
||||||
shouldClearAccessLog = j.processLogFile()
|
shouldClearAccessLog = j.processLogFile()
|
||||||
} else {
|
} 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.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -281,12 +283,21 @@ func partitionLiveIps(ipMap map[string]int64, observedThisScan map[string]bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *CheckClientIpJob) checkFail2BanInstalled() bool {
|
func (j *CheckClientIpJob) checkFail2BanInstalled() bool {
|
||||||
|
if !isFail2BanEnabled() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
cmd := "fail2ban-client"
|
cmd := "fail2ban-client"
|
||||||
args := []string{"-h"}
|
args := []string{"-h"}
|
||||||
err := exec.Command(cmd, args...).Run()
|
err := exec.Command(cmd, args...).Run()
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isFail2BanEnabled() bool {
|
||||||
|
value, ok := os.LookupEnv("XUI_ENABLE_FAIL2BAN")
|
||||||
|
return !ok || value == "true"
|
||||||
|
}
|
||||||
|
|
||||||
func (j *CheckClientIpJob) checkAccessLogAvailable(iplimitActive bool) bool {
|
func (j *CheckClientIpJob) checkAccessLogAvailable(iplimitActive bool) bool {
|
||||||
accessLogPath, err := xray.GetAccessLogPath()
|
accessLogPath, err := xray.GetAccessLogPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,49 @@ func ipSet(entries []IPWithTimestamp) map[string]int64 {
|
||||||
return out
|
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
|
// #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:
|
// few minutes ago, only one live ip is actually connecting. pre-fix:
|
||||||
// live ip got banned every tick and never appeared in the panel.
|
// live ip got banned every tick and never appeared in the panel.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -144,3 +147,75 @@ func TestPartitionLiveIps_EmptyScanLeavesDbIntact(t *testing.T) {
|
||||||
t.Fatalf("all merged entries should flow to historical\ngot: %v\nwant: [A B]", got)
|
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