From 9b258becd01125799b323e3651368f415d9731b3 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 15 Jul 2026 03:52:15 +0200 Subject: [PATCH] fix(db): mark the IP-limit cleanup seeder done on a fresh install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ResetIpLimitNoFail2ban is a one-time migration that, on a host without fail2ban, zeroes every existing client's limitIp because the limit can't be enforced. It was missing from the fresh-install fast-path seeder list, so on a brand-new DB it did not run on the first boot but fired on the second — wiping any IP limits the admin had set in between. Add it to the fast-path so a truly fresh install marks it done up front (there is nothing to clean), leaving later admin-set limits intact. --- internal/database/db.go | 2 +- internal/database/seeder_fastpath_test.go | 41 +++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 internal/database/seeder_fastpath_test.go diff --git a/internal/database/db.go b/internal/database/db.go index b59ff165f..9738faafc 100644 --- a/internal/database/db.go +++ b/internal/database/db.go @@ -1057,7 +1057,7 @@ func runSeeders(isUsersEmpty bool) error { } if empty && isUsersEmpty { - seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix", "ApiTokensHash", "LegacyProxySettingsCleanup", "WireguardPeersToClients", "MtprotoSecretsToClients", "NodeInboundsAdopted"} + seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix", "ApiTokensHash", "LegacyProxySettingsCleanup", "WireguardPeersToClients", "MtprotoSecretsToClients", "NodeInboundsAdopted", "ResetIpLimitNoFail2ban"} for _, name := range seeders { if err := db.Create(&model.HistoryOfSeeders{SeederName: name}).Error; err != nil { return err diff --git a/internal/database/seeder_fastpath_test.go b/internal/database/seeder_fastpath_test.go new file mode 100644 index 000000000..fca74a332 --- /dev/null +++ b/internal/database/seeder_fastpath_test.go @@ -0,0 +1,41 @@ +package database + +import ( + "path/filepath" + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/database/model" +) + +// On a fresh install the fast path marks the one-time migration seeders as done +// without running them. ResetIpLimitNoFail2ban must be in that set: otherwise it +// is skipped on the first boot and runs on the second, where — on a host without +// fail2ban — it destructively zeroes every client's limitIp, including limits the +// admin configured between the two boots. +func TestFreshInstallFastPathMarksResetIpLimitSeeder(t *testing.T) { + if err := InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { + t.Fatalf("InitDB: %v", err) + } + t.Cleanup(func() { _ = CloseDB() }) + + if err := db.Where("1 = 1").Delete(&model.HistoryOfSeeders{}).Error; err != nil { + t.Fatalf("reset seeder history: %v", err) + } + if err := db.Where("1 = 1").Delete(&model.User{}).Error; err != nil { + t.Fatalf("reset users: %v", err) + } + + if err := runSeeders(true); err != nil { + t.Fatalf("runSeeders: %v", err) + } + + var cnt int64 + if err := db.Model(&model.HistoryOfSeeders{}). + Where("seeder_name = ?", "ResetIpLimitNoFail2ban"). + Count(&cnt).Error; err != nil { + t.Fatalf("count seeder history: %v", err) + } + if cnt != 1 { + t.Fatal("fresh-install fast path must mark ResetIpLimitNoFail2ban done so it cannot wipe admin-set IP limits on the next boot") + } +}