fix(v1.0.2): agent placeholder in inbound export links + sync job wiping agent onlines
Fix 1: InboundsPage hostOverrideFor read NodeView.address raw; agent
nodes store the literal placeholder "agent" there, so exported inbound
links came out as vless://...@agent:<port>. Route the pick through the
new shareHostForNode (lib/xray/inbound-link): resolved ipv4/ipv6 first,
raw address only as a non-placeholder fallback; the placeholder can
never reach a link and the fallback chain continues when an agent has
no telemetry yet. Traditional nodes (address only) are unchanged.
Fix 2: agent nodes showed no online users on the nodes page. The
attribution keys were NOT misaligned (write effectiveNodeKey ==
read effectiveNodeGuid == "node:{id}" for guid-less agent nodes,
pinned by TestAgentNodeOnlineEmailsInNodeList). The node traffic sync
job (every 5s) pull-syncs every enabled+online node as if it were a
panel; agent nodes have no panel API, so FetchTrafficSnapshot always
failed and its failure branch ClearNodeOnlineClients wiped the online
tree the agent had just pushed via SetAgentOnlineClients. syncOne now
skips agent-managed nodes (push-only), mirroring the heartbeat job.
Pinned by TestSyncOne_AgentNodeKeepsReportedOnlines (fails pre-fix
with byGuid emptied after one tick, passes post-fix).
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
// An agent node's reported online clients must surface in the node list
|
||||
// aggregation (NodeView.OnlineEmails), like they do on the inbounds page.
|
||||
// Production shape: the agent node row carries the create form's placeholder
|
||||
// address and no panel GUID of its own; an agent_nodes binding row ties the
|
||||
// agent token to the node; the node hosts inbounds.
|
||||
//
|
||||
// This pins the write/read key alignment: SetAgentOnlineClients files the
|
||||
// list under effectiveNodeKey of the guid-only node row, and the node list
|
||||
// looks it up under effectiveNodeGuid of the full row — for a guid-less
|
||||
// agent node both are the synthetic "node:{id}" key. (The node page going
|
||||
// dark in production was NOT a key mismatch: the traffic sync job kept
|
||||
// deleting the filed tree — see the job package's
|
||||
// TestSyncOne_AgentNodeKeepsReportedOnlines.)
|
||||
func TestAgentNodeOnlineEmailsInNodeList(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
db := database.GetDB()
|
||||
|
||||
const nodeID = 7601
|
||||
ibSvc := InboundService{}
|
||||
ibSvc.ClearNodeOnlineClients(nodeID)
|
||||
t.Cleanup(func() { ibSvc.ClearNodeOnlineClients(nodeID) })
|
||||
|
||||
if err := db.Create(&model.Node{
|
||||
Id: nodeID, Name: "agent-node-7601", Address: "agent", Enable: true, Status: "online",
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create node: %v", err)
|
||||
}
|
||||
nid := nodeID
|
||||
if err := db.Create(&model.AgentNode{
|
||||
TokenHash: "hash-7601", NodeID: &nid,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create agent binding: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.Inbound{
|
||||
Tag: "agent-in-1", Enable: true, Port: 46994, Protocol: model.VLESS,
|
||||
Settings: `{"clients":[]}`, NodeID: &nid,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create inbound: %v", err)
|
||||
}
|
||||
|
||||
ibSvc.SetAgentOnlineClients(nodeID, []string{"online@example.com"})
|
||||
|
||||
var row model.Node
|
||||
if err := db.Select("guid").Where("id = ?", nodeID).First(&row).Error; err != nil {
|
||||
t.Fatalf("load node guid: %v", err)
|
||||
}
|
||||
row.Id = nodeID
|
||||
if writeKey := effectiveNodeKey(&row); writeKey != "node:7601" {
|
||||
t.Fatalf("write key = %q, want node:7601", writeKey)
|
||||
}
|
||||
assertStringSet(t, "byGuid[node:7601]", ibSvc.GetOnlineClientsByGuid()["node:7601"],
|
||||
[]string{"online@example.com"})
|
||||
|
||||
views, err := (&NodeService{}).GetNodeTreeView()
|
||||
if err != nil {
|
||||
t.Fatalf("GetNodeTreeView: %v", err)
|
||||
}
|
||||
for _, v := range views {
|
||||
if v.Id == nodeID {
|
||||
if v.OnlineCount != 1 {
|
||||
t.Fatalf("agent node OnlineCount = %d, want 1", v.OnlineCount)
|
||||
}
|
||||
assertStringSet(t, "agent node OnlineEmails", v.OnlineEmails,
|
||||
[]string{"online@example.com"})
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("agent node %d missing from node list views", nodeID)
|
||||
}
|
||||
Reference in New Issue
Block a user