mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +03:00
fix(node): merge each node inbound once and sweep what the node dropped
Two shapes of a node's inbound list cost the master without bound: - an inbound listed many times was merged once per listing, every pass walking its whole client set again; the first listing now wins and the repeats are dropped; - disabled inbounds were never swept, an exemption meant for inbounds the master created but has not delivered yet, so a node could report any number of them and the master kept them all; the exemption now covers only rows without an origin, i.e. never reported by the node.
This commit is contained in:
@@ -716,8 +716,12 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
||||
|
||||
newInboundIDs := make(map[int]struct{})
|
||||
|
||||
// Every pass below walks a central inbound's clients once per snapshot entry that
|
||||
// resolves to it, so only the first such entry is merged; repeats are dropped.
|
||||
mergedCentral := make(map[int]struct{}, len(central))
|
||||
|
||||
snapTags := make(map[string]struct{}, len(snap.Inbounds))
|
||||
for _, snapIb := range snap.Inbounds {
|
||||
for snapIdx, snapIb := range snap.Inbounds {
|
||||
if snapIb == nil {
|
||||
continue
|
||||
}
|
||||
@@ -761,6 +765,13 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
||||
return false, fmt.Errorf("setRemoteTraffic: inbound %q has ambiguous compatible central aliases", snapIb.Tag)
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
if _, merged := mergedCentral[c.Id]; merged {
|
||||
snap.Inbounds[snapIdx] = nil
|
||||
continue
|
||||
}
|
||||
mergedCentral[c.Id] = struct{}{}
|
||||
}
|
||||
if !ok {
|
||||
if dirty {
|
||||
continue
|
||||
@@ -839,6 +850,7 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
||||
}
|
||||
}
|
||||
newInboundIDs[newIb.Id] = struct{}{}
|
||||
mergedCentral[newIb.Id] = struct{}{}
|
||||
structuralChange = true
|
||||
continue
|
||||
}
|
||||
@@ -929,9 +941,9 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
||||
if dirty {
|
||||
continue
|
||||
}
|
||||
// A node inbound created disabled is never delivered, so its absence from
|
||||
// the snapshot is ambiguous rather than evidence of a node-side delete.
|
||||
if !c.Enable {
|
||||
// A node inbound created disabled is never delivered, so its absence from the snapshot
|
||||
// is ambiguous; a copy the node once reported (it has an origin) is not.
|
||||
if !c.Enable && c.OriginNodeGuid == "" {
|
||||
continue
|
||||
}
|
||||
if len(snapTags) == 0 {
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
@@ -160,3 +161,55 @@ func TestSyncInboundStoresTrimmedEmail(t *testing.T) {
|
||||
t.Fatalf("links after re-sync = %d, want 1", links)
|
||||
}
|
||||
}
|
||||
|
||||
// Only a master-created inbound not yet delivered is exempt from the sweep; otherwise a
|
||||
// node could report endless disabled inbounds that the master keeps forever.
|
||||
func TestDisabledInboundTheNodeReportedIsSwept(t *testing.T) {
|
||||
db := initTrafficTestDB(t)
|
||||
svc := &InboundService{}
|
||||
seedNodeRow(t, db, &model.Node{Id: 1, Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true})
|
||||
keep := snapshotWithClients(t, "keep", `{"clients":[]}`)
|
||||
reported := &runtime.TrafficSnapshot{Inbounds: append(keep.Inbounds,
|
||||
&model.Inbound{Tag: "junk", Port: 41003, Protocol: model.VLESS, Enable: false, Settings: `{"clients":[]}`})}
|
||||
if _, err := svc.setRemoteTrafficLocked(1, reported, false, false); err != nil {
|
||||
t.Fatalf("first sync: %v", err)
|
||||
}
|
||||
nid := 1
|
||||
if err := db.Create(&model.Inbound{UserId: 1, Tag: "undelivered", Port: 41004, Protocol: model.VLESS, NodeID: &nid, Settings: `{"clients":[]}`}).Error; err != nil {
|
||||
t.Fatalf("create undelivered inbound: %v", err)
|
||||
}
|
||||
if err := db.Model(&model.Inbound{}).Where("tag = ?", "undelivered").Update("enable", false).Error; err != nil {
|
||||
t.Fatalf("disable: %v", err)
|
||||
}
|
||||
if _, err := svc.setRemoteTrafficLocked(1, snapshotWithClients(t, "keep", `{"clients":[]}`), false, false); err != nil {
|
||||
t.Fatalf("second sync: %v", err)
|
||||
}
|
||||
var tags []string
|
||||
db.Model(&model.Inbound{}).Order("tag").Pluck("tag", &tags)
|
||||
if !slices.Equal(tags, []string{"keep", "undelivered"}) {
|
||||
t.Fatalf("inbounds after the node stopped reporting junk = %v, want [keep undelivered]", tags)
|
||||
}
|
||||
}
|
||||
|
||||
// A node listing one inbound many times made every pass repeat its whole client set;
|
||||
// the first entry for an inbound is merged and the repeats are dropped.
|
||||
func TestRepeatedSnapshotEntriesMergeTheInboundOnce(t *testing.T) {
|
||||
db := initTrafficTestDB(t)
|
||||
svc := &InboundService{}
|
||||
seedNodeRow(t, db, &model.Node{Id: 1, Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true})
|
||||
snap := &runtime.TrafficSnapshot{Inbounds: []*model.Inbound{
|
||||
{Tag: "dup", Port: 41005, Protocol: model.VLESS, Enable: true, Remark: "first", Settings: `{"clients":[]}`},
|
||||
{Tag: "dup", Port: 41005, Protocol: model.VLESS, Enable: true, Remark: "second", Settings: `{"clients":[]}`},
|
||||
}}
|
||||
for range 2 {
|
||||
if _, err := svc.setRemoteTrafficLocked(1, snap, false, false); err != nil {
|
||||
t.Fatalf("sync: %v", err)
|
||||
}
|
||||
snap.Inbounds[1] = &model.Inbound{Tag: "dup", Port: 41005, Protocol: model.VLESS, Enable: true, Remark: "second", Settings: `{"clients":[]}`}
|
||||
}
|
||||
var remarks []string
|
||||
db.Model(&model.Inbound{}).Where("tag = ?", "dup").Pluck("remark", &remarks)
|
||||
if !slices.Equal(remarks, []string{"first"}) {
|
||||
t.Fatalf("remarks = %v, want the first entry merged once", remarks)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user