package service import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) // TestGetInboundOptions_NodeAddress verifies that a node-managed inbound carries // its hosting node's externally reachable address, while this panel's own // inbounds report an empty NodeAddress. The clients page uses it as the // WireGuard endpoint host so a copied config points at the node, not the master. func TestGetInboundOptions_NodeAddress(t *testing.T) { setupConflictDB(t) node := &model.Node{Name: "de-fra-1", Address: "node.example.net", Port: 2053, Enable: true} if err := database.GetDB().Create(node).Error; err != nil { t.Fatalf("create node: %v", err) } nodeInbound := &model.Inbound{ UserId: 1, Tag: "in-51820-udp", Enable: true, Listen: "0.0.0.0", Port: 51820, Protocol: model.WireGuard, Settings: `{"clients":[],"secretKey":"QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0="}`, NodeID: &node.Id, } localInbound := &model.Inbound{ UserId: 1, Tag: "in-443-tcp", Enable: true, Listen: "0.0.0.0", Port: 443, Protocol: model.VLESS, StreamSettings: `{"network":"tcp"}`, Settings: `{"clients":[]}`, ShareAddrStrategy: "custom", ShareAddr: "vpn.example.com", } if err := database.GetDB().Create(nodeInbound).Error; err != nil { t.Fatalf("create node inbound: %v", err) } if err := database.GetDB().Create(localInbound).Error; err != nil { t.Fatalf("create local inbound: %v", err) } svc := &InboundService{} options, err := svc.GetInboundOptions(1) if err != nil { t.Fatalf("GetInboundOptions: %v", err) } byID := make(map[int]InboundOption, len(options)) for _, o := range options { byID[o.Id] = o } got, ok := byID[nodeInbound.Id] if !ok { t.Fatalf("node inbound %d missing from options", nodeInbound.Id) } if got.NodeAddress != "node.example.net" { t.Fatalf("node inbound NodeAddress = %q, want node.example.net", got.NodeAddress) } if got.Listen != "0.0.0.0" { t.Fatalf("node inbound Listen = %q, want 0.0.0.0", got.Listen) } if got.ShareAddrStrategy != "" { t.Fatalf("node inbound ShareAddrStrategy = %q, want empty (the default node strategy is elided so omitempty drops it)", got.ShareAddrStrategy) } local, ok := byID[localInbound.Id] if !ok { t.Fatalf("local inbound %d missing from options", localInbound.Id) } if local.NodeAddress != "" { t.Fatalf("local inbound NodeAddress = %q, want empty", local.NodeAddress) } if local.ShareAddrStrategy != "custom" { t.Fatalf("local inbound ShareAddrStrategy = %q, want custom", local.ShareAddrStrategy) } if local.ShareAddr != "vpn.example.com" { t.Fatalf("local inbound ShareAddr = %q, want vpn.example.com", local.ShareAddr) } } // TestGetInboundOptions_AgentNodeAddress verifies that an agent-managed // node's inbounds never inherit the node row's stored address: the // create form writes the literal placeholder "agent" there, which is // not a connectable host. The option must carry the agent's // telemetry-reported address instead (IPv4 first, then IPv6; empty when // the agent never reported one), so share/QR links and WireGuard // endpoints rendered from these options reach the agent host. func TestGetInboundOptions_AgentNodeAddress(t *testing.T) { setupConflictDB(t) db := database.GetDB() mkNode := func(name string) *model.Node { n := &model.Node{Name: name, Scheme: "https", Address: "agent", Port: 2053, Enable: true} if err := db.Create(n).Error; err != nil { t.Fatalf("create node %s: %v", name, err) } return n } mkInbound := func(tag string, node *model.Node) *model.Inbound { ib := &model.Inbound{ UserId: 1, Tag: tag, Enable: true, Listen: "0.0.0.0", Port: 31324, Protocol: model.VLESS, Settings: `{"clients":[]}`, NodeID: &node.Id, } if err := db.Create(ib).Error; err != nil { t.Fatalf("create inbound %s: %v", tag, err) } return ib } mkBinding := func(node *model.Node, ipv4, ipv6 string, lastSeen int64) { ag := &model.AgentNode{ TokenHash: "hash-" + node.Name, Name: node.Name, NodeID: &node.Id, IPv4: ipv4, IPv6: ipv6, LastSeen: lastSeen, } if err := db.Create(ag).Error; err != nil { t.Fatalf("create binding for %s: %v", node.Name, err) } } dualNode := mkNode("agent-dual") mkBinding(dualNode, "198.51.100.23", "2001:db8::23", 2000) dualInbound := mkInbound("in-agent-dual", dualNode) v6Node := mkNode("agent-v6") mkBinding(v6Node, "", "2001:db8::99", 2000) v6Inbound := mkInbound("in-agent-v6", v6Node) silentNode := mkNode("agent-silent") mkBinding(silentNode, "", "", 2000) silentInbound := mkInbound("in-agent-silent", silentNode) svc := &InboundService{} options, err := svc.GetInboundOptions(1) if err != nil { t.Fatalf("GetInboundOptions: %v", err) } byID := make(map[int]InboundOption, len(options)) for _, o := range options { byID[o.Id] = o } if got := byID[dualInbound.Id].NodeAddress; got != "198.51.100.23" { t.Fatalf("dual-stack agent NodeAddress = %q, want telemetry IPv4 198.51.100.23 (never the placeholder)", got) } if got := byID[v6Inbound.Id].NodeAddress; got != "2001:db8::99" { t.Fatalf("ipv6-only agent NodeAddress = %q, want telemetry IPv6 2001:db8::99", got) } if got := byID[silentInbound.Id].NodeAddress; got != "" { t.Fatalf("silent agent NodeAddress = %q, want empty (never the placeholder)", got) } for id, o := range byID { if o.NodeAddress == "agent" { t.Fatalf("inbound %d NodeAddress is the placeholder %q", id, o.NodeAddress) } } }