mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
fix(hwid): track devices only for subscriptions a client owns
The subscription routes run the HWID gate before they look up the subId, and the gate read "no device limit" and "no such subscription" alike, so a request naming any path segment with an X-HWID header was answered 404 yet stored that segment, up to about 1 MiB, in client_hwids for good. Anyone reaching the subscription port could fill the disk until the panel could no longer save anything. An unlimited subscription is tracked only while an enabled client owns its subId, and a one-time seeder drops the rows already stored for subIds no client owns.
This commit is contained in:
+19
-1
@@ -1530,7 +1530,7 @@ func runSeeders(isUsersEmpty bool) error {
|
||||
}
|
||||
|
||||
if empty && isUsersEmpty {
|
||||
seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix2", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix", "FreedomFinalRulesPrivateEgressBlock", "UppercaseFreedomFinalRulesFix", "InboundRealityFinalmaskTcpStrip", "ApiTokensHash", "LegacyProxySettingsCleanup", "SessionSecretRotation", "OutboundRemovedKeysFix", "FreedomDomainStrategyFix", "DNSOutboundLegacyKeysFix", "DNSOutboundQTypeZeroFix", "WireguardDomainStrategyFix", "XdnsFinalmaskNamesAddrsFix", "UDPHopClientMaskFix", "HostVerifyPeerCertByNameBoolFix", "HostMihomoX25519Reset", "WireguardPeersToClients", "MtprotoSecretsToClients", "NodeInboundsAdopted", "NodeDownNotified", "ResetIpLimitNoFail2ban", "ClientGroupNameTrim"}
|
||||
seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix2", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix", "FreedomFinalRulesPrivateEgressBlock", "UppercaseFreedomFinalRulesFix", "InboundRealityFinalmaskTcpStrip", "ApiTokensHash", "LegacyProxySettingsCleanup", "SessionSecretRotation", "OutboundRemovedKeysFix", "FreedomDomainStrategyFix", "DNSOutboundLegacyKeysFix", "DNSOutboundQTypeZeroFix", "WireguardDomainStrategyFix", "XdnsFinalmaskNamesAddrsFix", "UDPHopClientMaskFix", "HostVerifyPeerCertByNameBoolFix", "HostMihomoX25519Reset", "WireguardPeersToClients", "MtprotoSecretsToClients", "NodeInboundsAdopted", "NodeDownNotified", "ResetIpLimitNoFail2ban", "ClientGroupNameTrim", "OwnerlessClientHwidPurge"}
|
||||
for _, name := range seeders {
|
||||
if err := db.Create(&model.HistoryOfSeeders{SeederName: name}).Error; err != nil {
|
||||
return err
|
||||
@@ -1719,6 +1719,12 @@ func runSeeders(isUsersEmpty bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Contains(seedersHistory, "OwnerlessClientHwidPurge") {
|
||||
if err := purgeOwnerlessClientHwids(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := seedHostsFromExternalProxy(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1914,6 +1920,18 @@ func clearLegacyProxySettings() error {
|
||||
})
|
||||
}
|
||||
|
||||
// The HWID gate stored a row for any subId a request named, owned or not; those rows
|
||||
// belong to no subscription and only grow the database.
|
||||
func purgeOwnerlessClientHwids() error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("sub_id NOT IN (SELECT sub_id FROM clients WHERE sub_id IS NOT NULL)").
|
||||
Delete(&model.ClientHwid{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&model.HistoryOfSeeders{SeederName: "OwnerlessClientHwidPurge"}).Error
|
||||
})
|
||||
}
|
||||
|
||||
// The groups page addresses a group by its trimmed name, so a padded stored name listed as
|
||||
// its own group while every action on it hit the trimmed one.
|
||||
func trimClientGroupNames() error {
|
||||
|
||||
@@ -2,6 +2,7 @@ package database
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
@@ -376,3 +377,37 @@ func TestClientGroupNamesTrimmedOnUpgrade(t *testing.T) {
|
||||
t.Fatalf("groups after upgrade = %q, want [vip]", groups)
|
||||
}
|
||||
}
|
||||
|
||||
// Requests naming a subId no client owns stored HWID rows that nothing ever reads; the
|
||||
// upgrade drops them and keeps the rows of real subscriptions.
|
||||
func TestOwnerlessClientHwidsPurgedOnUpgrade(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "x-ui.db")
|
||||
if err := InitDB(dbPath); err != nil {
|
||||
t.Fatalf("InitDB: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = CloseDB() })
|
||||
if err := db.Create(&model.ClientRecord{Email: "a@x", SubID: "owned", Enable: true}).Error; err != nil {
|
||||
t.Fatalf("seed client: %v", err)
|
||||
}
|
||||
for i, sub := range []string{"owned", "junk-1", "junk-2"} {
|
||||
if err := db.Create(&model.ClientHwid{SubID: sub, HwidHash: fmt.Sprintf("h%d", i), FirstSeen: 1, LastSeen: 1}).Error; err != nil {
|
||||
t.Fatalf("seed hwid %s: %v", sub, err)
|
||||
}
|
||||
}
|
||||
if err := db.Where("seeder_name = ?", "OwnerlessClientHwidPurge").Delete(&model.HistoryOfSeeders{}).Error; err != nil {
|
||||
t.Fatalf("clear seeder history: %v", err)
|
||||
}
|
||||
if err := CloseDB(); err != nil {
|
||||
t.Fatalf("CloseDB: %v", err)
|
||||
}
|
||||
if err := InitDB(dbPath); err != nil {
|
||||
t.Fatalf("reopen InitDB: %v", err)
|
||||
}
|
||||
var subs []string
|
||||
if err := db.Model(&model.ClientHwid{}).Pluck("sub_id", &subs).Error; err != nil {
|
||||
t.Fatalf("read hwids: %v", err)
|
||||
}
|
||||
if len(subs) != 1 || subs[0] != "owned" {
|
||||
t.Fatalf("hwid rows after upgrade = %q, want [owned]", subs)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user