fix(sub): restore TUIC links in subscriptions after v3.11.0 (#6963)

The subscription settings cache strips each inbound's clients array, but the TUIC link generator still reads credentials from it, so raw subscriptions (v2rayN, Shadowrocket) lost every TUIC link while the browser page kept it. TUIC now reads its full settings, as WireGuard and AmneziaWG already do.
This commit is contained in:
Denis Gritsenko
2026-11-02 17:27:49 +03:00
committed by GitHub
parent 81059edc91
commit 43af7ada96
2 changed files with 46 additions and 3 deletions
+3 -3
View File
@@ -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)
+43
View File
@@ -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) {