diff --git a/internal/sub/service.go b/internal/sub/service.go index 4b0b23ad1..49c53cef4 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -863,15 +863,15 @@ func (s *SubService) loadInboundsBySubId(subId string) ([]*model.Inbound, error) return inbounds, nil } -// subscriptionSettings is an inbound's settings for a subscription body: WireGuard and AmneziaWG -// keep their peers, every other protocol gets the shared copy without the clients array. +// WireGuard/AmneziaWG need their peers, and TUIC link generation reads embedded clients. +// Other protocols can share cached settings without the clients array. func (s *SubService) subscriptionSettings(inbound *model.Inbound) (string, error) { load := func() (string, error) { var settings string err := database.GetDB().Model(&model.Inbound{}).Select("settings").Where("id = ?", inbound.Id).Row().Scan(&settings) return settings, err } - if inbound.Protocol == model.WireGuard || inbound.Protocol == model.AmneziaWG { + if inbound.Protocol == model.WireGuard || inbound.Protocol == model.AmneziaWG || inbound.Protocol == model.TUIC { return load() } return s.settingsCache.settingsFor(inbound.Id, load) diff --git a/internal/sub/settings_cache_test.go b/internal/sub/settings_cache_test.go index 57b8ee98f..92bd373f2 100644 --- a/internal/sub/settings_cache_test.go +++ b/internal/sub/settings_cache_test.go @@ -166,6 +166,49 @@ func TestSettingsCacheRendersTheSameSubscription(t *testing.T) { } } +func TestSettingsCachePreservesTuicSubscriptions(t *testing.T) { + seedSubDB(t) + ib := seedSubProtocolInbound(t, "tuic-sub", "tuic", 4704, 1, `{}`, model.TUIC) + settings := `{"server":{"sni":"tuic.example.com"},"clients":[{"uuid":"11111111-2222-4333-8444-000000004704","password":"tuic-password","email":"tuic@e","subId":"tuic-sub","enable":true}]}` + if err := database.GetDB().Model(ib).Update("settings", settings).Error; err != nil { + t.Fatal(err) + } + if err := database.GetDB().Model(&model.ClientRecord{}).Where("email = ?", "tuic@e").Update("password", "tuic-password").Error; err != nil { + t.Fatal(err) + } + render := func(svc *SubService) []string { + t.Helper() + req := svc.ForRequest("req.example.com") + req.subscriptionBody = true + links, _, _, _, err := req.getSubs("tuic-sub") + if err != nil { + t.Fatal(err) + } + if len(links) != 1 || !strings.HasPrefix(links[0], "tuic://") { + t.Fatalf("TUIC link missing from subscription: %v", links) + } + clash, _, err := NewSubClashService(false, "", false, svc).GetClash("tuic-sub", "req.example.com") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(clash, "tuic-password") { + t.Fatalf("TUIC credentials missing from Clash subscription: %s", clash) + } + return []string{links[0], clash} + } + want := render(NewSubService("")) + cached := NewSubService("") + t.Cleanup(cached.enableSettingsCache()) + for round := range 2 { + got := render(cached) + for i, format := range []string{"raw", "clash"} { + if got[i] != want[i] { + t.Fatalf("round %d %s differs with settings cache:\n got: %s\nwant: %s", round, format, got[i], want[i]) + } + } + } +} + // Only the settings blob is cached: the inbound row is read on every fetch, so a port change or a // disabled inbound reaches the next fetch, while settings wait for a panel edit's purge. func TestSettingsCacheReadsTheInboundRowFresh(t *testing.T) {