mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 03:05:02 +03:00
fix(node): bound the panel GUIDs a peer reports
A peer's panelGuid is copied into nodes.guid and into every inbound row it reports (an indexed column), a forwarded origin GUID likewise, and a GUID-keyed IP tree or a master's global-traffic push keys one row per client. One oversized value therefore multiplied into gigabytes stored. GUIDs over 64 bytes (panels mint 36-character UUIDs) now fail the heartbeat, fall back to the reporting node's key, or are dropped.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -233,3 +234,17 @@ func TestSnapshotListNotOverlaid_SlimOverlaid(t *testing.T) {
|
|||||||
t.Errorf("slim list should carry the global overlay, got up=%d", slim[0].ClientStats[0].Up)
|
t.Errorf("slim list should carry the global overlay, got up=%d", slim[0].ClientStats[0].Up)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The master GUID keys one stored row per client, so an oversized one is dropped.
|
||||||
|
func TestAcceptGlobalTrafficDropsAnOversizedMasterGuid(t *testing.T) {
|
||||||
|
db := initTrafficTestDB(t)
|
||||||
|
seedClientRow(t, "alice", 1, 0, 0, 0)
|
||||||
|
if err := (&InboundService{}).AcceptGlobalTraffic(strings.Repeat("m", maxPanelGuidLen+1), []*xray.ClientTraffic{{Email: "alice", Up: 1, Down: 1}}); err != nil {
|
||||||
|
t.Fatalf("AcceptGlobalTraffic: %v", err)
|
||||||
|
}
|
||||||
|
var rows int64
|
||||||
|
db.Model(&model.ClientGlobalTraffic{}).Count(&rows)
|
||||||
|
if rows != 0 {
|
||||||
|
t.Fatalf("stored %d row(s) under an oversized master GUID", rows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -561,7 +561,7 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
|||||||
selfKey := effectiveNodeKey(&model.Node{Id: nodeID, Guid: nodeRow.Guid})
|
selfKey := effectiveNodeKey(&model.Node{Id: nodeID, Guid: nodeRow.Guid})
|
||||||
guidShared := nodeRow.Guid != "" && selfKey != nodeRow.Guid
|
guidShared := nodeRow.Guid != "" && selfKey != nodeRow.Guid
|
||||||
originGuidFor := func(snapIb *model.Inbound) string {
|
originGuidFor := func(snapIb *model.Inbound) string {
|
||||||
if snapIb.OriginNodeGuid != "" && snapIb.OriginNodeGuid != nodeRow.Guid {
|
if snapIb.OriginNodeGuid != "" && snapIb.OriginNodeGuid != nodeRow.Guid && len(snapIb.OriginNodeGuid) <= maxPanelGuidLen {
|
||||||
return snapIb.OriginNodeGuid
|
return snapIb.OriginNodeGuid
|
||||||
}
|
}
|
||||||
return selfKey
|
return selfKey
|
||||||
|
|||||||
@@ -139,6 +139,10 @@ func (s *InboundService) MergeClientIpsByGuid(node *model.Node, trees map[string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for guid, perEmail := range trees {
|
for guid, perEmail := range trees {
|
||||||
|
// Stored once per email: an oversized key would multiply into every row.
|
||||||
|
if len(guid) > maxPanelGuidLen {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err := upsertNodeClientIps(guid, perEmail); err != nil {
|
if err := upsertNodeClientIps(guid, perEmail); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const globalTrafficHeartbeatInterval = time.Hour
|
|||||||
// Identical usage refreshes hourly; client_traffics remains local-only for delta accounting.
|
// Identical usage refreshes hourly; client_traffics remains local-only for delta accounting.
|
||||||
func (s *InboundService) AcceptGlobalTraffic(masterGuid string, traffics []*xray.ClientTraffic) error {
|
func (s *InboundService) AcceptGlobalTraffic(masterGuid string, traffics []*xray.ClientTraffic) error {
|
||||||
masterGuid = strings.TrimSpace(masterGuid)
|
masterGuid = strings.TrimSpace(masterGuid)
|
||||||
if masterGuid == "" {
|
if masterGuid == "" || len(masterGuid) > maxPanelGuidLen {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
emails := make([]string, 0, len(traffics))
|
emails := make([]string, 0, len(traffics))
|
||||||
|
|||||||
@@ -1285,6 +1285,10 @@ func (b *outboundBridge) removeInbound() {
|
|||||||
// broken node from dictating the master's allocation on every heartbeat.
|
// broken node from dictating the master's allocation on every heartbeat.
|
||||||
const maxProbeBodyBytes = 1 << 20 // 1 MiB
|
const maxProbeBodyBytes = 1 << 20 // 1 MiB
|
||||||
|
|
||||||
|
// maxPanelGuidLen bounds a GUID a peer reports: panels mint 36-character UUIDs, and the
|
||||||
|
// value is copied into every inbound row and indexed columns.
|
||||||
|
const maxPanelGuidLen = 64
|
||||||
|
|
||||||
func (s *NodeService) probe(ctx context.Context, n *model.Node, proxyURL string) (HeartbeatPatch, error) {
|
func (s *NodeService) probe(ctx context.Context, n *model.Node, proxyURL string) (HeartbeatPatch, error) {
|
||||||
patch := HeartbeatPatch{LastHeartbeat: time.Now().Unix()}
|
patch := HeartbeatPatch{LastHeartbeat: time.Now().Unix()}
|
||||||
|
|
||||||
@@ -1392,6 +1396,10 @@ func (s *NodeService) probe(ctx context.Context, n *model.Node, proxyURL string)
|
|||||||
patch.XrayState = o.Xray.State
|
patch.XrayState = o.Xray.State
|
||||||
patch.XrayError = o.Xray.ErrorMsg
|
patch.XrayError = o.Xray.ErrorMsg
|
||||||
patch.PanelVersion = o.PanelVersion
|
patch.PanelVersion = o.PanelVersion
|
||||||
|
if len(o.PanelGuid) > maxPanelGuidLen {
|
||||||
|
patch.LastError = fmt.Sprintf("remote panel reported a %d-byte panelGuid", len(o.PanelGuid))
|
||||||
|
return patch, errors.New(patch.LastError)
|
||||||
|
}
|
||||||
patch.Guid = o.PanelGuid
|
patch.Guid = o.PanelGuid
|
||||||
patch.UptimeSecs = o.Uptime
|
patch.UptimeSecs = o.Uptime
|
||||||
patch.NetUp = o.NetIO.Up
|
patch.NetUp = o.NetIO.Up
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||||
@@ -448,3 +449,24 @@ func TestSetRemoteTraffic_DefaultsShareAddressFieldsForNewCentralInbound(t *test
|
|||||||
t.Fatalf("new central inbound share fields = (%q, %q), want (node, empty)", ib.ShareAddrStrategy, ib.ShareAddr)
|
t.Fatalf("new central inbound share fields = (%q, %q), want (node, empty)", ib.ShareAddrStrategy, ib.ShareAddr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An origin GUID is copied into the inbound row and its index; one a node forwards over
|
||||||
|
// the GUID bound is attributed to the reporting node instead of being stored.
|
||||||
|
func TestSetRemoteTraffic_RefusesAnOversizedForwardedOrigin(t *testing.T) {
|
||||||
|
setupConflictDB(t)
|
||||||
|
db := database.GetDB()
|
||||||
|
if err := db.Create(&model.Node{Id: 1, Name: "node2", Address: "10.0.0.2", Port: 2053, ApiToken: "t", Guid: "node2-guid"}).Error; err != nil {
|
||||||
|
t.Fatalf("create node: %v", err)
|
||||||
|
}
|
||||||
|
snap := &runtime.TrafficSnapshot{Inbounds: []*model.Inbound{{
|
||||||
|
Tag: "in-443-tcp", Enable: true, Port: 443, Protocol: model.VLESS, Settings: `{"clients":[]}`,
|
||||||
|
OriginNodeGuid: strings.Repeat("g", maxPanelGuidLen+1),
|
||||||
|
}}}
|
||||||
|
if _, err := (&InboundService{}).setRemoteTrafficLocked(1, snap, false, false); err != nil {
|
||||||
|
t.Fatalf("setRemoteTrafficLocked: %v", err)
|
||||||
|
}
|
||||||
|
var ib model.Inbound
|
||||||
|
if err := db.Where("tag = ?", "in-443-tcp").First(&ib).Error; err != nil || ib.OriginNodeGuid != "node2-guid" {
|
||||||
|
t.Fatalf("origin = %d bytes (%v), want the reporting node's GUID", len(ib.OriginNodeGuid), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,3 +48,19 @@ func TestProbeRejectsOversizedStatusBody(t *testing.T) {
|
|||||||
t.Fatalf("LastError = %q, want a \"decode response: \" rejection", patch.LastError)
|
t.Fatalf("LastError = %q, want a \"decode response: \" rejection", patch.LastError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A panelGuid is copied into every inbound the node reports and into indexed columns, so
|
||||||
|
// one oversized value multiplies into gigabytes of stored copies.
|
||||||
|
func TestProbeRefusesAnOversizedPanelGuid(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
_, _ = w.Write([]byte(`{"success":true,"obj":{"cpuPct":1,"panelGuid":"` + strings.Repeat("g", maxPanelGuidLen+1) + `"}}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
u, _ := url.Parse(srv.URL)
|
||||||
|
port, _ := strconv.Atoi(u.Port())
|
||||||
|
n := &model.Node{Id: 1, Scheme: "http", Address: u.Hostname(), Port: port, BasePath: "/", Enable: true, AllowPrivateAddress: true}
|
||||||
|
patch, err := (&NodeService{}).Probe(context.Background(), n)
|
||||||
|
if err == nil || patch.Guid != "" || !strings.Contains(patch.LastError, "panelGuid") {
|
||||||
|
t.Fatalf("Probe = guid %d bytes, %v (%q); want the heartbeat refused", len(patch.Guid), err, patch.LastError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user