mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-15 14:32:03 +00:00
feat: validate matchmaking latency evidence
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ProbeFreshness = 30 * time.Second
|
||||
ProbeFutureSkew = 5 * time.Second
|
||||
MaxOpaqueLocationBytes = 512
|
||||
DiscrepancyWindow = 24 * time.Hour
|
||||
DiscrepancyLimit = 3
|
||||
CleanSamplesToRelease = 5
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidProbe = errors.New("invalid latency probe evidence")
|
||||
ErrProbeQuarantined = errors.New("latency samples are quarantined")
|
||||
)
|
||||
|
||||
// ProbeEvidence deliberately treats Steam's location as opaque. The backend
|
||||
// validates freshness/nonce and computes RTT from its own receive timestamps;
|
||||
// no client-provided RTT is used for placement.
|
||||
type ProbeEvidence struct {
|
||||
OpaqueLocation []byte
|
||||
Nonce []byte
|
||||
IssuedAt time.Time
|
||||
Region string
|
||||
ServerRTT time.Duration
|
||||
}
|
||||
|
||||
func ValidateProbe(evidence ProbeEvidence, expectedNonce []byte, now time.Time) error {
|
||||
if len(evidence.OpaqueLocation) == 0 || len(evidence.OpaqueLocation) > MaxOpaqueLocationBytes || len(expectedNonce) == 0 {
|
||||
return ErrInvalidProbe
|
||||
}
|
||||
if len(evidence.Nonce) != len(expectedNonce) || subtle.ConstantTimeCompare(evidence.Nonce, expectedNonce) != 1 {
|
||||
return fmt.Errorf("%w: nonce mismatch", ErrInvalidProbe)
|
||||
}
|
||||
if evidence.IssuedAt.After(now.Add(ProbeFutureSkew)) || now.Sub(evidence.IssuedAt) > ProbeFreshness {
|
||||
return fmt.Errorf("%w: stale or future timestamp", ErrInvalidProbe)
|
||||
}
|
||||
if evidence.Region != "EU" && evidence.Region != "NA" {
|
||||
return fmt.Errorf("%w: unsupported region", ErrInvalidProbe)
|
||||
}
|
||||
if evidence.ServerRTT < 0 {
|
||||
return fmt.Errorf("%w: negative RTT", ErrInvalidProbe)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DiscrepancyTracker struct {
|
||||
BadSamples []time.Time
|
||||
CleanSamples int
|
||||
Quarantined bool
|
||||
}
|
||||
|
||||
// RecordComparison compares backend-computed predicted and observed RTT. A
|
||||
// discrepancy is over 25 ms or 30% (whichever is larger). Three bad samples
|
||||
// in 24 hours quarantine placement evidence; five clean samples release it.
|
||||
func (tracker *DiscrepancyTracker) RecordComparison(predicted, observed time.Duration, now time.Time) {
|
||||
tracker.prune(now)
|
||||
maxAllowed := 25 * time.Millisecond
|
||||
if predicted > 0 {
|
||||
percent := time.Duration(float64(predicted) * 0.30)
|
||||
if percent > maxAllowed { maxAllowed = percent }
|
||||
}
|
||||
delta := predicted - observed
|
||||
if delta < 0 { delta = -delta }
|
||||
if delta > maxAllowed {
|
||||
tracker.BadSamples = append(tracker.BadSamples, now)
|
||||
tracker.CleanSamples = 0
|
||||
if len(tracker.BadSamples) >= DiscrepancyLimit { tracker.Quarantined = true }
|
||||
return
|
||||
}
|
||||
if tracker.Quarantined {
|
||||
tracker.CleanSamples++
|
||||
if tracker.CleanSamples >= CleanSamplesToRelease {
|
||||
tracker.Quarantined = false
|
||||
tracker.BadSamples = nil
|
||||
tracker.CleanSamples = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tracker *DiscrepancyTracker) prune(now time.Time) {
|
||||
cutoff := now.Add(-DiscrepancyWindow)
|
||||
kept := tracker.BadSamples[:0]
|
||||
for _, sample := range tracker.BadSamples {
|
||||
if !sample.Before(cutoff) { kept = append(kept, sample) }
|
||||
}
|
||||
tracker.BadSamples = kept
|
||||
}
|
||||
|
||||
func (tracker DiscrepancyTracker) PlacementAllowed() error {
|
||||
if tracker.Quarantined { return ErrProbeQuarantined }
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user