mirror of
https://github.com/Quad4-Software/Reticulum-Go
synced 2026-08-29 23:48:44 -04:00
617 lines
16 KiB
Go
617 lines
16 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright (c) 2024-2026 Quad4.io
|
|
|
|
package protect
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"quad4/reticulum-go/pkg/health"
|
|
)
|
|
|
|
func fixedOpts(mode Mode, maxPPS float64) Options {
|
|
return Options{
|
|
Mode: mode,
|
|
MaxPPS: maxPPS,
|
|
WarnInterval: time.Hour,
|
|
DisableAdaptive: true,
|
|
DisableCoolDown: true,
|
|
}
|
|
}
|
|
|
|
func TestParseMode(t *testing.T) {
|
|
cases := map[string]Mode{
|
|
"off": ModeOff,
|
|
"detect": ModeDetect,
|
|
"prevent": ModePrevent,
|
|
"ids": ModeDetect,
|
|
"ips": ModePrevent,
|
|
"block": ModePrevent,
|
|
"": ModeOff,
|
|
}
|
|
for in, want := range cases {
|
|
got, ok := ParseMode(in)
|
|
if !ok || got != want {
|
|
t.Fatalf("ParseMode(%q) = %v %v want %v true", in, got, ok, want)
|
|
}
|
|
}
|
|
if _, ok := ParseMode("bogus"); ok {
|
|
t.Fatal("expected bogus mode to fail")
|
|
}
|
|
}
|
|
|
|
func TestAdmitPacketOffNoTrip(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
opts := fixedOpts(ModeOff, 10)
|
|
opts.WarnWriter = &buf
|
|
e := New(opts)
|
|
for range 100 {
|
|
d := e.AdmitPacket("udp0", 100)
|
|
if !d.Allow || d.Trip {
|
|
t.Fatalf("off mode decision %#v", d)
|
|
}
|
|
}
|
|
if buf.Len() != 0 {
|
|
t.Fatalf("unexpected warn %q", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestAdmitPacketDetectTripsAllows(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
clock := time.Unix(1_700_000_000, 0)
|
|
opts := fixedOpts(ModeDetect, 5)
|
|
opts.WarnWriter = &buf
|
|
opts.Now = func() time.Time { return clock }
|
|
e := New(opts)
|
|
var tripped bool
|
|
for range 20 {
|
|
d := e.AdmitPacket("udp0", 10)
|
|
if !d.Allow {
|
|
t.Fatal("detect must allow")
|
|
}
|
|
if d.Trip {
|
|
tripped = true
|
|
}
|
|
}
|
|
if !tripped {
|
|
t.Fatal("expected pps trip")
|
|
}
|
|
if e.TripCount(ReasonPPS) == 0 {
|
|
t.Fatal("expected trip count")
|
|
}
|
|
if !strings.Contains(buf.String(), "WARNING: dos_protection detect trip reason=pps iface=udp0") {
|
|
t.Fatalf("warn missing got %q", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestAdmitPacketPreventBlocks(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
clock := time.Unix(1_700_000_000, 0)
|
|
opts := fixedOpts(ModePrevent, 5)
|
|
opts.WarnWriter = &buf
|
|
opts.Now = func() time.Time { return clock }
|
|
e := New(opts)
|
|
blocked := false
|
|
for range 20 {
|
|
d := e.AdmitPacket("tcp0", 10)
|
|
if !d.Allow {
|
|
blocked = true
|
|
if !d.Trip || d.Reason != ReasonPPS {
|
|
t.Fatalf("unexpected decision %#v", d)
|
|
}
|
|
}
|
|
}
|
|
if !blocked {
|
|
t.Fatal("prevent must block after threshold")
|
|
}
|
|
}
|
|
|
|
func TestWarnRateLimit(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
clock := time.Unix(1_700_000_000, 0)
|
|
opts := fixedOpts(ModeDetect, 1)
|
|
opts.WarnWriter = &buf
|
|
opts.WarnInterval = time.Second
|
|
opts.Now = func() time.Time { return clock }
|
|
e := New(opts)
|
|
for range 10 {
|
|
_ = e.AdmitPacket("udp0", 1)
|
|
}
|
|
lines := strings.Count(buf.String(), "\n")
|
|
if lines != 1 {
|
|
t.Fatalf("want 1 warn line got %d %q", lines, buf.String())
|
|
}
|
|
clock = clock.Add(2 * time.Second)
|
|
for range 3 {
|
|
_ = e.AdmitPacket("udp0", 1)
|
|
}
|
|
if !strings.Contains(buf.String(), "suppressed=") {
|
|
t.Fatalf("expected suppressed count %q", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestAdmitHandlerModes(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
detect := New(Options{Mode: ModeDetect, WarnWriter: &buf, WarnInterval: time.Hour, DisableCoolDown: true})
|
|
d := detect.AdmitHandler("udp0")
|
|
if !d.Allow || !d.Trip || d.Reason != ReasonHandler {
|
|
t.Fatalf("detect handler %#v", d)
|
|
}
|
|
prevent := New(Options{Mode: ModePrevent, WarnWriter: &buf, WarnInterval: time.Hour, DisableCoolDown: true})
|
|
d = prevent.AdmitHandler("udp0")
|
|
if d.Allow || !d.Trip {
|
|
t.Fatalf("prevent handler %#v", d)
|
|
}
|
|
}
|
|
|
|
func TestAdmitConnPreventCap(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
e := New(Options{Mode: ModePrevent, MaxConns: 2, WarnWriter: &buf, WarnInterval: time.Hour, DisableCoolDown: true})
|
|
d1, r1 := e.AdmitConn("tcp0")
|
|
d2, r2 := e.AdmitConn("tcp0")
|
|
d3, r3 := e.AdmitConn("tcp0")
|
|
if !d1.Allow || !d2.Allow {
|
|
t.Fatal("first two should allow")
|
|
}
|
|
if d3.Allow || !d3.Trip {
|
|
t.Fatalf("third should block %#v", d3)
|
|
}
|
|
r1()
|
|
r2()
|
|
r3()
|
|
d4, r4 := e.AdmitConn("tcp0")
|
|
if !d4.Allow {
|
|
t.Fatal("after release should allow")
|
|
}
|
|
r4()
|
|
}
|
|
|
|
func TestAdmitConnDetectOverCap(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
e := New(Options{Mode: ModeDetect, MaxConns: 1, WarnWriter: &buf, WarnInterval: time.Hour, DisableCoolDown: true})
|
|
d1, r1 := e.AdmitConn("tcp0")
|
|
d2, r2 := e.AdmitConn("tcp0")
|
|
if !d1.Allow || !d2.Allow {
|
|
t.Fatal("detect must allow over cap")
|
|
}
|
|
if !d2.Trip {
|
|
t.Fatal("detect must trip over cap")
|
|
}
|
|
r1()
|
|
r2()
|
|
}
|
|
|
|
func TestAdmitResourcePreventCap(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
e := New(Options{Mode: ModePrevent, MaxResources: 1, WarnWriter: &buf, WarnInterval: time.Hour})
|
|
d1, r1 := e.AdmitResource(100)
|
|
d2, r2 := e.AdmitResource(100)
|
|
if !d1.Allow || d2.Allow {
|
|
t.Fatalf("resource cap d1=%#v d2=%#v", d1, d2)
|
|
}
|
|
r1()
|
|
r2()
|
|
}
|
|
|
|
func TestAdmitCryptoPreventCap(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
e := New(Options{Mode: ModePrevent, MaxCrypto: 1, WarnWriter: &buf, WarnInterval: time.Hour})
|
|
d1, r1 := e.AdmitCrypto("udp0")
|
|
d2, r2 := e.AdmitCrypto("udp0")
|
|
if !d1.Allow || d2.Allow {
|
|
t.Fatalf("crypto cap d1=%#v d2=%#v", d1, d2)
|
|
}
|
|
if e.TripCount(ReasonCrypto) == 0 {
|
|
t.Fatal("expected crypto trip")
|
|
}
|
|
r1()
|
|
r2()
|
|
}
|
|
|
|
func TestAdmitHandshakePreventCap(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
e := New(Options{Mode: ModePrevent, MaxHandshake: 1, WarnWriter: &buf, WarnInterval: time.Hour})
|
|
d1, r1 := e.AdmitHandshake("tcp0")
|
|
d2, r2 := e.AdmitHandshake("tcp0")
|
|
if !d1.Allow || d2.Allow {
|
|
t.Fatalf("handshake cap d1=%#v d2=%#v", d1, d2)
|
|
}
|
|
r1()
|
|
r2()
|
|
}
|
|
|
|
func TestCoolDownAfterSustainedTrips(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
clock := time.Unix(1_700_000_000, 0)
|
|
e := New(Options{
|
|
Mode: ModePrevent,
|
|
MaxPPS: 2,
|
|
WarnWriter: &buf,
|
|
WarnInterval: time.Hour,
|
|
DisableAdaptive: true,
|
|
DisableCoolDown: false,
|
|
EnableIfaceCoolDown: true,
|
|
Now: func() time.Time { return clock },
|
|
})
|
|
for range CoolDownTripThreshold + 5 {
|
|
_ = e.AdmitPacket("cd0", 1)
|
|
clock = clock.Add(time.Millisecond)
|
|
}
|
|
if e.TripCount(ReasonCoolDown) == 0 {
|
|
t.Fatal("expected cooldown trip")
|
|
}
|
|
if !e.InCoolDown("cd0") {
|
|
t.Fatal("expected iface in cooldown")
|
|
}
|
|
d := e.AdmitPacket("cd0", 1)
|
|
if d.Allow || d.Reason != ReasonCoolDown {
|
|
t.Fatalf("cooldown should block %#v", d)
|
|
}
|
|
clock = clock.Add(CoolDownDuration + time.Second)
|
|
d = e.AdmitPacket("cd0", 1)
|
|
if d.Reason == ReasonCoolDown && !d.Allow {
|
|
t.Fatal("cooldown should have expired")
|
|
}
|
|
}
|
|
|
|
func TestIfaceCoolDownOffByDefault(t *testing.T) {
|
|
health.Default.Reset()
|
|
clock := time.Unix(1_700_000_000, 0)
|
|
e := New(Options{
|
|
Mode: ModePrevent,
|
|
MaxPPS: 2,
|
|
WarnWriter: &bytes.Buffer{},
|
|
WarnInterval: time.Hour,
|
|
DisableAdaptive: true,
|
|
Now: func() time.Time { return clock },
|
|
})
|
|
for range CoolDownTripThreshold + 20 {
|
|
_ = e.admitWithOpts("pub0", 1, AdmitOpts{Class: ClassShedFirst})
|
|
clock = clock.Add(time.Millisecond)
|
|
}
|
|
if e.InCoolDown("pub0") {
|
|
t.Fatal("default policy must not blackhole a public iface")
|
|
}
|
|
clock = clock.Add(time.Second)
|
|
d := e.admitWithOpts("pub0", 1, AdmitOpts{Class: ClassPreferKeep})
|
|
if !d.Allow {
|
|
t.Fatalf("quiet link packet after announce flood must still pass %#v", d)
|
|
}
|
|
}
|
|
|
|
func TestAdaptiveRaisesTripLineAboveFloor(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
clock := time.Unix(1_700_000_000, 0)
|
|
e := New(Options{
|
|
Mode: ModePrevent,
|
|
MaxPPS: 2000,
|
|
FloorPPS: 20,
|
|
WarnWriter: &buf,
|
|
WarnInterval: time.Hour,
|
|
DisableAdaptive: false,
|
|
DisableCoolDown: true,
|
|
Now: func() time.Time { return clock },
|
|
})
|
|
// Quiet traffic to learn a low baseline.
|
|
for range AdaptiveWarmupSamples + 2 {
|
|
_ = e.AdmitPacket("ad0", 1)
|
|
clock = clock.Add(2 * time.Second)
|
|
}
|
|
// Burst well above adaptive line but below absolute max.
|
|
clock = clock.Add(time.Second)
|
|
blocked := 0
|
|
for range 200 {
|
|
d := e.AdmitPacket("ad0", 1)
|
|
if !d.Allow {
|
|
blocked++
|
|
}
|
|
}
|
|
if blocked == 0 {
|
|
t.Fatal("adaptive prevent should block burst above quiet baseline")
|
|
}
|
|
}
|
|
|
|
func TestObserveMemoryShed(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
var heap uint64 = 100
|
|
e := New(Options{
|
|
Mode: ModePrevent,
|
|
SoftMemoryLimit: 1000,
|
|
WarnWriter: &buf,
|
|
WarnInterval: time.Hour,
|
|
DisableCoolDown: true,
|
|
MemorySampleFunc: func() uint64 { return heap },
|
|
})
|
|
e.ObserveMemory()
|
|
if e.Shedding() {
|
|
t.Fatal("should not shed yet")
|
|
}
|
|
heap = 900
|
|
e.ObserveMemory()
|
|
if !e.Shedding() {
|
|
t.Fatal("should shed at 85%")
|
|
}
|
|
d := e.AdmitPacket("udp0", 1)
|
|
if d.Allow {
|
|
t.Fatal("prevent must block under memory shed")
|
|
}
|
|
heap = 600
|
|
e.ObserveMemory()
|
|
if e.Shedding() {
|
|
t.Fatal("should clear shed below 70%")
|
|
}
|
|
}
|
|
|
|
func TestMemoryShedEnforcedDuringAutoLearning(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
var heap uint64 = 100
|
|
e := New(Options{
|
|
Mode: ModeAuto,
|
|
SoftMemoryLimit: 1000,
|
|
WarnWriter: &buf,
|
|
WarnInterval: time.Hour,
|
|
DisableCoolDown: true,
|
|
MemorySampleFunc: func() uint64 { return heap },
|
|
})
|
|
if e.Phase() != AutoLearning {
|
|
t.Fatal("expected fresh auto engine to start in learning phase")
|
|
}
|
|
heap = 900
|
|
e.ObserveMemory()
|
|
if !e.Shedding() {
|
|
t.Fatal("should shed at 85%")
|
|
}
|
|
d := e.AdmitPacket("udp0", 1)
|
|
if d.Allow {
|
|
t.Fatal("auto mode must enforce memory shed immediately, even while still learning a baseline")
|
|
}
|
|
if e.Phase() != AutoLearning {
|
|
t.Fatal("memory enforcement should not itself promote the engine to armed")
|
|
}
|
|
}
|
|
|
|
func TestMemoryShedStaysObserveOnlyInExplicitDetect(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
var heap uint64 = 900
|
|
e := New(Options{
|
|
Mode: ModeDetect,
|
|
SoftMemoryLimit: 1000,
|
|
WarnWriter: &buf,
|
|
WarnInterval: time.Hour,
|
|
DisableCoolDown: true,
|
|
MemorySampleFunc: func() uint64 { return heap },
|
|
})
|
|
e.ObserveMemory()
|
|
if !e.Shedding() {
|
|
t.Fatal("should shed at 85%")
|
|
}
|
|
d := e.AdmitPacket("udp0", 1)
|
|
if !d.Allow {
|
|
t.Fatal("explicit detect mode must never block, even under memory shed")
|
|
}
|
|
}
|
|
|
|
func TestPreferKeepLeniencyRecordsTrip(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
clock := time.Unix(1_700_000_000, 0)
|
|
e := New(Options{
|
|
Mode: ModePrevent,
|
|
MaxPPS: 2,
|
|
WarnWriter: &buf,
|
|
WarnInterval: time.Hour,
|
|
DisableAdaptive: true,
|
|
DisableCoolDown: true,
|
|
Now: func() time.Time { return clock },
|
|
})
|
|
opts := AdmitOpts{Class: ClassPreferKeep}
|
|
d1 := e.admitWithOpts("pk0", 1, opts)
|
|
d2 := e.admitWithOpts("pk0", 1, opts)
|
|
d3 := e.admitWithOpts("pk0", 1, opts) // pps=3 over MaxPPS(2) but within the 2x leniency band.
|
|
if !d1.Allow || !d2.Allow || !d3.Allow {
|
|
t.Fatalf("prefer-keep class should ride out bursts under 2x: %#v %#v %#v", d1, d2, d3)
|
|
}
|
|
if e.TripCount(ReasonPPS) == 0 {
|
|
t.Fatal("prefer-keep leniency use must still be visible in trip counters, not silently invisible")
|
|
}
|
|
if !strings.Contains(buf.String(), "trip reason=pps") {
|
|
t.Fatalf("expected a trip warning even while allowed under leniency, got %q", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestTripCoolDownOnlyDoesNotArmIfaceCoolDown(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
clock := time.Unix(1_700_000_000, 0)
|
|
e := New(Options{
|
|
Mode: ModePrevent,
|
|
WarnWriter: &buf,
|
|
WarnInterval: time.Hour,
|
|
Now: func() time.Time { return clock },
|
|
})
|
|
for range CoolDownTripThreshold + 8 {
|
|
d := e.tripCoolDownOnly("pk0", ReasonPPS)
|
|
if !d.Allow {
|
|
t.Fatalf("prefer-keep leniency must stay allowed: %#v", d)
|
|
}
|
|
clock = clock.Add(10 * time.Millisecond)
|
|
}
|
|
if e.InCoolDown("pk0") {
|
|
t.Fatal("leniency band must not arm iface cool-down (that drops resource transfers after a quiet baseline)")
|
|
}
|
|
if e.TripCount(ReasonPPS) == 0 {
|
|
t.Fatal("leniency usage should still be visible in trip counters")
|
|
}
|
|
}
|
|
|
|
func TestPreferKeepOverTwoXCoolsDown(t *testing.T) {
|
|
health.Default.Reset()
|
|
clock := time.Unix(1_700_000_000, 0)
|
|
e := New(Options{
|
|
Mode: ModePrevent,
|
|
MaxPPS: 10,
|
|
WarnWriter: &bytes.Buffer{},
|
|
WarnInterval: time.Hour,
|
|
DisableAdaptive: true,
|
|
Now: func() time.Time { return clock },
|
|
})
|
|
opts := AdmitOpts{Class: ClassPreferKeep}
|
|
blocked := 0
|
|
for range CoolDownTripThreshold + 20 {
|
|
d := e.admitWithOpts("pk1", 1, opts)
|
|
if !d.Allow {
|
|
blocked++
|
|
}
|
|
}
|
|
if blocked == 0 {
|
|
t.Fatal("prefer-keep above 2x the trip line must shed")
|
|
}
|
|
}
|
|
|
|
func TestPeerIsolationShedsOnlyTheFloodingPeer(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
clock := time.Unix(1_700_000_000, 0)
|
|
e := New(Options{
|
|
Mode: ModePrevent,
|
|
MaxPPS: 100,
|
|
WarnWriter: &buf,
|
|
WarnInterval: time.Hour,
|
|
DisableAdaptive: true,
|
|
DisableCoolDown: true,
|
|
Now: func() time.Time { return clock },
|
|
})
|
|
// Peer budget is PeerBudgetFraction (0.5) of the 100 pps interface line,
|
|
// so a single peer sending more than 50 pps should trip its own bucket
|
|
// well before the interface aggregate itself is threatened.
|
|
floodOpts := AdmitOpts{PeerKey: "attacker:1", Class: ClassShedFirst}
|
|
quietOpts := AdmitOpts{PeerKey: "friend:1", Class: ClassPreferKeep}
|
|
|
|
floodBlocked := false
|
|
for range 80 {
|
|
d := e.admitWithOpts("shared0", 1, floodOpts)
|
|
if !d.Allow {
|
|
floodBlocked = true
|
|
}
|
|
}
|
|
if !floodBlocked {
|
|
t.Fatal("expected the flooding peer to eventually be shed by its own sub-bucket")
|
|
}
|
|
|
|
// A different, quiet peer on the same shared interface must be
|
|
// unaffected by the flooding peer's sub-bucket trip.
|
|
d := e.admitWithOpts("shared0", 1, quietOpts)
|
|
if !d.Allow {
|
|
t.Fatalf("quiet peer should not be collaterally shed: %#v", d)
|
|
}
|
|
}
|
|
|
|
func TestPeerIsolationDisabledFallsBackToSharedBudget(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
clock := time.Unix(1_700_000_000, 0)
|
|
e := New(Options{
|
|
Mode: ModePrevent,
|
|
MaxPPS: 5,
|
|
WarnWriter: &buf,
|
|
WarnInterval: time.Hour,
|
|
DisableAdaptive: true,
|
|
DisableCoolDown: true,
|
|
DisablePeerIsolation: true,
|
|
Now: func() time.Time { return clock },
|
|
})
|
|
opts := AdmitOpts{PeerKey: "attacker:1", Class: ClassShedFirst}
|
|
blocked := false
|
|
for range 20 {
|
|
d := e.admitWithOpts("shared0", 1, opts)
|
|
if !d.Allow {
|
|
blocked = true
|
|
}
|
|
}
|
|
if !blocked {
|
|
t.Fatal("expected the shared interface aggregate to still trip when peer isolation is disabled")
|
|
}
|
|
}
|
|
|
|
func TestPeerSubBucketEvictionIsBounded(t *testing.T) {
|
|
health.Default.Reset()
|
|
var buf bytes.Buffer
|
|
clock := time.Unix(1_700_000_000, 0)
|
|
e := New(Options{
|
|
Mode: ModePrevent,
|
|
MaxPPS: 1_000_000,
|
|
WarnWriter: &buf,
|
|
WarnInterval: time.Hour,
|
|
DisableAdaptive: true,
|
|
DisableCoolDown: true,
|
|
Now: func() time.Time { return clock },
|
|
})
|
|
for i := range MaxTrackedPeersPerIface + 50 {
|
|
_ = e.admitWithOpts("shared0", 1, AdmitOpts{PeerKey: fmt.Sprintf("peer-%d", i)})
|
|
}
|
|
e.mu.Lock()
|
|
n := len(e.ifaces["shared0"].peers)
|
|
e.mu.Unlock()
|
|
if n > MaxTrackedPeersPerIface {
|
|
t.Fatalf("peer bucket map grew unbounded: %d entries", n)
|
|
}
|
|
}
|
|
|
|
func TestConfigureFromConfig(t *testing.T) {
|
|
t.Cleanup(func() { SetDefault(nil) })
|
|
e := ConfigureFromConfig("detect", 0, "", nil)
|
|
if e.Mode() != ModeDetect {
|
|
t.Fatalf("mode %v", e.Mode())
|
|
}
|
|
e.StopMemoryMonitor()
|
|
SetDefault(nil)
|
|
}
|
|
|
|
func TestAdmitPacketConcurrent(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
e := New(Options{Mode: ModePrevent, MaxPPS: 1000, WarnWriter: &buf, WarnInterval: time.Hour, DisableCoolDown: true, DisableAdaptive: true})
|
|
var wg sync.WaitGroup
|
|
for range 8 {
|
|
wg.Go(func() {
|
|
for range 200 {
|
|
_ = e.AdmitPacket("udp0", 8)
|
|
}
|
|
})
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestRateWindowBuckets(t *testing.T) {
|
|
var w rateWindow
|
|
now := time.Unix(1_700_000_000, 0)
|
|
for i := range 5 {
|
|
pps, _ := w.add(now, 10)
|
|
if pps < float64(i+1) {
|
|
t.Fatalf("pps=%v after %d", pps, i+1)
|
|
}
|
|
}
|
|
later := now.Add(2 * time.Second)
|
|
pps, bps := w.add(later, 100)
|
|
if pps != 1 || bps != 100 {
|
|
t.Fatalf("after advance pps=%v bps=%v", pps, bps)
|
|
}
|
|
}
|