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,89 @@
|
||||
package job
|
||||
|
||||
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"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
||||
)
|
||||
|
||||
// REPRO (V1.0.2 fix 2): agent nodes are push-only — their online clients
|
||||
// arrive via agent traffic reports (InboundService.SetAgentOnlineClients)
|
||||
// and are keyed exactly where the node list looks them up (the service-level
|
||||
// alignment is pinned by TestReproAgentNodeOnlineEmailsInNodeList). But the
|
||||
// traffic sync job's pull loop treats every enabled+online node as a panel:
|
||||
// for an agent node the snapshot fetch can never succeed (address is the
|
||||
// literal placeholder "agent", there is no panel API behind it), and the
|
||||
// fetch-failure branch calls ClearNodeOnlineClients — wiping the agent's
|
||||
// pushed online state on every tick. The heartbeat job already skips
|
||||
// agent-managed nodes for probing; the sync job must not pull them either.
|
||||
func TestSyncOne_AgentNodeKeepsReportedOnlines(t *testing.T) {
|
||||
setupIntegrationDB(t)
|
||||
db := database.GetDB()
|
||||
|
||||
node := &model.Node{
|
||||
Name: "agent-sync-node",
|
||||
Address: "agent",
|
||||
Port: 2053,
|
||||
Enable: true,
|
||||
Status: "online",
|
||||
}
|
||||
if err := db.Create(node).Error; err != nil {
|
||||
t.Fatalf("create node: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.AgentNode{
|
||||
TokenHash: "hash-agent-sync-node",
|
||||
Name: "agent-sync-node",
|
||||
NodeID: &node.Id,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create agent binding: %v", err)
|
||||
}
|
||||
// Production form: the agent node actually hosts inbounds (without any
|
||||
// node-bound inbound in the DB, NodeService.GetAll takes its early
|
||||
// return and never fills per-node stats at all).
|
||||
if err := db.Create(&model.Inbound{
|
||||
Tag: "agent-sync-in-1", Enable: true, Port: 46994, Protocol: model.VLESS,
|
||||
Settings: `{"clients":[]}`, NodeID: &node.Id,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create inbound: %v", err)
|
||||
}
|
||||
|
||||
inboundSvc := service.InboundService{}
|
||||
inboundSvc.ClearNodeOnlineClients(node.Id)
|
||||
t.Cleanup(func() { inboundSvc.ClearNodeOnlineClients(node.Id) })
|
||||
|
||||
inboundSvc.SetAgentOnlineClients(node.Id, []string{"agent-online@example.com"})
|
||||
if got := inboundSvc.GetOnlineClients(); len(got) != 1 {
|
||||
t.Fatalf("precondition: online union = %v, want the agent-reported email", got)
|
||||
}
|
||||
|
||||
j := NewNodeTrafficSyncJob()
|
||||
j.syncOne(runtime.NewManager(), node, false)
|
||||
|
||||
byGuid := inboundSvc.GetOnlineClientsByGuid()
|
||||
found := false
|
||||
for _, emails := range byGuid {
|
||||
for _, e := range emails {
|
||||
if e == "agent-online@example.com" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("agent onlines wiped by sync tick: byGuid = %v", byGuid)
|
||||
}
|
||||
|
||||
views, err := (&service.NodeService{}).GetNodeTreeView()
|
||||
if err != nil {
|
||||
t.Fatalf("GetNodeTreeView: %v", err)
|
||||
}
|
||||
for _, v := range views {
|
||||
if v.Id == node.Id {
|
||||
if len(v.OnlineEmails) != 1 || v.OnlineEmails[0] != "agent-online@example.com" {
|
||||
t.Fatalf("node view online emails = %v, want [agent-online@example.com]", v.OnlineEmails)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user