fix(traffic): fix flow counter inflation from TOCTOU race in agent traffic reporter

Replace read-then-subtract pattern in collectAndReport with atomic
swap-to-zero to eliminate race where AddTraffic increments counters
between snapshot and clearReportedTraffic, causing residual traffic
to accumulate indefinitely and inflate user flow counters.

Also add defensive check in processFlowItem to skip AddFlow when
forward no longer exists, and send DeleteService to clean up orphaned
agent services.
This commit is contained in:
sagitchu
2026-04-21 14:32:08 +08:00
parent eaf16bf17b
commit 30e1473f06
3 changed files with 47 additions and 70 deletions
@@ -43,16 +43,19 @@ func (h *Handler) processFlowItem(nodeID int64, item flowItem) {
forwardID, userID, userTunnelID, ok := parseFlowServiceIDs(serviceName)
if ok {
inFlow, outFlow := h.scaleFlowByTunnel(forwardID, item.D, item.U)
_ = h.repo.AddFlow(forwardID, userID, userTunnelID, inFlow, outFlow)
if quota, quotaErr := h.repo.AddUserQuotaUsage(userID, inFlow+outFlow, time.Now()); quotaErr == nil {
h.enforceUserQuotaIfNeeded(userID, quota)
if h.forwardExists(forwardID) {
inFlow, outFlow := h.scaleFlowByTunnel(forwardID, item.D, item.U)
_ = h.repo.AddFlow(forwardID, userID, userTunnelID, inFlow, outFlow)
if quota, quotaErr := h.repo.AddUserQuotaUsage(userID, inFlow+outFlow, time.Now()); quotaErr == nil {
h.enforceUserQuotaIfNeeded(userID, quota)
}
if userTunnelID > 0 {
h.enforceFlowPolicies(userID, userTunnelID)
}
} else if nodeID > 0 {
h.sendDeleteOrphanedForwardService(nodeID, serviceName)
}
h.processPeerShareFlowFromForward(forwardID, nodeID, serviceName, item)
if userTunnelID > 0 {
h.enforceFlowPolicies(userID, userTunnelID)
}
return
}
@@ -628,6 +631,21 @@ func (h *Handler) forwardExists(forwardID int64) bool {
return ok
}
func (h *Handler) sendDeleteOrphanedForwardService(nodeID int64, serviceName string) {
parts := strings.Split(serviceName, "_")
if len(parts) < 3 {
return
}
forwardID, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil || forwardID <= 0 {
return
}
base := parts[0] + "_" + parts[1] + "_" + parts[2]
_, _ = h.sendNodeCommand(nodeID, "DeleteService", map[string]interface{}{
"services": []string{base + "_tcp", base + "_udp"},
}, false, true)
}
func (h *Handler) speedLimiterExists(name string) bool {
if name == "" {
return false