mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
111 lines
3.0 KiB
Go
111 lines
3.0 KiB
Go
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
|
|
}
|