mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-22 20:01:02 +00:00
Some checks are pending
CI / go-test (push) Waiting to run
CI / postgres-durable-first (push) Waiting to run
CI / codegen (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / race (push) Waiting to run
CI / node-e2e (push) Waiting to run
CI / fuzz-smoke (push) Waiting to run
CI / golangci (push) Waiting to run
CI / frontend (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Docs CI / build (push) Waiting to run
Docs Deploy (GitHub Pages) / build (push) Waiting to run
Docs Deploy (GitHub Pages) / deploy (push) Blocked by required conditions
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
When a node snapshot reports an inbound under a tag that matches neither the central tag nor an origin/port alias, SetRemoteTraffic adopts it as a new central inbound and, in the same tick, sweeps the old one. The sweep deleted the old inbound's client_traffics rows even though the snapshot still reported those emails under the new inbound. existingEmails was computed before the sweep, so the rows were not recreated that tick; on the next tick the inbound was no longer new, the rows came back seeded at 0/0 and the node baseline jumped to the node's totals. Quota accounting lost every byte the clients had used. The sweep now moves rows for emails the snapshot still reports to the inbound now carrying them, instead of deleting them. Only emails the node no longer reports at all lose their row, mirroring the existing baseline rule. The alias check also treated a master-created node inbound that had not been tag-matched yet (empty origin_node_guid) as foreign, so a renamed tag on its first sync replaced it instead of aliasing it. An empty origin now counts as this node. PostgreSQL lane not run locally; the change is a plain UPDATE through GORM. Closes #6749
90 lines
3.5 KiB
Go
90 lines
3.5 KiB
Go
package service
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"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/xray"
|
|
)
|
|
|
|
// A snapshot tag that matches neither the central tag nor an alias replaces the
|
|
// inbound in one tick; the client's accumulated usage must survive the swap.
|
|
func TestSetRemoteTraffic_InboundReplacedKeepsClientHistory(t *testing.T) {
|
|
db := initTrafficTestDB(t)
|
|
const nodeID = 2
|
|
if err := db.Create(&model.Node{Id: nodeID, Name: "node", Address: "10.0.0.2", Port: 2053, ApiToken: "t", Guid: "node-guid"}).Error; err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
const email = "baba"
|
|
settings := `{"clients":[{"email":"baba","enable":true}]}`
|
|
createNodeInboundWithClient(t, db, nodeID, "n2-in-2053-tcp", 2053, email)
|
|
svc := &InboundService{}
|
|
sync := func(up, down int64) {
|
|
t.Helper()
|
|
snap := &runtime.TrafficSnapshot{Inbounds: []*model.Inbound{{
|
|
Tag: "in-2053-tcp", OriginNodeGuid: "node-guid", Enable: true, Port: 2053, Protocol: model.VLESS,
|
|
Settings: settings, ClientStats: []xray.ClientTraffic{{Email: email, Up: up, Down: down, Enable: true}},
|
|
}}}
|
|
if _, err := svc.setRemoteTrafficLocked(nodeID, snap, false, false); err != nil {
|
|
t.Fatalf("setRemoteTrafficLocked: %v", err)
|
|
}
|
|
}
|
|
|
|
sync(100, 200)
|
|
sync(600, 1200)
|
|
assertUpDown(t, readTraffic(t, db, email), 500, 1000, "before the replace")
|
|
|
|
// Desync the central row so the next snapshot neither tag-matches nor aliases it.
|
|
if err := db.Model(&model.Inbound{}).Where("node_id = ?", nodeID).
|
|
Updates(map[string]any{"tag": "n2-legacy", "origin_node_guid": "stale-guid"}).Error; err != nil {
|
|
t.Fatalf("desync central inbound: %v", err)
|
|
}
|
|
|
|
sync(650, 1300)
|
|
assertUpDown(t, readTraffic(t, db, email), 550, 1100, "replace tick")
|
|
sync(700, 1400)
|
|
assertUpDown(t, readTraffic(t, db, email), 600, 1200, "tick after the replace")
|
|
|
|
var ib model.Inbound
|
|
if err := db.Where("node_id = ?", nodeID).First(&ib).Error; err != nil {
|
|
t.Fatalf("read node inbound: %v", err)
|
|
}
|
|
if ib.Tag != "in-2053-tcp" {
|
|
t.Fatalf("fixture did not replace the inbound: surviving tag %q", ib.Tag)
|
|
}
|
|
if ct := readTraffic(t, db, email); ct.InboundId != ib.Id {
|
|
t.Errorf("client_traffics.inbound_id = %d, want the surviving inbound %d", ct.InboundId, ib.Id)
|
|
}
|
|
}
|
|
|
|
// A node inbound created on the master has no origin until its first tag match;
|
|
// that empty origin is still this node, so a renamed tag aliases instead of replacing.
|
|
func TestSetRemoteTraffic_AliasesInboundWithEmptyOrigin(t *testing.T) {
|
|
db := initTrafficTestDB(t)
|
|
const nodeID = 2
|
|
if err := db.Create(&model.Node{Id: nodeID, Name: "node", Address: "10.0.0.2", Port: 2053, ApiToken: "t", Guid: "node-guid"}).Error; err != nil {
|
|
t.Fatalf("create node: %v", err)
|
|
}
|
|
createNodeInbound(t, db, nodeID, "n2-in-2053-tcp", 2053)
|
|
var before model.Inbound
|
|
if err := db.Where("node_id = ?", nodeID).First(&before).Error; err != nil {
|
|
t.Fatalf("read node inbound: %v", err)
|
|
}
|
|
|
|
snap := &runtime.TrafficSnapshot{Inbounds: []*model.Inbound{{
|
|
Tag: "in-2053-tcp-2", OriginNodeGuid: "node-guid", Enable: true, Port: 2053, Protocol: model.VLESS,
|
|
Settings: `{"clients":[]}`,
|
|
}}}
|
|
if _, err := (&InboundService{}).setRemoteTrafficLocked(nodeID, snap, false, false); err != nil {
|
|
t.Fatalf("setRemoteTrafficLocked: %v", err)
|
|
}
|
|
|
|
var rows []model.Inbound
|
|
if err := db.Where("node_id = ?", nodeID).Find(&rows).Error; err != nil {
|
|
t.Fatalf("list node inbounds: %v", err)
|
|
}
|
|
if len(rows) != 1 || rows[0].Id != before.Id {
|
|
t.Fatalf("node inbounds = %#v, want only the original id %d", rows, before.Id)
|
|
}
|
|
}
|