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
+48
View File
@@ -113,6 +113,9 @@ func initModels() error {
if err := normalizeInboundSubSortIndex(); err != nil {
return err
}
if err := repairOverflowedTrafficCounters(); err != nil {
return err
}
if err := migrateLegacySocksInboundsToMixed(); err != nil {
return err
}
@@ -518,6 +521,51 @@ func normalizeInboundSubSortIndex() error {
return nil
}
// repairOverflowedTrafficCounters heals traffic counters that historic
// compounding bugs pushed past int64: on SQLite an overflowing INTEGER is
// silently promoted to REAL, after which the column no longer scans into the
// Go int64 field and every reader of the table fails (#5762). REAL cells are
// cast back to INTEGER (SQLite caps the cast at math.MaxInt64), then values
// are clamped into [0, TrafficMax] on both backends so the next delta cannot
// overflow again.
func repairOverflowedTrafficCounters() error {
targets := []struct {
table string
columns []string
}{
{"client_traffics", []string{"up", "down"}},
{"inbounds", []string{"up", "down"}},
{"outbound_traffics", []string{"up", "down", "total"}},
{"node_client_traffics", []string{"up", "down"}},
}
for _, target := range targets {
for _, col := range target.columns {
statements := []string{
fmt.Sprintf("UPDATE %s SET %s = %d WHERE %s > %d", target.table, col, TrafficMax, col, TrafficMax),
fmt.Sprintf("UPDATE %s SET %s = 0 WHERE %s < 0", target.table, col, col),
}
if !IsPostgres() {
statements = append([]string{
fmt.Sprintf("UPDATE %s SET %s = CAST(%s AS INTEGER) WHERE typeof(%s) = 'real'", target.table, col, col, col),
}, statements...)
}
var repaired int64
for _, statement := range statements {
res := db.Exec(statement)
if res.Error != nil {
log.Printf("Error repairing %s.%s: %v", target.table, col, res.Error)
return res.Error
}
repaired += res.RowsAffected
}
if repaired > 0 {
log.Printf("Repaired %d overflowed %s.%s value(s)", repaired, target.table, col)
}
}
}
return nil
}
func isIgnorableDuplicateColumnErr(err error, mdl any) bool {
if err == nil {
return false