The grant is the keystone that makes server->client sends safe: created only by an authenticated control-plane action, bound at creation to the session's OBSERVED data-plane source (so it can never be aimed at a third party), and bounded by bytes, average rate and expiry. Sends stop the moment the budget runs out, so a buggy action cannot become a flood. Two granted actions on top of it: - downtrain: N packets at a given size/interval toward the client, with seq + send-timestamp in the payload — downstream loss/reorder/jitter, which an upstream-only train cannot measure. - big_send: one datagram per requested size, echoing the intended size in the payload — downstream MTU / black-hole evidence the client cannot produce for itself (only the far end can emit a large packet toward it). Tests cover the security properties: no grant without a verified destination, client requests clamped to server limits, byte budget stops sending exactly, expiry refuses, and the rate ceiling throttles a burst. Capabilities gain downtrain + big-send. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
95 lines
2.7 KiB
Go
95 lines
2.7 KiB
Go
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package session
|
|
|
|
import (
|
|
"net/netip"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func sessionWithSource(t *testing.T) *Session {
|
|
t.Helper()
|
|
m := NewManager(time.Minute)
|
|
s, _, err := m.New("dev", "cred", netip.MustParseAddr("127.0.0.1"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.NoteDataSource(netip.MustParseAddrPort("127.0.0.1:5000"))
|
|
return s
|
|
}
|
|
|
|
// Without an observed data-plane source there is nowhere verified to send, so no grant may exist.
|
|
func TestNoGrantWithoutObservedSource(t *testing.T) {
|
|
m := NewManager(time.Minute)
|
|
s, _, _ := m.New("dev", "cred", netip.MustParseAddr("127.0.0.1"))
|
|
if g := s.NewGrant("a1", 1000, 1000, DefaultGrantLimits); g != nil {
|
|
t.Fatal("granted a send with no verified destination")
|
|
}
|
|
}
|
|
|
|
func TestGrantIsBoundToObservedSourceAndClamped(t *testing.T) {
|
|
s := sessionWithSource(t)
|
|
g := s.NewGrant("a1", 1<<40, 1<<30, DefaultGrantLimits) // absurd request
|
|
if g == nil {
|
|
t.Fatal("expected a grant")
|
|
}
|
|
if g.Dest != "127.0.0.1:5000" {
|
|
t.Fatalf("grant destination = %s, want the observed source", g.Dest)
|
|
}
|
|
if g.MaxBytes != DefaultGrantLimits.MaxBytes || g.MaxKbps != DefaultGrantLimits.MaxKbps {
|
|
t.Fatalf("client request was not clamped to server limits: %d bytes / %d kbps",
|
|
g.MaxBytes, g.MaxKbps)
|
|
}
|
|
}
|
|
|
|
// The byte budget must actually stop sending — this is the anti-amplification guarantee.
|
|
func TestGrantStopsAtByteBudget(t *testing.T) {
|
|
s := sessionWithSource(t)
|
|
g := s.NewGrant("a1", 1000, 0, DefaultGrantLimits)
|
|
sent := 0
|
|
for i := 0; i < 100; i++ {
|
|
if !g.Allow(100) {
|
|
break
|
|
}
|
|
sent += 100
|
|
}
|
|
if sent != 1000 {
|
|
t.Fatalf("sent %d bytes, want exactly the 1000-byte budget", sent)
|
|
}
|
|
if g.Allow(1) {
|
|
t.Fatal("grant allowed a send after the budget was exhausted")
|
|
}
|
|
if g.Sent() != 1000 {
|
|
t.Fatalf("Sent() = %d, want 1000", g.Sent())
|
|
}
|
|
}
|
|
|
|
func TestExpiredGrantRefuses(t *testing.T) {
|
|
s := sessionWithSource(t)
|
|
g := s.NewGrant("a1", 10_000, 0, GrantLimits{MaxBytes: 10_000, MaxKbps: 1000, MaxHold: time.Millisecond})
|
|
time.Sleep(5 * time.Millisecond)
|
|
if g.Allow(10) {
|
|
t.Fatal("expired grant still allowed a send")
|
|
}
|
|
}
|
|
|
|
// The rate ceiling must throttle a burst that is well inside the byte budget.
|
|
func TestGrantEnforcesRate(t *testing.T) {
|
|
s := sessionWithSource(t)
|
|
// 8 kbps = 1000 bytes/s. A burst far beyond one second's worth must be refused.
|
|
g := s.NewGrant("a1", 1<<20, 8, DefaultGrantLimits)
|
|
time.Sleep(60 * time.Millisecond) // let the rate window open a little
|
|
sent := 0
|
|
for i := 0; i < 1000; i++ {
|
|
if !g.Allow(100) {
|
|
break
|
|
}
|
|
sent += 100
|
|
}
|
|
if sent > 5000 {
|
|
t.Fatalf("rate limit let %d bytes through in ~60ms at 8kbps", sent)
|
|
}
|
|
}
|