mirror of
https://github.com/soypat/lneto.git
synced 2026-08-31 20:09:05 +00:00
435 lines
13 KiB
Go
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)
|
|
}
|
|
}
|