Files
lneto/tcp/policy_test.go
T
2026-08-24 16:47:06 -03:00

435 lines
13 KiB
Go

package tcp
import (
"math/rand"
"testing"
"github.com/soypat/lneto/ethernet"
)
// recordingPolicy records every hook invocation and lets the test steer what is
// returned to the Handler. It is the [Policy] counterpart driven by the Handler
// under test.
type recordingPolicy struct {
resets int
preRx []Segment
preTx int
postRx []Segment
postTx []txRecord
// Values handed back to the Handler.
keep bool // PreRx result. Default true (see newRecordingPolicy).
rtxFrom Value
retransmit bool
holdNew bool
// writeOpts, when non-empty, is appended as TCP options by PreTx.
writeOpts []byte
}
// txRecord is what PostTx observed on the emitted frame.
type txRecord struct {
seg Segment
offset uint8
sport uint16
dport uint16
}
func newRecordingPolicy() *recordingPolicy { return &recordingPolicy{keep: true} }
var _ Policy = (*recordingPolicy)(nil)
func (p *recordingPolicy) Reset() { p.resets++ }
func (p *recordingPolicy) PreRx(h *Handler, incoming Frame) bool {
p.preRx = append(p.preRx, incoming.Segment(len(incoming.Payload())))
return p.keep
}
func (p *recordingPolicy) PostRx(h *Handler, prevState State, accepted Frame) {
p.postRx = append(p.postRx, accepted.Segment(len(accepted.Payload())))
}
func (p *recordingPolicy) PreTx(h *Handler, outgoingOpts Frame) (Value, bool, bool) {
p.preTx++
if len(p.writeOpts) > 0 {
// Raise the offset first: Options() is sized from it.
words := uint8(5 + (len(p.writeOpts)+3)/4)
outgoingOpts.SetOffsetAndFlags(words, 0)
copy(outgoingOpts.Options(), p.writeOpts)
}
return p.rtxFrom, p.retransmit, p.holdNew
}
func (p *recordingPolicy) PostTx(h *Handler, outgoing Frame) {
offset, _ := outgoing.OffsetAndFlags()
p.postTx = append(p.postTx, txRecord{
seg: outgoing.Segment(len(outgoing.Payload())),
offset: offset,
sport: outgoing.SourcePort(),
dport: outgoing.DestinationPort(),
})
}
// TestPolicy_DisabledByDefault verifies the Handler runs normally with no Policy
// installed: the transmit and receive paths never touch a nil Policy.
func TestPolicy_DisabledByDefault(t *testing.T) {
const mtu = ethernet.MaxMTU
rng := rand.New(rand.NewSource(1))
client, server := newHandler(t, mtu, 3), newHandler(t, mtu, 3)
setupClientServer(t, rng, client, server)
var buf [mtu]byte
establish(t, client, server, buf[:]) // must not panic on nil Policy.
}
// TestPolicy_HooksInvoked verifies the Handler drives the full hook contract
// across a handshake: Reset on open, PreTx+PostTx on transmit, PreRx+PostRx on
// receive.
func TestPolicy_HooksInvoked(t *testing.T) {
const mtu = ethernet.MaxMTU
rng := rand.New(rand.NewSource(2))
client, server := newHandler(t, mtu, 3), newHandler(t, mtu, 3)
pol := newRecordingPolicy()
client.SetPolicy(pol)
setupClientServer(t, rng, client, server) // OpenActive → reset → Reset().
if pol.resets == 0 {
t.Fatal("Reset not called on open")
}
var buf [mtu]byte
establish(t, client, server, buf[:])
if pol.preTx == 0 {
t.Fatal("PreTx never called on transmit")
}
if len(pol.postTx) == 0 {
t.Fatal("PostTx never called on transmit")
}
if pol.preTx < len(pol.postTx) {
t.Fatalf("PreTx calls=%d < PostTx calls=%d: PostTx must never fire without PreTx", pol.preTx, len(pol.postTx))
}
// Client received the SYN-ACK and accepted it.
if len(pol.preRx) == 0 {
t.Fatal("PreRx never called on receive")
}
if len(pol.postRx) == 0 {
t.Fatal("PostRx never called on accepted receive")
}
// PostTx receives the segment actually emitted: the first is the SYN.
if !pol.postTx[0].seg.Flags.HasAny(FlagSYN) {
t.Fatalf("first PostTx segment flags=%s, want SYN", pol.postTx[0].seg.Flags)
}
}
// TestPolicy_PostTxSeesWrittenFrame verifies PostTx observes the fully populated
// frame — ports, sequence numbers and payload length as emitted — and not the
// frame as it stood before the segment was written into it.
func TestPolicy_PostTxSeesWrittenFrame(t *testing.T) {
const mtu = ethernet.MaxMTU
rng := rand.New(rand.NewSource(6))
client, server := newHandler(t, mtu, 3), newHandler(t, mtu, 3)
pol := newRecordingPolicy()
client.SetPolicy(pol)
setupClientServer(t, rng, client, server)
var buf [mtu]byte
establish(t, client, server, buf[:])
data := []byte("payload")
if _, err := client.Write(data); err != nil {
t.Fatal("client write:", err)
}
clear(buf[:])
n, err := client.Send(buf[:])
if err != nil {
t.Fatal("client send:", err)
}
last := pol.postTx[len(pol.postTx)-1]
wantSeg := mustSegment(t, buf[:n], n-int(last.offset)*4)
if last.seg != wantSeg {
t.Fatalf("PostTx segment=%+v, want emitted %+v", last.seg, wantSeg)
}
if int(last.seg.DATALEN) != len(data) {
t.Fatalf("PostTx DATALEN=%d, want %d", last.seg.DATALEN, len(data))
}
if last.sport != client.LocalPort() || last.dport != client.RemotePort() {
t.Fatalf("PostTx ports=%d→%d, want %d→%d", last.sport, last.dport, client.LocalPort(), client.RemotePort())
}
}
// TestPolicy_NoPostTxWithoutSegment verifies a transmit attempt that emits
// nothing still runs PreTx but never PostTx, so a Policy cannot mistake a
// no-op Send for a segment on the wire.
func TestPolicy_NoPostTxWithoutSegment(t *testing.T) {
const mtu = ethernet.MaxMTU
rng := rand.New(rand.NewSource(7))
client, server := newHandler(t, mtu, 3), newHandler(t, mtu, 3)
pol := newRecordingPolicy()
client.SetPolicy(pol)
setupClientServer(t, rng, client, server)
var buf [mtu]byte
establish(t, client, server, buf[:])
preTxBefore, postTxBefore := pol.preTx, len(pol.postTx)
n, err := client.Send(buf[:]) // Nothing queued: no segment.
if err != nil {
t.Fatal("client send:", err)
}
if n != 0 {
t.Fatalf("expected no segment, got %d bytes", n)
}
if pol.preTx != preTxBefore+1 {
t.Fatalf("PreTx calls=%d, want %d: PreTx must run on every attempt", pol.preTx, preTxBefore+1)
}
if len(pol.postTx) != postTxBefore {
t.Fatalf("PostTx calls=%d, want %d: no segment was emitted", len(pol.postTx), postTxBefore)
}
}
// TestPolicy_PreTxOptions verifies options written by PreTx survive to the wire:
// the data offset accounts for them and the payload starts after them.
func TestPolicy_PreTxOptions(t *testing.T) {
const mtu = ethernet.MaxMTU
rng := rand.New(rand.NewSource(8))
client, server := newHandler(t, mtu, 3), newHandler(t, mtu, 3)
pol := newRecordingPolicy()
client.SetPolicy(pol)
setupClientServer(t, rng, client, server)
var buf [mtu]byte
establish(t, client, server, buf[:])
// One 4-byte option word: NOP,NOP,NOP,EOL.
opts := []byte{1, 1, 1, 0}
pol.writeOpts = opts
data := []byte("payload")
if _, err := client.Write(data); err != nil {
t.Fatal("client write:", err)
}
clear(buf[:])
n, err := client.Send(buf[:])
if err != nil {
t.Fatal("client send:", err)
}
frm, err := NewFrame(buf[:n])
if err != nil {
t.Fatal("frame:", err)
}
offset, _ := frm.OffsetAndFlags()
if offset != 6 {
t.Fatalf("data offset=%d, want 6 (header + one option word)", offset)
}
if got := frm.Options(); string(got) != string(opts) {
t.Fatalf("options=%v, want %v", got, opts)
}
if got := frm.Payload(); string(got) != string(data) {
t.Fatalf("payload=%q, want %q: options must not overlap data", got, data)
}
if n != int(offset)*4+len(data) {
t.Fatalf("frame length=%d, want %d", n, int(offset)*4+len(data))
}
}
// TestPolicy_PreRxDropsSegment verifies keep=false drops the segment before the
// state machine sees it: the payload is not buffered, connection state is
// untouched and PostRx never fires.
func TestPolicy_PreRxDropsSegment(t *testing.T) {
const mtu = ethernet.MaxMTU
rng := rand.New(rand.NewSource(4))
client, server := newHandler(t, mtu, 3), newHandler(t, mtu, 3)
pol := newRecordingPolicy()
server.SetPolicy(pol)
setupClientServer(t, rng, client, server)
var buf [mtu]byte
establish(t, client, server, buf[:]) // keep=true so handshake completes.
// Now start dropping everything the server receives.
pol.keep = false
preRxBefore, postRxBefore := len(pol.preRx), len(pol.postRx)
data := []byte("dropme")
if _, err := client.Write(data); err != nil {
t.Fatal("client write:", err)
}
clear(buf[:])
n, err := client.Send(buf[:])
if err != nil {
t.Fatal("client send:", err)
}
if err := server.Recv(buf[:n]); err != nil {
t.Fatalf("dropped segment must return nil, got %v", err)
}
if len(pol.preRx) != preRxBefore+1 {
t.Fatalf("PreRx calls=%d, want %d (segment must reach PreRx)", len(pol.preRx), preRxBefore+1)
}
if len(pol.postRx) != postRxBefore {
t.Fatalf("PostRx calls=%d, want %d: a dropped segment was never accepted", len(pol.postRx), postRxBefore)
}
if server.BufferedInput() != 0 {
t.Fatalf("dropped segment must not be buffered, got %d bytes", server.BufferedInput())
}
if server.State() != StateEstablished {
t.Fatalf("dropped segment must not change state, got %s", server.State())
}
}
// TestPolicy_PreTxRetransmit verifies a PreTx retransmit directive drives
// go-back-N: the Handler rewinds the send sequence and the transmit buffer
// together and re-emits already-sent, unacknowledged data from snd.UNA.
func TestPolicy_PreTxRetransmit(t *testing.T) {
const mtu = ethernet.MaxMTU
rng := rand.New(rand.NewSource(5))
client, server := newHandler(t, mtu, 3), newHandler(t, mtu, 3)
pol := newRecordingPolicy()
client.SetPolicy(pol)
setupClientServer(t, rng, client, server)
var buf [mtu]byte
establish(t, client, server, buf[:])
// Emit one data segment; server never ACKs, so it stays unacknowledged.
data := []byte("payload")
if _, err := client.Write(data); err != nil {
t.Fatal("client write:", err)
}
clear(buf[:])
n, err := client.Send(buf[:])
if err != nil {
t.Fatal("client send data:", err)
}
if n <= sizeHeaderTCP {
t.Fatal("expected data segment")
}
firstSeg := mustSegment(t, buf[:n], n-sizeHeaderTCP)
firstData := append([]byte(nil), buf[sizeHeaderTCP:n]...)
// Direct go-back-N on the next transmit.
pol.rtxFrom, pol.retransmit = client.ControlBlock().SendUNA(), true
clear(buf[:])
n, err = client.Send(buf[:])
if err != nil {
t.Fatal("client send retransmit:", err)
}
if n <= sizeHeaderTCP {
t.Fatal("expected retransmitted data segment")
}
rtSeg := mustSegment(t, buf[:n], n-sizeHeaderTCP)
if rtSeg.SEQ != firstSeg.SEQ {
t.Fatalf("retransmit SEQ=%d, want original UNA SEQ=%d (go-back-N)", rtSeg.SEQ, firstSeg.SEQ)
}
if rtSeg.DATALEN != firstSeg.DATALEN {
t.Fatalf("retransmit DATALEN=%d, want %d", rtSeg.DATALEN, firstSeg.DATALEN)
}
if got := buf[sizeHeaderTCP:n]; string(got) != string(firstData) {
t.Fatalf("retransmit payload=%q, want %q", got, firstData)
}
}
// TestPolicy_PreTxRetransmitOutOfRange verifies an out-of-range rtxFrom is
// refused, leaving the send sequence and transmit buffer untouched.
func TestPolicy_PreTxRetransmitOutOfRange(t *testing.T) {
const mtu = ethernet.MaxMTU
rng := rand.New(rand.NewSource(9))
client, server := newHandler(t, mtu, 3), newHandler(t, mtu, 3)
pol := newRecordingPolicy()
client.SetPolicy(pol)
setupClientServer(t, rng, client, server)
var buf [mtu]byte
establish(t, client, server, buf[:])
if _, err := client.Write([]byte("payload")); err != nil {
t.Fatal("client write:", err)
}
clear(buf[:])
if _, err := client.Send(buf[:]); err != nil {
t.Fatal("client send data:", err)
}
nxtBefore := client.ControlBlock().SendNext()
// Well beyond snd.NXT: must be refused.
pol.rtxFrom, pol.retransmit = nxtBefore+1000, true
clear(buf[:])
if _, err := client.Send(buf[:]); err != nil {
t.Fatal("client send:", err)
}
if got := client.ControlBlock().SendNext(); got != nxtBefore {
t.Fatalf("snd.NXT=%d, want unchanged %d: out-of-range rtxFrom must be refused", got, nxtBefore)
}
}
// TestPolicy_HoldNew verifies holdNew suppresses new data while leaving control
// segments free to go out.
func TestPolicy_HoldNew(t *testing.T) {
const mtu = ethernet.MaxMTU
rng := rand.New(rand.NewSource(10))
client, server := newHandler(t, mtu, 3), newHandler(t, mtu, 3)
pol := newRecordingPolicy()
client.SetPolicy(pol)
setupClientServer(t, rng, client, server)
var buf [mtu]byte
establish(t, client, server, buf[:])
pol.holdNew = true
if _, err := client.Write([]byte("payload")); err != nil {
t.Fatal("client write:", err)
}
clear(buf[:])
n, err := client.Send(buf[:])
if err != nil {
t.Fatal("client send:", err)
}
if n > sizeHeaderTCP {
t.Fatalf("holdNew must suppress new data, got %d payload bytes", n-sizeHeaderTCP)
}
// Releasing the hold lets the same data out.
pol.holdNew = false
clear(buf[:])
n, err = client.Send(buf[:])
if err != nil {
t.Fatal("client send after hold:", err)
}
if n <= sizeHeaderTCP {
t.Fatal("data must flow once holdNew is cleared")
}
}
// TestPolicy_ResetOnReopen verifies Reset fires on every (re)open and on Abort,
// so a single Policy value can be reused across connection reuse.
func TestPolicy_ResetOnReopen(t *testing.T) {
const mtu = ethernet.MaxMTU
client := newHandler(t, mtu, 3)
pol := newRecordingPolicy()
client.SetPolicy(pol)
if err := client.OpenActive(1234, 5678, 0); err != nil {
t.Fatal("open 1:", err)
}
afterOpen := pol.resets
if afterOpen == 0 {
t.Fatal("Reset not called on first open")
}
client.Abort()
if pol.resets <= afterOpen {
t.Fatalf("Reset not called on Abort: resets=%d, want >%d", pol.resets, afterOpen)
}
afterAbort := pol.resets
if err := client.OpenActive(1234, 5678, 0); err != nil {
t.Fatal("open 2:", err)
}
if pol.resets <= afterAbort {
t.Fatalf("Reset not called on reopen: resets=%d, want >%d", pol.resets, afterAbort)
}
}