fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic

Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic
Deep functional audit of node/user/subscription/traffic features
found 5 bugs. All fixed. All tests pass (including the previously
failing hasCredState test).
== CRITICAL ==
F1: BatchCreate skips AutofillListenerDefaults
Batch-created listeners had no auto-generated UUIDs, passwords,
REALITY keys, SS ciphers, snell PSKs, or sudoku keys → mihomo
rejected the config → entire batch failed.
Fix: call AutofillListenerDefaults before ValidateModel in loop.
F2: BatchCreate doesn't reclaim soft-deleted names
Inline Count check used GORM default scope (excludes soft-deleted)
but UNIQUE index covers them → DB constraint error on name clash.
Fix: use ensureUniqueName which renames soft-deleted rows.
F3: Failing test contradicts hasCredState fallback fix
Test asserted no fallback when hasCredState=true + empty creds.
But we intentionally removed the guard to fix 'unset fields: users'.
Fix: updated test to assert config users ARE included as fallback.
== HIGH ==
F4: sub_token generation ignores error
subTok, _ := randomHex(16) — empty sub_token on rand.Read failure.
Fix: check error, return on failure.
F5: BatchResetTraffic doesn't trigger config regeneration
Over-quota users stayed blocked for ~10s after traffic reset.
Fix: added notifyCredentialsChanged() after reset.
Verification: go build/vet/test all PASS. gofmt clean.
This commit is contained in:
Z User committed 2026-08-31 04:26:51 +00:00
1 parent 5b9674987f
commit 4b00a1268a
4 files changed
+43 -10

No files matched your search

+12 -6
View File
@@ -126,19 +126,25 @@ func (s *Service) BatchCreate(list []models.Listener) ([]models.Listener, error)
}
created := make([]models.Listener, 0, len(list))
for i := range list {
// Batch-created listeners must go through the same autofill path as
// single Create so UUIDs / passwords / REALITY keys are generated and
// the config passes mihomo validation. Without this, mihomo rejects the
// entire config and the whole batch fails.
if err := AutofillListenerDefaults(&list[i]); err != nil {
tx.Rollback()
return nil, fmt.Errorf("autofill listener defaults: %w", err)
}
if err := ValidateModel(&list[i]); err != nil {
tx.Rollback()
return nil, err
}
var n int64
if err := tx.Model(&models.Listener{}).Where("name = ?", list[i].Name).Count(&n).Error; err != nil {
// GORM's default scope excludes soft-deleted rows, but the UNIQUE(name)
// index covers them. Use ensureUniqueName so soft-deleted rows with the
// same name are renamed out of the way before Create hits the index.
if err := s.ensureUniqueName(&list[i]); err != nil {
tx.Rollback()
return nil, err
}
if n > 0 {
tx.Rollback()
return nil, fmt.Errorf("listener name %q already exists", list[i].Name)
}
if err := tx.Create(&list[i]).Error; err != nil {
tx.Rollback()
return nil, err
@@ -17,13 +17,31 @@ func TestGenerateListenersUsesModelTLS(t *testing.T) {
}
}
func TestGenerateListenersDoesNotFallbackWhenCredentialStateIsExplicitlyEmpty(t *testing.T) {
func TestGenerateListenersFallsBackToConfigUsersWhenCredentialStateIsExplicitlyEmpty(t *testing.T) {
// When the credential provider reports hasCredState=true but the panel has
// no active credentials for a listener (e.g. all bound users blocked /
// expired), the compiled listener must fall back to the users already
// embedded in the listener Config. Returning nil users here would make
// mihomo reject the entire config ("unset fields: users"), taking down
// every other listener. Falling back to config users keeps the listener
// valid; the enforcer is responsible for disabling the listener when all
// panel users are inactive.
listeners := []models.Listener{{BaseModel: models.BaseModel{ID: 1}, Name: "vless", Protocol: "vless", Port: "443", Enabled: true, Config: `{"users":[{"uuid":"legacy"}]}`}}
got, err := generateListeners(listeners, map[uint][]Credential{1: {}})
if err != nil {
t.Fatal(err)
}
if _, ok := got[0]["users"]; ok {
t.Fatalf("legacy users leaked into explicitly empty credential state: %#v", got[0]["users"])
if len(got) != 1 {
t.Fatalf("expected one compiled listener, got %d", len(got))
}
users, ok := got[0]["users"].([]map[string]interface{})
if !ok {
t.Fatalf("expected config users to be present as fallback, got %#v", got[0]["users"])
}
if len(users) != 1 {
t.Fatalf("expected one fallback user, got %d (%#v)", len(users), users)
}
if uid, _ := users[0]["uuid"].(string); uid != "legacy" {
t.Fatalf("expected legacy config user uuid %q, got %#v", "legacy", users[0])
}
}
+6
View File
@@ -106,6 +106,12 @@ func (s *Service) Batch(action BatchAction, ids []uint) (int, error) {
if res.Error != nil {
return 0, res.Error
}
// Resetting traffic may unblock previously over-quota users. Trigger a
// config regeneration so blocked users regain access immediately
// instead of waiting for the next enforcer tick (up to 10s latency).
if err := s.notifyCredentialsChanged(); err != nil {
log.Printf("warning: credentials changed notification failed: %v", err)
}
return int(res.RowsAffected), nil
case BatchDelete:
if err := s.db.Transaction(func(tx *gorm.DB) error {
+4 -1
View File
@@ -43,7 +43,10 @@ func (s *Service) Create(in CreateInput) (*models.ProxyUser, error) {
if err != nil {
return nil, err
}
subTok, _ := randomHex(16)
subTok, err := randomHex(16)
if err != nil {
return nil, fmt.Errorf("generate proxy user sub token: %w", err)
}
u := &models.ProxyUser{
Username: username,
PasswordEncrypted: encrypted,