fix(inbounds): slim list stamps agent-hosted inbounds with the agent node's key
Agent inbounds defined centrally store an empty originNodeGuid, and inbounds re-homed from a panel node keep the previous host's GUID, so the inbounds page looked the agent's online set up under a key it is never filed under. GetInboundsSlim now annotates agent-hosted inbounds at read time with the node's effective attribution key (the key SetAgentOnlineClients files onlines under).
This commit is contained in:
@@ -196,6 +196,59 @@ func (s *InboundService) annotateLocalOriginGuid(inbounds []*model.Inbound) {
|
||||
}
|
||||
}
|
||||
|
||||
// annotateAgentOriginGuid fills OriginNodeGuid (read-time only, like
|
||||
// annotateLocalOriginGuid) for inbounds hosted on agent-managed nodes with
|
||||
// the node's effective attribution key — the exact key
|
||||
// SetAgentOnlineClients files the node's reported online clients under.
|
||||
//
|
||||
// Agent inbounds are defined centrally on the master, so their stored
|
||||
// OriginNodeGuid is either empty (created straight onto the agent node) or
|
||||
// STALE: an inbound re-homed from a panel node keeps the GUID the snapshot
|
||||
// merge stamped for its previous host, and nothing on the move path rewrites
|
||||
// it. The inbounds page looks the online set up by that stored GUID first,
|
||||
// so a re-homed agent inbound's online list never matched and its clients
|
||||
// never lit up. An agent has no sub-node chain to preserve attribution for —
|
||||
// the hosting node physically terminates every one of its inbounds — so the
|
||||
// node's own key is always the right value, and computing it at read time
|
||||
// also heals rows already carrying a stale GUID without a storage rewrite.
|
||||
func (s *InboundService) annotateAgentOriginGuid(db *gorm.DB, inbounds []*model.Inbound) {
|
||||
nodeIDs := make(map[int]struct{})
|
||||
for _, ib := range inbounds {
|
||||
if ib.NodeID != nil {
|
||||
nodeIDs[*ib.NodeID] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(nodeIDs) == 0 {
|
||||
return
|
||||
}
|
||||
agents := agentManagedNodeIDs()
|
||||
keyByNode := make(map[int]string, len(nodeIDs))
|
||||
for id := range nodeIDs {
|
||||
if !agents[id] {
|
||||
continue
|
||||
}
|
||||
var nodeRow model.Node
|
||||
if err := db.Select("guid").Where("id = ?", id).First(&nodeRow).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
nodeRow.Id = id
|
||||
if key := effectiveNodeKey(&nodeRow); key != "" {
|
||||
keyByNode[id] = key
|
||||
}
|
||||
}
|
||||
if len(keyByNode) == 0 {
|
||||
return
|
||||
}
|
||||
for _, ib := range inbounds {
|
||||
if ib.NodeID == nil {
|
||||
continue
|
||||
}
|
||||
if key, ok := keyByNode[*ib.NodeID]; ok {
|
||||
ib.OriginNodeGuid = key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetInboundsSlim returns the same list of inbounds as GetInbounds but
|
||||
// strips every per-client field other than email / enable / comment from
|
||||
// settings.clients and skips UUID/SubId enrichment on ClientStats. The
|
||||
@@ -215,6 +268,7 @@ func (s *InboundService) GetInboundsSlim(userId int) ([]*model.Inbound, error) {
|
||||
}
|
||||
s.annotateFallbackParents(db, inbounds)
|
||||
s.annotateLocalOriginGuid(inbounds)
|
||||
s.annotateAgentOriginGuid(db, inbounds)
|
||||
// Top up stats rows owned by sibling inbounds (multi-attached clients)
|
||||
// so the list's depleted/expiring badges see every client; the UUID/SubId
|
||||
// enrichment stays skipped. Must run before slimming strips the settings.
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
// The inbounds page looks a node's online set up under the inbound's
|
||||
// originNodeGuid (falling back to node:{id}). Agent inbounds are defined
|
||||
// centrally: a fresh one stores no origin GUID, and one re-homed from a
|
||||
// panel node keeps the GUID the snapshot merge stamped for its PREVIOUS
|
||||
// host — while the agent's onlines are filed under the agent node's own
|
||||
// effective key. The slim list must therefore stamp agent-hosted inbounds
|
||||
// with the hosting node's effective key at read time, or their clients
|
||||
// never show as online on the inbounds page (production: every converted
|
||||
// agent node was affected). Traditional inbounds' stored origins are
|
||||
// chain attribution (#4983) and must pass through untouched.
|
||||
func TestGetInboundsSlim_AnnotatesAgentOriginGuid(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
db := database.GetDB()
|
||||
svc := InboundService{}
|
||||
|
||||
const agentNodeID = 8201
|
||||
const tradNodeID = 8202
|
||||
const userID = 1
|
||||
|
||||
if err := db.Create(&model.Node{Id: agentNodeID, Name: "agent-node", Address: "10.0.8.1"}).Error; err != nil {
|
||||
t.Fatalf("create agent node: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.Node{Id: tradNodeID, Name: "trad-node", Address: "10.0.8.2", Port: 2053, ApiToken: "t", Guid: "trad-guid-8202"}).Error; err != nil {
|
||||
t.Fatalf("create trad node: %v", err)
|
||||
}
|
||||
// The agent binding is what makes the node "agent-managed".
|
||||
agentID := agentNodeID
|
||||
if err := db.Create(&model.AgentNode{
|
||||
TokenHash: fmt.Sprintf("%064d", agentNodeID),
|
||||
Name: "agent-node",
|
||||
NodeID: &agentID,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create agent binding: %v", err)
|
||||
}
|
||||
|
||||
mkInbound := func(tag string, nodeID *int, origin string) {
|
||||
ib := &model.Inbound{
|
||||
UserId: userID,
|
||||
Tag: tag,
|
||||
Enable: true,
|
||||
Port: 443,
|
||||
Protocol: model.VLESS,
|
||||
Settings: `{"clients":[{"email":"a@example.com","enable":true}]}`,
|
||||
NodeID: nodeID,
|
||||
OriginNodeGuid: origin,
|
||||
}
|
||||
if err := db.Create(ib).Error; err != nil {
|
||||
t.Fatalf("create inbound %s: %v", tag, err)
|
||||
}
|
||||
}
|
||||
agentIDCopy := agentNodeID
|
||||
tradIDCopy := tradNodeID
|
||||
mkInbound("agent-stale", &agentIDCopy, "guid-of-previous-panel-host")
|
||||
mkInbound("agent-fresh", &agentIDCopy, "")
|
||||
mkInbound("trad-synced", &tradIDCopy, "trad-guid-8202")
|
||||
|
||||
slim, err := svc.GetInboundsSlim(userID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetInboundsSlim: %v", err)
|
||||
}
|
||||
byTag := make(map[string]*model.Inbound, len(slim))
|
||||
for _, ib := range slim {
|
||||
byTag[ib.Tag] = ib
|
||||
}
|
||||
|
||||
wantKey := fmt.Sprintf("node:%d", agentNodeID)
|
||||
if got := byTag["agent-stale"].OriginNodeGuid; got != wantKey {
|
||||
t.Errorf("agent-stale originNodeGuid = %q, want %q (stale host GUID healed to the agent's key)", got, wantKey)
|
||||
}
|
||||
if got := byTag["agent-fresh"].OriginNodeGuid; got != wantKey {
|
||||
t.Errorf("agent-fresh originNodeGuid = %q, want %q", got, wantKey)
|
||||
}
|
||||
if got := byTag["trad-synced"].OriginNodeGuid; got != "trad-guid-8202" {
|
||||
t.Errorf("trad-synced originNodeGuid = %q, want stored GUID untouched", got)
|
||||
}
|
||||
|
||||
// Read-time only: the stored value must not be rewritten.
|
||||
var stored model.Inbound
|
||||
if err := db.Where("tag = ?", "agent-stale").First(&stored).Error; err != nil {
|
||||
t.Fatalf("reload agent-stale: %v", err)
|
||||
}
|
||||
if stored.OriginNodeGuid != "guid-of-previous-panel-host" {
|
||||
t.Errorf("stored origin_node_guid = %q, want the stale value preserved (annotation is read-time only)", stored.OriginNodeGuid)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user