From c0bd5720853827d1dbf4d2a7ee71634d5ad8333d Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 1 Nov 2026 00:21:03 +0100 Subject: [PATCH] fix(sub): look up a socket child's fallback front on the child's own host projectThroughFallbackMaster found the front of a unix-socket child by scanning every inbound's settings.fallbacks for that socket. A node's inbound settings are mirrored from the node, so a node could name any socket and have its own port, security and external proxies substituted into the subscription links of another host's child. A socket exists only on the host that binds it, so the lookup, and the same scan the inbounds page uses for share links, now considers only inbounds hosted where the child runs. --- frontend/src/pages/inbounds/InboundsPage.tsx | 1 + internal/sub/host_sub_test.go | 51 ++++++++++++++++++++ internal/sub/service.go | 12 ++++- 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/inbounds/InboundsPage.tsx b/frontend/src/pages/inbounds/InboundsPage.tsx index ff1d54691..46b031237 100644 --- a/frontend/src/pages/inbounds/InboundsPage.tsx +++ b/frontend/src/pages/inbounds/InboundsPage.tsx @@ -294,6 +294,7 @@ export default function InboundsPage() { if (!dbInbound?.listen?.startsWith?.('@')) return dbInbound; for (const candidate of dbInbounds) { if (candidate.id === dbInbound.id) continue; + if ((candidate.nodeId ?? 0) !== (dbInbound.nodeId ?? 0)) continue; if (!['trojan', 'vless'].includes(candidate.protocol)) continue; const candStream = coerceInboundJsonField(candidate.streamSettings) as { network?: string }; if (candStream.network !== 'tcp') continue; diff --git a/internal/sub/host_sub_test.go b/internal/sub/host_sub_test.go index e7781e83e..3d674bbc6 100644 --- a/internal/sub/host_sub_test.go +++ b/internal/sub/host_sub_test.go @@ -233,6 +233,57 @@ func TestSub_HostOverFallback(t *testing.T) { } } +// The legacy "@socket" lookup stays on the child's own host: an older inbound +// mirrored from another node that names the same socket is never the master. +func TestSub_LegacyFallbackMasterStaysOnChildHost(t *testing.T) { + foreign, own := 7, 8 + for _, tc := range []struct { + name string + nodeID *int + }{ + {"local child", nil}, + {"node child", &own}, + } { + t.Run(tc.name, func(t *testing.T) { + seedSubDB(t) + db := database.GetDB() + for _, master := range []*model.Inbound{ + { + UserId: 1, Tag: "foreign", Enable: true, Port: 44301, Protocol: model.VLESS, NodeID: &foreign, + Settings: `{"clients":[],"decryption":"none","fallbacks":[{"dest":"@vless-ws"}]}`, + StreamSettings: `{"network":"tcp","security":"none","externalProxy":[{"forceTls":"none","dest":"evil.example","port":8080}]}`, + }, + { + UserId: 1, Tag: "front", Enable: true, Port: 9443, Protocol: model.VLESS, NodeID: tc.nodeID, + Settings: `{"clients":[],"decryption":"none","fallbacks":[{"dest":"@vless-ws"}]}`, + StreamSettings: `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"front.sni"}}`, + }, + } { + if err := db.Create(master).Error; err != nil { + t.Fatalf("seed %s: %v", master.Tag, err) + } + } + child := seedSubInbound(t, "s1", "child", 4441, 1, `{"network":"ws","security":"none"}`) + if err := db.Model(&model.Inbound{}).Where("id = ?", child.Id). + Updates(map[string]any{"listen": "@vless-ws", "node_id": tc.nodeID}).Error; err != nil { + t.Fatalf("place child: %v", err) + } + + links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com") + if err != nil { + t.Fatalf("GetSubs: %v", err) + } + joined := strings.Join(links, "\n") + if strings.Contains(joined, "evil.example") { + t.Fatalf("another node's inbound was used as fallback master: %s", joined) + } + if !strings.Contains(joined, ":9443") || !strings.Contains(joined, "sni=front.sni") { + t.Fatalf("the front on the child's host must be projected: %s", joined) + } + }) + } +} + // #8 — a client only gets hosts for inbounds it is actually on (the // clients ⋈ client_inbounds ⋈ inbounds join), never arbitrary inbounds. func TestSub_HostsResolveViaClientInbounds(t *testing.T) { diff --git a/internal/sub/service.go b/internal/sub/service.go index 89cc785f0..4d6f95e5f 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -827,7 +827,7 @@ func (s *SubService) indexStatsBySubId(subId string) { // when applicable. Covers both fallback mechanisms: // - panel-tracked: an inbound_fallbacks row where child_id = inbound.Id // - legacy unix-socket: inbound.Listen begins with "@" and some VLESS/ -// Trojan inbound's settings.fallbacks references that listen address +// Trojan inbound on the same host names that socket in settings.fallbacks // // Returns true when a projection happened; sub services call this before // generating links so a child VLESS-WS bound to 127.0.0.1 emits the @@ -861,7 +861,15 @@ func (s *SubService) projectThroughFallbackMaster(inbound *model.Inbound) bool { if master == nil && len(inbound.Listen) > 0 && inbound.Listen[0] == '@' { var m model.Inbound - if err := db.Model(model.Inbound{}). + // A unix socket exists only on the host that binds it, so only an inbound + // hosted where the child runs (this panel, or the same node) can front it. + q := db.Model(model.Inbound{}) + if inbound.NodeID != nil { + q = q.Where("node_id = ?", *inbound.NodeID) + } else { + q = q.Where("node_id IS NULL") + } + if err := q. Where("JSON_TYPE(settings, '$.fallbacks') = 'array'"). Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", inbound.Listen). First(&m).Error; err == nil {