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
|
// GetInboundsSlim returns the same list of inbounds as GetInbounds but
|
||||||
// strips every per-client field other than email / enable / comment from
|
// strips every per-client field other than email / enable / comment from
|
||||||
// settings.clients and skips UUID/SubId enrichment on ClientStats. The
|
// 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.annotateFallbackParents(db, inbounds)
|
||||||
s.annotateLocalOriginGuid(inbounds)
|
s.annotateLocalOriginGuid(inbounds)
|
||||||
|
s.annotateAgentOriginGuid(db, inbounds)
|
||||||
// Top up stats rows owned by sibling inbounds (multi-attached clients)
|
// Top up stats rows owned by sibling inbounds (multi-attached clients)
|
||||||
// so the list's depleted/expiring badges see every client; the UUID/SubId
|
// so the list's depleted/expiring badges see every client; the UUID/SubId
|
||||||
// enrichment stays skipped. Must run before slimming strips the settings.
|
// 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