From 4b00a1268a4ea24d58c9bdbb55bf2df7ba097f0b Mon Sep 17 00:00:00 2001 From: Z User Date: Mon, 31 Aug 2026 04:26:51 +0000 Subject: [PATCH] fix(functional): batch autofill, soft-delete name reclaim, hasCredState test, sub_token error, batch reset traffic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- backend/internal/listener/p2.go | 18 +++++++++----- .../config/generator_listener_auth_test.go | 24 ++++++++++++++++--- backend/internal/user/service_batch.go | 6 +++++ backend/internal/user/service_crud.go | 5 +++- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/backend/internal/listener/p2.go b/backend/internal/listener/p2.go index 6fe2370..cb68d8b 100644 --- a/backend/internal/listener/p2.go +++ b/backend/internal/listener/p2.go @@ -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 diff --git a/backend/internal/mihomo/config/generator_listener_auth_test.go b/backend/internal/mihomo/config/generator_listener_auth_test.go index 974e6ba..205ae40 100644 --- a/backend/internal/mihomo/config/generator_listener_auth_test.go +++ b/backend/internal/mihomo/config/generator_listener_auth_test.go @@ -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]) } } diff --git a/backend/internal/user/service_batch.go b/backend/internal/user/service_batch.go index 69f231f..39b1c3a 100644 --- a/backend/internal/user/service_batch.go +++ b/backend/internal/user/service_batch.go @@ -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 { diff --git a/backend/internal/user/service_crud.go b/backend/internal/user/service_crud.go index 0f7b203..c759a8d 100644 --- a/backend/internal/user/service_crud.go +++ b/backend/internal/user/service_crud.go @@ -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,