Files
mrambossekandClaude Opus 5 8118e213ae server: upstream trains, observed TTL/DSCP/ECN, rate limits, action ids
Types 0x03/0x04/0x05 land with a bounded columnar train buffer (head kept,
truncation declared) and grant-free multi-part reports - a report row is
smaller than the packet it answers, so $3.4 holds without a grant. The
read loop now collects TTL/TOS cmsgs on Linux, replacing the 0xFF stubs in
the observation block with what the kernel saw; downtrain gained a dscp
parameter, so DSCP survival is measurable in both directions.

Rate limiting ($2.5) exists now: per-credential AND per-source buckets,
429 on the control plane, silent drop on the data plane after the HMAC
gate and before the replay window. UDP ceilings default above the largest
legitimate run - a limit that clips a real measurement produces a
confidently wrong number.

Every granted packet carries its action_id at payload[8:16]; overlapping
actions were unattributable before. Canary DNS logs now honor the stated
24h privacy default. /admin/enroll-tokens answers the spec's JSON shape.
protocol_version 1.0.1 (additive).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 13:04:54 +02:00

63 lines
1.8 KiB
Go

// SPDX-FileCopyrightText: 2026 Echolot contributors
// SPDX-License-Identifier: GPL-3.0-or-later
package session
import "testing"
func TestTrainBufferKeepsHeadAndDeclaresTruncation(t *testing.T) {
s := &Session{}
over := trainCap + 100
for i := 0; i < over; i++ {
s.RecordTrainPacket(7, TrainEntry{Seq: uint32(i), Size: 64})
}
tr, ok := s.TrainView(7)
if !ok {
t.Fatal("train not found")
}
if tr.Received != over {
t.Fatalf("Received = %d, want %d — the count must include unbuffered packets", tr.Received, over)
}
if len(tr.Entries) != trainCap {
t.Fatalf("buffered %d entries, want the cap %d", len(tr.Entries), trainCap)
}
if !tr.Truncated {
t.Fatal("overflow must be declared, not silent")
}
// The head must survive: it is what a ring buffer would have lost.
if tr.Entries[0].Seq != 0 || tr.Entries[trainCap-1].Seq != trainCap-1 {
t.Fatalf("buffer kept seqs %d..%d, want the head 0..%d",
tr.Entries[0].Seq, tr.Entries[trainCap-1].Seq, trainCap-1)
}
}
func TestTrainEvictionDropsOldestTrain(t *testing.T) {
s := &Session{}
for id := uint32(0); id < maxTrains+2; id++ {
s.RecordTrainPacket(id, TrainEntry{Seq: 1})
}
if _, ok := s.TrainView(0); ok {
t.Fatal("oldest train should have been evicted")
}
if _, ok := s.TrainView(1); ok {
t.Fatal("second-oldest train should have been evicted")
}
if _, ok := s.TrainView(maxTrains + 1); !ok {
t.Fatal("newest train missing")
}
if got := len(s.Trains()); got != maxTrains {
t.Fatalf("holding %d trains, want %d", got, maxTrains)
}
}
func TestTrainViewReturnsACopy(t *testing.T) {
s := &Session{}
s.RecordTrainPacket(3, TrainEntry{Seq: 10})
tr, _ := s.TrainView(3)
tr.Entries[0].Seq = 99
again, _ := s.TrainView(3)
if again.Entries[0].Seq != 10 {
t.Fatal("TrainView leaked the internal slice")
}
}