fix(db): clamp traffic counters below int64 max and repair overflowed rows

A counter pushed past int64 (multi-node setups hit this via historic
delta-compounding bugs) makes SQLite silently promote the INTEGER cell
to REAL. From then on the column no longer scans into the Go int64
field and every reader of client_traffics fails at once: the inbounds
page, xray restarts, and node traffic sync all return "converting
driver.Value type float64 to int64".

Two-part fix: every unbounded "up = up + ?" add (local traffic, node
delta merge, inbound counters, plus the Go-side outbound accumulation)
now saturates at TrafficMax, a cap safely below math.MaxInt64 so one
more delta cannot overflow; and a startup repair casts REAL-promoted
cells back to INTEGER and clamps all traffic counters into
[0, TrafficMax] across client_traffics, inbounds, outbound_traffics
and node_client_traffics, restoring access to already-corrupted panels
without manual sqlite surgery.

Closes #5762
This commit is contained in:
MHSanaei
2026-07-05 20:33:09 +02:00
parent b1fa76f9b6
commit 837cf5f24e
6 changed files with 185 additions and 7 deletions
+3 -1
View File
@@ -744,10 +744,12 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
if err := tx.Exec(
fmt.Sprintf(
`UPDATE client_traffics
SET up = up + ?, down = down + ?, enable = %s, total = ?,
SET up = %s, down = %s, enable = %s, total = ?,
expiry_time = CASE WHEN expiry_time > 0 AND CAST(? AS BIGINT) <= 0 THEN expiry_time ELSE CAST(? AS BIGINT) END,
reset = ?, last_online = %s
WHERE email = ?`,
database.ClampedAddExpr("up"),
database.ClampedAddExpr("down"),
enableExpr,
database.GreatestExpr("last_online", "?"),
),
+5 -3
View File
@@ -87,8 +87,8 @@ func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic
if traffic.IsInbound {
err = tx.Model(&model.Inbound{}).Where("tag = ? AND node_id IS NULL", traffic.Tag).
Updates(map[string]any{
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down),
"up": gorm.Expr(database.ClampedAddExpr("up"), traffic.Up),
"down": gorm.Expr(database.ClampedAddExpr("down"), traffic.Down),
}).Error
if err != nil {
return err
@@ -153,7 +153,9 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
}
if err = tx.Exec(
fmt.Sprintf(
`UPDATE client_traffics SET up = up + ?, down = down + ?, last_online = %s WHERE email = ?`,
`UPDATE client_traffics SET up = %s, down = %s, last_online = %s WHERE email = ?`,
database.ClampedAddExpr("up"),
database.ClampedAddExpr("down"),
database.GreatestExpr("last_online", "?"),
),
t.Up, t.Down, now, ct.Email,
+13 -3
View File
@@ -42,6 +42,16 @@ func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []
return nil, false
}
// saturatingAdd caps counters at database.TrafficMax: unlike the SQL paths,
// this read-modify-write add happens in Go, where an int64 overflow silently
// wraps negative instead of erroring (#5762).
func saturatingAdd(a, b int64) int64 {
if b > database.TrafficMax-a {
return database.TrafficMax
}
return a + b
}
func (s *OutboundService) addOutboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
if len(traffics) == 0 {
return nil
@@ -61,9 +71,9 @@ func (s *OutboundService) addOutboundTraffic(tx *gorm.DB, traffics []*xray.Traff
}
outbound.Tag = traffic.Tag
outbound.Up = outbound.Up + traffic.Up
outbound.Down = outbound.Down + traffic.Down
outbound.Total = outbound.Up + outbound.Down
outbound.Up = saturatingAdd(outbound.Up, traffic.Up)
outbound.Down = saturatingAdd(outbound.Down, traffic.Down)
outbound.Total = saturatingAdd(outbound.Up, outbound.Down)
err = tx.Save(&outbound).Error
if err != nil {