fix(sub): agent-hosted inbounds export the agent's telemetry address, not the 'agent' placeholder
Agent nodes store the create form's placeholder in nodes.address
('agent', port 2053; NodeFormModal.buildPayload). The export fallback
chain treated it as a real node address, so every host-less inbound on
an agent node produced links like vless://...@agent:31324.
SubService now loads agent_nodes bindings alongside nodes (latest
last_seen per node, same rule as the node list) and resolves an
agent-managed node's export address through the single convergence
point agentNodeAddress(nodeID): telemetry IPv4, then IPv6, then ""
so the chain continues to listen/public/request host and can never
emit the placeholder. Host-bound inbounds and traditional nodes are
untouched. GetInboundOptions applies the same substitution via
agentAdvertisedAddresses so the frontend share/QR/WireGuard feed
never sees the placeholder either.
This commit is contained in:
@@ -87,3 +87,89 @@ func TestGetInboundOptions_NodeAddress(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user