mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-13 16:16:06 +00:00
* feat(nodes): add stable panel GUID identity (multi-hop phase 0) Per-panel autoincrement node ids are meaningless one hop away, so in a chained topology (Node1 -> Node2 -> Node3) the master cannot attribute online clients or inbounds to the physical node that hosts them (#4983). Introduce a stable self-identifier: each panel generates and persists a panelGuid (settings table, mirroring GetSecret), returns it in panel/api/server/status, and the master learns it per node via the heartbeat into a new Node.Guid column. Guarded so an old-build node or a failed probe never clears a known GUID. No behavior change yet - this is the identity foundation Phases 1-2 key on. Refs #4983 * feat(nodes): attribute inbounds to their origin node by GUID (multi-hop phase 1) Add Inbound.OriginNodeGuid: the GUID of the panel that physically hosts an inbound. Empty means this panel's own xray; set means it was synced from a node. SetRemoteTraffic now fills it per synced inbound - keeping a non-empty value the node forwarded from its own sub-node (so a transitive inbound stays attributed to the deepest node across hops), and otherwise attributing the node's own local inbounds to that node's GUID. Empty (old-build node without a GUID) leaves the existing node_id-based attribution untouched. The field rides the existing inbound JSON, so /list propagates it up the chain with no serve-side change. Phase 2 will key per-node online off this instead of the panel-local node_id. Refs #4983 * feat(nodes): key online status by node GUID end-to-end (multi-hop phase 2) Replace the panel-local node-id keying of per-node online status with the stable panelGuid, so a client several hops down a node chain is attributed to the node that physically hosts it instead of the intermediate node it syncs through (#4983). xray/process.go stores each direct node's reported GUID-keyed subtree and merges them (correct at any depth); the service assembles GetOnlineClientsByGuid (own clients under this panel's GUID + every node under its GUID). FetchTrafficSnapshot fetches the new /clients/onlinesByGuid, falling back to the flat /onlines for old-build nodes (keyed under the node's GUID or a master-local synthetic id). The node rollup, the WS onlineByGuid/activeInbounds fields, and the inbounds-page rollup all scope by GUID; local inbounds get their OriginNodeGuid filled with the panel's GUID at serve time so the frontend keys uniformly. Old-build nodes degrade to the prior flat behaviour via the synthetic node:<id> key. Refs #4983 Refs #4983 * feat(nodes): surface transitive sub-nodes on the master (multi-hop phase 3a) Each panel publishes read-only summaries of the nodes it manages via GET /panel/api/server/descendants (node API token). The heartbeat job caches each direct node's summaries; GetNodeTree merges them as transitive model.Node projections (Id 0, Transitive=true, ParentGuid = their parent node's GUID) and recomputes InboundCount/OnlineCount/DepletedCount per origin GUID so a direct node shows only its own inbounds and each sub-node shows its own (#4983). The Nodes-page list endpoint and the heartbeat broadcast now return the tree; GetAll stays direct-only for probing/syncing. One transitive level is surfaced (covers Node1->Node2->Node3); deeper recursion is a follow-up. Backend only - the Nodes-page nested UI lands next. Refs #4983 * feat(nodes): render transitive sub-nodes nested + read-only on the Nodes page (multi-hop phase 3b) The Nodes page now shows a node's downstream sub-nodes (learned via the descendants tree) as indented, read-only rows ordered right under their parent: no enable toggle, probe, edit, delete, update, selection, or history expander - just a 'Sub-node' tag whose tooltip names the parent it is reached through. Desktop table and mobile cards both handle it. Transitive rows are keyed by GUID (their Id is 0) so they don't collide with real nodes (#4983). Rows nest by parentGuid rather than AntD tree-children to avoid clashing with the existing per-row history expander. New labels added to en-US (other locales fall back until translated). Refs #4983 Refs #4983 * i18n(nodes): translate subNode/subNodeTip across all locales Phase 3b added these two Nodes-page keys (read-only sub-node tag + tooltip) only to en-US; fill in the other 12 locales so the multi-hop sub-node UI is fully localized. The {parent} placeholder is preserved in every translation. Refs #4983
This commit is contained in:
+91
-72
@@ -155,13 +155,18 @@ type process struct {
|
||||
// two signals stay aligned — an email within grace always has the
|
||||
// inbound it used within grace too.
|
||||
localInboundLastActive map[string]int64
|
||||
// nodeOnlineClients holds the online-emails list reported by each
|
||||
// remote node, keyed by node id. NodeTrafficSyncJob populates entries
|
||||
// per cron tick and clears them when a node's probe fails. The mutex
|
||||
// guards this map, onlineClients, and localLastOnline above so the
|
||||
// nodeOnlineTrees holds, per direct remote node (keyed by that node's
|
||||
// panel-local id), the GUID-keyed online-emails subtree that node
|
||||
// reported — its own clients under its panelGuid plus every descendant
|
||||
// under theirs. Keying the stored value by GUID (not node id) lets the
|
||||
// master attribute a deeply nested client to the node that physically
|
||||
// hosts it across a chain (#4983); the outer node-id key is only so a
|
||||
// failed probe can drop that whole branch's contribution. NodeTrafficSyncJob
|
||||
// populates entries per cron tick and clears them when a probe fails. The
|
||||
// mutex guards this map, onlineClients, and localLastOnline above so the
|
||||
// online getters never see a torn read.
|
||||
nodeOnlineClients map[int][]string
|
||||
onlineMu sync.RWMutex
|
||||
nodeOnlineTrees map[int]map[string][]string
|
||||
onlineMu sync.RWMutex
|
||||
|
||||
config *Config
|
||||
configPath string // if set, use this path instead of GetConfigPath() and remove on Stop
|
||||
@@ -177,12 +182,6 @@ var (
|
||||
xrayForceStopTimeout = 2 * time.Second
|
||||
)
|
||||
|
||||
// localNodeKey is the GetOnlineClientsByNode key under which this panel's
|
||||
// own (non-node-managed) inbounds report their online clients. Node ids
|
||||
// autoincrement from 1, so 0 is a safe sentinel that never collides with a
|
||||
// real node. The frontend mirrors this contract (nodeId ?? 0).
|
||||
const localNodeKey = 0
|
||||
|
||||
// newProcess creates a new internal process struct for Xray.
|
||||
func newProcess(config *Config) *process {
|
||||
return &process{
|
||||
@@ -255,7 +254,7 @@ func (p *Process) GetOnlineClients() []string {
|
||||
p.onlineMu.RLock()
|
||||
defer p.onlineMu.RUnlock()
|
||||
|
||||
if len(p.nodeOnlineClients) == 0 {
|
||||
if len(p.nodeOnlineTrees) == 0 {
|
||||
// Hot path for single-panel deployments: avoid the map+dedupe
|
||||
// work entirely and return the local slice as-is.
|
||||
return p.onlineClients
|
||||
@@ -263,15 +262,8 @@ func (p *Process) GetOnlineClients() []string {
|
||||
|
||||
seen := make(map[string]struct{}, len(p.onlineClients))
|
||||
out := make([]string, 0, len(p.onlineClients))
|
||||
for _, email := range p.onlineClients {
|
||||
if _, dup := seen[email]; dup {
|
||||
continue
|
||||
}
|
||||
seen[email] = struct{}{}
|
||||
out = append(out, email)
|
||||
}
|
||||
for _, list := range p.nodeOnlineClients {
|
||||
for _, email := range list {
|
||||
add := func(emails []string) {
|
||||
for _, email := range emails {
|
||||
if _, dup := seen[email]; dup {
|
||||
continue
|
||||
}
|
||||
@@ -279,53 +271,79 @@ func (p *Process) GetOnlineClients() []string {
|
||||
out = append(out, email)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetOnlineClientsByNode returns online emails grouped by the node that
|
||||
// reported them: this panel's own xray clients under localNodeKey (0), and
|
||||
// each remote node's clients under that node's id. Unlike GetOnlineClients
|
||||
// (which flattens everything into one deduped union), this preserves node
|
||||
// attribution so per-inbound/per-node online counts don't bleed a client
|
||||
// connected to one node onto every other node. Empty groups are omitted.
|
||||
func (p *Process) GetOnlineClientsByNode() map[int][]string {
|
||||
p.onlineMu.RLock()
|
||||
defer p.onlineMu.RUnlock()
|
||||
|
||||
out := make(map[int][]string, len(p.nodeOnlineClients)+1)
|
||||
if len(p.onlineClients) > 0 {
|
||||
local := make([]string, len(p.onlineClients))
|
||||
copy(local, p.onlineClients)
|
||||
out[localNodeKey] = local
|
||||
}
|
||||
for nodeID, list := range p.nodeOnlineClients {
|
||||
if len(list) == 0 {
|
||||
continue
|
||||
add(p.onlineClients)
|
||||
for _, tree := range p.nodeOnlineTrees {
|
||||
for _, emails := range tree {
|
||||
add(emails)
|
||||
}
|
||||
cp := make([]string, len(list))
|
||||
copy(cp, list)
|
||||
out[nodeID] = cp
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetActiveInboundsByNode returns the inbound tags that carried traffic within
|
||||
// the grace window, grouped by node. Only this panel's own xray reports
|
||||
// per-inbound activity (under localNodeKey); remote-node snapshots don't carry
|
||||
// it, so their nodes are simply absent — the per-inbound view reads "node
|
||||
// missing" as "don't gate" and falls back to the email-only signal there.
|
||||
// Empty groups are omitted, mirroring GetOnlineClientsByNode.
|
||||
func (p *Process) GetActiveInboundsByNode() map[int][]string {
|
||||
// GetLocalOnlineClients returns a copy of the emails online on THIS panel's own
|
||||
// xray within the grace window. The service layer keys these under the panel's
|
||||
// own GUID when assembling the per-node online view.
|
||||
func (p *Process) GetLocalOnlineClients() []string {
|
||||
p.onlineMu.RLock()
|
||||
defer p.onlineMu.RUnlock()
|
||||
|
||||
if len(p.localActiveInbounds) == 0 {
|
||||
return map[int][]string{}
|
||||
if len(p.onlineClients) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[int][]string, 1)
|
||||
local := make([]string, len(p.localActiveInbounds))
|
||||
copy(local, p.localActiveInbounds)
|
||||
out[localNodeKey] = local
|
||||
out := make([]string, len(p.onlineClients))
|
||||
copy(out, p.onlineClients)
|
||||
return out
|
||||
}
|
||||
|
||||
// GetMergedNodeTrees returns the union of every direct node's reported subtree,
|
||||
// keyed by the panelGuid of the node that physically hosts each client set.
|
||||
// Because each child already reports its descendants under their own GUIDs,
|
||||
// merging the direct children yields the whole tree at any depth (#4983), so a
|
||||
// client three hops down is attributed to its real node, not the intermediate
|
||||
// one. GUIDs are globally unique, but a set reported under the same GUID by more
|
||||
// than one path is deduped per key; empty sets are omitted.
|
||||
func (p *Process) GetMergedNodeTrees() map[string][]string {
|
||||
p.onlineMu.RLock()
|
||||
defer p.onlineMu.RUnlock()
|
||||
if len(p.nodeOnlineTrees) == 0 {
|
||||
return map[string][]string{}
|
||||
}
|
||||
out := make(map[string][]string)
|
||||
seen := make(map[string]map[string]struct{})
|
||||
for _, tree := range p.nodeOnlineTrees {
|
||||
for guid, emails := range tree {
|
||||
if guid == "" || len(emails) == 0 {
|
||||
continue
|
||||
}
|
||||
dedup := seen[guid]
|
||||
if dedup == nil {
|
||||
dedup = make(map[string]struct{}, len(emails))
|
||||
seen[guid] = dedup
|
||||
}
|
||||
for _, email := range emails {
|
||||
if _, ok := dedup[email]; ok {
|
||||
continue
|
||||
}
|
||||
dedup[email] = struct{}{}
|
||||
out[guid] = append(out[guid], email)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetLocalActiveInbounds returns a copy of THIS panel's inbound tags that
|
||||
// carried traffic within the grace window. Only the local xray reports
|
||||
// per-inbound activity; remote-node snapshots don't carry it, so the service
|
||||
// layer keys these under the panel's own GUID and a node missing from the
|
||||
// active-inbounds map means "don't gate" (fall back to the email-only signal).
|
||||
func (p *Process) GetLocalActiveInbounds() []string {
|
||||
p.onlineMu.RLock()
|
||||
defer p.onlineMu.RUnlock()
|
||||
if len(p.localActiveInbounds) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, len(p.localActiveInbounds))
|
||||
copy(out, p.localActiveInbounds)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -371,25 +389,26 @@ func (p *Process) RefreshLocalOnline(activeEmails, activeInboundTags []string, n
|
||||
p.localActiveInbounds = activeInbounds
|
||||
}
|
||||
|
||||
// SetNodeOnlineClients records the online-emails set for one remote
|
||||
// node. Replaces any previous entry for that node — NodeTrafficSyncJob
|
||||
// always sends the full list per tick.
|
||||
func (p *Process) SetNodeOnlineClients(nodeID int, emails []string) {
|
||||
// SetNodeOnlineTree records the GUID-keyed online subtree one direct remote
|
||||
// node reported (its own clients under its panelGuid plus every descendant
|
||||
// under theirs). Replaces any previous entry for that node — NodeTrafficSyncJob
|
||||
// always sends the full subtree per tick.
|
||||
func (p *Process) SetNodeOnlineTree(nodeID int, tree map[string][]string) {
|
||||
p.onlineMu.Lock()
|
||||
defer p.onlineMu.Unlock()
|
||||
if p.nodeOnlineClients == nil {
|
||||
p.nodeOnlineClients = map[int][]string{}
|
||||
if p.nodeOnlineTrees == nil {
|
||||
p.nodeOnlineTrees = map[int]map[string][]string{}
|
||||
}
|
||||
p.nodeOnlineClients[nodeID] = emails
|
||||
p.nodeOnlineTrees[nodeID] = tree
|
||||
}
|
||||
|
||||
// ClearNodeOnlineClients drops a node's contribution to the online set.
|
||||
// Called when a probe fails so a downed node doesn't keep its clients
|
||||
// listed as "online" until the next successful probe.
|
||||
// ClearNodeOnlineClients drops a direct node's whole subtree contribution.
|
||||
// Called when a probe fails so a downed node — and everything behind it — doesn't
|
||||
// keep its clients listed as "online" until the next successful probe.
|
||||
func (p *Process) ClearNodeOnlineClients(nodeID int) {
|
||||
p.onlineMu.Lock()
|
||||
defer p.onlineMu.Unlock()
|
||||
delete(p.nodeOnlineClients, nodeID)
|
||||
delete(p.nodeOnlineTrees, nodeID)
|
||||
}
|
||||
|
||||
// GetUptime returns the uptime of the Xray process in seconds.
|
||||
|
||||
Reference in New Issue
Block a user