Files
3x-ui/internal/web/service/node_origin_guid_test.go
T
MHSanaei 7458ed4064 fix(nodes): remap a cloned node's own-panelGuid origin so the inbound page shows online
These nodes report their OWN inbounds with their own panelGuid as OriginNodeGuid, so originGuidFor returned the shared GUID verbatim and never remapped it. origin_node_guid stayed the shared GUID while online was keyed under the node-unique key, so the inbound page (which reads the stored origin_node_guid) looked up an empty bucket and showed everyone offline — even though the Nodes page (which derives the key live) was correct. Treat an origin equal to the node's own panelGuid as the node's own inbound and resolve it through selfKey; keep only a genuinely different (descendant) origin across hops.
2026-06-22 16:22:14 +02:00

233 lines
6.8 KiB
Go

package service
import (
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
)
// #4983: a synced inbound's OriginNodeGuid must point at the panel that
// physically hosts it. A node's own local inbound (empty origin in its
// snapshot) is attributed to the node's own GUID; an inbound the node forwards
// from its own sub-node (non-empty origin) keeps that deeper GUID across the
// hop — so a chained Node1->Node2->Node3 attributes Node3's inbounds to Node3.
func TestSetRemoteTraffic_AttributesOriginNodeGuid(t *testing.T) {
setupConflictDB(t)
db := database.GetDB()
const nodeID = 1
if err := db.Create(&model.Node{
Id: nodeID,
Name: "node2",
Address: "10.0.0.2",
Port: 2053,
ApiToken: "t",
Guid: "node2-guid",
}).Error; err != nil {
t.Fatalf("create node: %v", err)
}
snap := &runtime.TrafficSnapshot{
Inbounds: []*model.Inbound{
{ // node2's own local inbound — reports no origin
Tag: "in-443-tcp",
Enable: true,
Port: 443,
Protocol: model.VLESS,
Settings: `{"clients":[]}`,
},
{ // forwarded from node2's sub-node (node3) — carries node3's guid
Tag: "in-8443-tcp",
Enable: true,
Port: 8443,
Protocol: model.VLESS,
Settings: `{"clients":[]}`,
OriginNodeGuid: "node3-guid",
},
},
}
svc := InboundService{}
if _, err := svc.setRemoteTrafficLocked(nodeID, snap, false); err != nil {
t.Fatalf("setRemoteTrafficLocked: %v", err)
}
origin := func(tag string) string {
var ib model.Inbound
if err := db.Where("tag = ?", tag).First(&ib).Error; err != nil {
t.Fatalf("load inbound %q: %v", tag, err)
}
return ib.OriginNodeGuid
}
if og := origin("in-443-tcp"); og != "node2-guid" {
t.Fatalf("local inbound origin = %q, want node2-guid (the node's own GUID)", og)
}
if og := origin("in-8443-tcp"); og != "node3-guid" {
t.Fatalf("forwarded inbound origin = %q, want node3-guid (kept across the hop)", og)
}
}
// A cloned node reports its OWN inbound with its own (duplicated) panelGuid as
// the origin. That must be remapped to the node-unique key, not stored verbatim
// — otherwise origin_node_guid keeps the shared GUID while online is keyed by
// the node-unique key, and the inbound page reads an empty bucket (shows
// offline). A genuinely forwarded sub-node GUID is still kept across the hop.
func TestSetRemoteTraffic_RemapsClonedNodeOwnGuidOrigin(t *testing.T) {
setupConflictDB(t)
db := database.GetDB()
// Two nodes share one panelGuid (cloned servers).
for _, n := range []*model.Node{
{Id: 1, Name: "a", Address: "10.0.0.1", Port: 2053, ApiToken: "t", Guid: "dup"},
{Id: 2, Name: "b", Address: "10.0.0.2", Port: 2053, ApiToken: "t", Guid: "dup"},
} {
if err := db.Create(n).Error; err != nil {
t.Fatalf("create node %s: %v", n.Name, err)
}
}
snap := &runtime.TrafficSnapshot{
Inbounds: []*model.Inbound{
{ // node 1's OWN inbound, reporting its own (shared) panelGuid as origin
Tag: "own-443-tcp",
Enable: true,
Port: 443,
Protocol: model.VLESS,
Settings: `{"clients":[]}`,
OriginNodeGuid: "dup",
},
{ // forwarded from a sub-node with a distinct guid — kept across the hop
Tag: "fwd-8443-tcp",
Enable: true,
Port: 8443,
Protocol: model.VLESS,
Settings: `{"clients":[]}`,
OriginNodeGuid: "child-guid",
},
},
}
svc := InboundService{}
if _, err := svc.setRemoteTrafficLocked(1, snap, false); err != nil {
t.Fatalf("setRemoteTrafficLocked: %v", err)
}
origin := func(tag string) string {
var ib model.Inbound
if err := db.Where("tag = ?", tag).First(&ib).Error; err != nil {
t.Fatalf("load inbound %q: %v", tag, err)
}
return ib.OriginNodeGuid
}
if og := origin("own-443-tcp"); og != "node:1" {
t.Fatalf("cloned node's own inbound origin = %q, want node:1 (remapped from shared GUID)", og)
}
if og := origin("fwd-8443-tcp"); og != "child-guid" {
t.Fatalf("forwarded inbound origin = %q, want child-guid (kept across the hop)", og)
}
}
func TestSetRemoteTraffic_PreservesLocalShareAddressStrategy(t *testing.T) {
setupConflictDB(t)
db := database.GetDB()
const nodeID = 1
if err := db.Create(&model.Node{
Id: nodeID,
Name: "node2",
Address: "10.0.0.2",
Port: 2053,
ApiToken: "t",
Guid: "node2-guid",
}).Error; err != nil {
t.Fatalf("create node: %v", err)
}
nodeIDPtr := nodeID
if err := db.Create(&model.Inbound{
UserId: 1,
NodeID: &nodeIDPtr,
Tag: "remote-in",
Enable: true,
Port: 443,
Protocol: model.VLESS,
Settings: `{"clients":[]}`,
ShareAddrStrategy: "custom",
ShareAddr: "edge.example.com",
}).Error; err != nil {
t.Fatalf("create central inbound: %v", err)
}
snap := &runtime.TrafficSnapshot{
Inbounds: []*model.Inbound{{
Tag: "remote-in",
Enable: true,
Port: 8443,
Protocol: model.VLESS,
Settings: `{"clients":[]}`,
}},
}
svc := InboundService{}
if _, err := svc.setRemoteTrafficLocked(nodeID, snap, false); err != nil {
t.Fatalf("setRemoteTrafficLocked: %v", err)
}
var ib model.Inbound
if err := db.Where("tag = ?", "remote-in").First(&ib).Error; err != nil {
t.Fatalf("load inbound: %v", err)
}
if ib.ShareAddrStrategy != "custom" || ib.ShareAddr != "edge.example.com" {
t.Fatalf("share address fields were overwritten: strategy=%q addr=%q", ib.ShareAddrStrategy, ib.ShareAddr)
}
if ib.Port != 8443 {
t.Fatalf("sync should still update regular remote fields; port = %d, want 8443", ib.Port)
}
}
func TestSetRemoteTraffic_DefaultsShareAddressFieldsForNewCentralInbound(t *testing.T) {
setupConflictDB(t)
db := database.GetDB()
const nodeID = 1
if err := db.Create(&model.Node{
Id: nodeID,
Name: "node2",
Address: "10.0.0.2",
Port: 2053,
ApiToken: "t",
Guid: "node2-guid",
}).Error; err != nil {
t.Fatalf("create node: %v", err)
}
snap := &runtime.TrafficSnapshot{
Inbounds: []*model.Inbound{{
Tag: "remote-in",
Enable: true,
Port: 8443,
Protocol: model.VLESS,
Settings: `{"clients":[]}`,
ShareAddrStrategy: "custom",
ShareAddr: "remote.example.com",
}},
}
svc := InboundService{}
if _, err := svc.setRemoteTrafficLocked(nodeID, snap, false); err != nil {
t.Fatalf("setRemoteTrafficLocked: %v", err)
}
var ib model.Inbound
if err := db.Where("tag = ?", "remote-in").First(&ib).Error; err != nil {
t.Fatalf("load inbound: %v", err)
}
if ib.ShareAddrStrategy != "node" || ib.ShareAddr != "" {
t.Fatalf("new central inbound share fields = (%q, %q), want (node, empty)", ib.ShareAddrStrategy, ib.ShareAddr)
}
}