Fuzz fixes (#73)

* remove old fuzz corpus and stick to using PCG for seeding fuzz test

* polish up drain logic and reduce action search space

* improve fuzzing debug prints

* more fuzz fixes also formatter capture printer fixes

* refactor StackSeeded test

* fix challenge ack infinite packet bug

* more expressive challenge satisfy

* remove printing
This commit is contained in:
Pat Whittingslow
2026-04-11 18:01:05 -03:00
committed by GitHub
parent 66a1aec593
commit 54bc52d9d8
17 changed files with 487 additions and 297 deletions
+2
View File
@@ -40,6 +40,8 @@ agents.md
# For local development and testing create `local` directories.
local
*.local
*local.md
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
+63 -12
View File
@@ -14,6 +14,10 @@ const (
retransmitAfterDupacks = 3
// retransmitMaxQueued sets maximum amount of retransmits to queue while receiving dupacks.
retransmitMaxQueued = 2
// maxChallengeRejects is the number of consecutive challenge ACKs sent without
// a successful Recv before aborting. Prevents infinite ACK ping-pong when both
// sides have diverged state (e.g. after packet mutation).
maxChallengeRejects = 8
)
// ControlBlock is a partial Transmission Control Block (TCB) implementation as
@@ -63,9 +67,14 @@ type ControlBlock struct {
// pending is the queue of pending flags to be sent in the next 2 segments.
// On a call to Send the queue is advanced and flags set in the segment are unset.
// The second position of the queue is used for FIN segments.
pending [2]Flags
_state State // leading underscore so field not suggested on top of exported State method when developing.
challengeAck bool
pending [2]Flags
_state State // leading underscore so field not suggested on top of exported State method when developing.
// challengeAcks counts consecutive challenge acks queued on receiving out of window segment.
// challengeAcks signedness indicates whether the challengeAck is pending being sent.
// A negative value of challengeAcks means a challenge ack is pending being sent.
challengeAcks int8
// dupack counts received ACK==snd.UNA && ACK<snd.NXT received. Does not count ack that set UNA.
dupack uint8
// nRetransmit counts number of retransmits sent since last UNA update.
@@ -152,6 +161,20 @@ func (tcb *ControlBlock) MakeDupACK() Segment {
}
}
// MakeChallengeAck returns a challenge ACK segment for the current ControlBlock state
// used to respond to unexpected or ambiguous segments that require the remote peer to confirm
// its connection state. A challenge ACK does not acknowledge new data,
// consume sequence space, or carry a payload.
func (tcb *ControlBlock) MakeChallengeACK() Segment {
return Segment{
SEQ: tcb.snd.NXT, // Current sequence number (no data)
ACK: tcb.rcv.NXT, // Acknowledging expected next byte
Flags: FlagACK, // Pure ACK, no SYN/FIN/RST
WND: tcb.rcv.WND, // Current receive window size
DATALEN: 0, // No payload
}
}
// sendSpace contains Send Sequence Space data. Its sequence numbers correspond to local data.
type sendSpace struct {
ISS Value // initial send sequence number, defined locally on connection start
@@ -215,7 +238,7 @@ func (tcb *ControlBlock) prepareToHandshake(iss Value, wnd Size, newState State)
// HasPending returns true if there is a pending control segment to send. Calls to Send will advance the pending queue.
func (tcb *ControlBlock) HasPending() bool {
return tcb.pending[0] != 0 || tcb.challengeAck || tcb.HasPendingRetransmit()
return tcb.pending[0] != 0 || tcb.pendingChallengeAck() || tcb.HasPendingRetransmit()
}
// HasPending returns true if the control block is pending a retransmit according to simple optmist
@@ -229,10 +252,10 @@ func (tcb *ControlBlock) HasPendingRetransmit() bool {
// It does not modify the ControlBlock state or pending segment queue.
func (tcb *ControlBlock) PendingSegment(payloadLen int) (_ Segment, ok bool) {
pending := tcb.pending[0]
if tcb.challengeAck {
if tcb.pendingChallengeAck() {
// Do not clear challengeAck here: PendingSegment is documented as read-only.
// The flag is consumed in Send when the ACK segment is actually transmitted.
return Segment{SEQ: tcb.snd.NXT, ACK: tcb.rcv.NXT, Flags: FlagACK, WND: tcb.rcv.WND}, true
return tcb.MakeChallengeACK(), true
} else if !pending.HasAny(flagctl) && tcb.HasPendingRetransmit() {
// Optimist Strategy: retransmit oldest data once.
return Segment{SEQ: tcb.snd.UNA, DATALEN: Size(payloadLen), ACK: tcb.rcv.NXT, WND: tcb.rcv.WND, Flags: FlagACK}, true
@@ -305,7 +328,7 @@ func (tcb *ControlBlock) Recv(seg Segment) (err error) {
// RFC 9293 §3.10.7.4: SYN on synchronized connection → challenge ACK.
if seg.Flags.HasAny(FlagSYN) && !tcb._state.IsPreestablished() {
tcb.challengeAck = true
tcb.triggerChallengeAckEmit()
tcb.pending[0] |= FlagACK
return errDropSegment
}
@@ -341,6 +364,7 @@ func (tcb *ControlBlock) Recv(seg Segment) (err error) {
if err != nil {
return err
}
tcb.triggerChallengeAckSatisfied() // Successful Recv — reset challenge counter.
tcb.pending[0] |= pending
if prevNxt != 0 && tcb.snd.NXT != prevNxt && tcb.logenabled(slog.LevelDebug) {
@@ -430,8 +454,8 @@ func (tcb *ControlBlock) Send(seg Segment) error {
tcb.pending[0] |= newPending
// Sending an ACK satisfies any outstanding challenge-ACK obligation.
if tcb.challengeAck && seg.Flags.HasAny(FlagACK) {
tcb.challengeAck = false
if tcb.pendingChallengeAck() && seg.Flags.HasAny(FlagACK) {
tcb.triggerChallengeAckSent()
}
// The segment is valid, we can update TCB state.
@@ -530,8 +554,10 @@ func (tcb *ControlBlock) validateIncomingSegment(seg Segment) (err error) {
switch err {
case errSeqNotInWindow, errLastNotInWindow, errRequireSequential, errZeroWindow:
if !flags.HasAny(FlagRST) {
tcb.challengeAck = true
tcb.pending[0] |= FlagACK
if tcb.tooManyChallengeAcks() {
return net.ErrClosed
}
tcb.triggerChallengeAckEmit()
}
}
return err
@@ -614,7 +640,7 @@ func (tcb *ControlBlock) handleRST(seq Value) error {
}
// Synchronized states: exact SEQ match required; challenge ACK for in-window non-exact.
if seq != tcb.rcv.NXT {
tcb.challengeAck = true
tcb.triggerChallengeAckEmit()
tcb.pending[0] |= FlagACK
return errDropSegment
}
@@ -680,3 +706,28 @@ func (tcb *ControlBlock) Close() (err error) {
}
return err
}
func (tcb *ControlBlock) triggerChallengeAckSatisfied() {
tcb.challengeAcks = 0
}
func (tcb *ControlBlock) triggerChallengeAckEmit() {
if tcb.challengeAcks >= 0 {
// Only increment challenge ack counter if last challenge ack already sent.
tcb.challengeAcks = -tcb.challengeAcks - 1
}
}
func (tcb *ControlBlock) triggerChallengeAckSent() {
if tcb.challengeAcks < 0 {
tcb.challengeAcks = -tcb.challengeAcks // Make positive.
}
}
func (tcb *ControlBlock) pendingChallengeAck() bool {
return tcb.challengeAcks < 0
}
func (tcb *ControlBlock) tooManyChallengeAcks() bool {
if tcb.challengeAcks >= 0 {
return tcb.challengeAcks > maxChallengeRejects
} else {
return tcb.challengeAcks < -maxChallengeRejects
}
}
+53 -2
View File
@@ -187,7 +187,7 @@ func TestPendingSegment_ChallengeACK_Idempotent(t *testing.T) {
var tcb ControlBlock
tcb.HelperInitState(StateEstablished, 100, 101, 1024)
tcb.HelperInitRcv(500, 501, 1024)
tcb.challengeAck = true
tcb.triggerChallengeAckEmit()
seg1, ok1 := tcb.PendingSegment(0)
if !ok1 {
@@ -196,7 +196,7 @@ func TestPendingSegment_ChallengeACK_Idempotent(t *testing.T) {
// PendingSegment must not consume challengeAck (read-only contract).
// Currently FAILS: challengeAck is set to false on the first call.
if !tcb.challengeAck {
if !tcb.pendingChallengeAck() {
t.Error("PendingSegment cleared challengeAck flag; violates documented read-only contract")
}
@@ -375,3 +375,54 @@ func TestPendingSegment_RetransmitAfter3DupACKs(t *testing.T) {
t.Fatalf("nRetransmit after progress = %d; want 0", tcb.nRetransmit)
}
}
// TestACKLoop_MutualOutOfWindow verifies that two TCBs with diverged state
// (simulating post-mutation) don't enter an infinite challenge-ACK ping-pong.
// Each side sees the other's segment as out-of-window → challenge ACK → loop.
func TestACKLoop_MutualOutOfWindow(t *testing.T) {
const wnd Size = 64
// Setup: A.snd.NXT=101, B.rcv.NXT=5000 → A's segments are outside B's window.
// B.snd.NXT=501, A.rcv.NXT=8000 → B's segments are outside A's window.
// Both will reject each other's challenge ACKs forever without a limit.
var tcbA ControlBlock
tcbA.HelperInitState(StateEstablished, 100, 101, wnd)
tcbA.HelperInitRcv(8000, 8001, wnd) // A expects seq from B around 8001
tcbA.snd.UNA = 101
tcbA.snd.WND = wnd
tcbA.snd.WL1 = 8001
tcbA.snd.WL2 = 101
var tcbB ControlBlock
tcbB.HelperInitState(StateEstablished, 500, 501, wnd)
tcbB.HelperInitRcv(5000, 5001, wnd) // B expects seq from A around 5001
tcbB.snd.UNA = 501
tcbB.snd.WND = wnd
tcbB.snd.WL1 = 5001
tcbB.snd.WL2 = 501
// Kick off: A has a pending ACK (simulating normal data exchange trigger).
tcbA.pending[0] = FlagACK
const maxRounds = 50
for round := 0; round < maxRounds; round++ {
segA, okA := tcbA.PendingSegment(0)
if okA {
tcbA.Send(segA)
// A sends seq=101, but B expects [5001, 5001+64) → out of window → challenge ACK
tcbB.Recv(segA)
}
segB, okB := tcbB.PendingSegment(0)
if okB {
tcbB.Send(segB)
// B sends seq=501, but A expects [8001, 8001+64) → out of window → challenge ACK
tcbA.Recv(segB)
}
if !okA && !okB {
return // Converged.
}
}
t.Fatal("ACK ping-pong did not converge after", maxRounds, "rounds — infinite loop bug")
}
+1 -1
View File
@@ -46,7 +46,7 @@ func (tcb *ControlBlock) traceRcv(msg string) {
slog.String("state", tcb._state.String()),
slog.Uint64("rcv.nxt", uint64(tcb.rcv.NXT)),
slog.Uint64("rcv.wnd", uint64(tcb.rcv.WND)),
slog.Bool("challenge", tcb.challengeAck),
slog.Bool("challenge", tcb.pendingChallengeAck()),
)
}
+1 -1
View File
@@ -934,7 +934,7 @@ func TestChallengeACKWithBufferedData(t *testing.T) {
if err == nil {
t.Fatal("expected error from out-of-order segment")
}
if !server.scb.challengeAck {
if !server.scb.pendingChallengeAck() {
t.Fatal("challengeAck flag not set after out-of-order segment")
}
if server.State() != StateEstablished {
+4 -5
View File
@@ -91,15 +91,14 @@ func (stack *CapturePrinter) PrintPacket(prefix string, pkt []byte) {
}
fmtbuf = append(fmtbuf, ' ')
}
fmtbuf = append(fmtbuf, prefix...)
// Ensure minimum width of packet length display for less jitter in log viewline.
prevlen := len(prefix)
fmtbuf = strconv.AppendInt(fmtbuf, int64(len(pkt)), 10)
numLength := len(fmtbuf) - prevlen
appendSpaces := max(0, stack.namespaceminwidth-numLength) + 1 // add single space to separate actual format from packet length.
appendSpaces := max(0, stack.namespaceminwidth-len(prefix)) + 1 // add single space to separate actual format from packet length.
for range appendSpaces {
fmtbuf = append(fmtbuf, ' ')
}
fmtbuf = strconv.AppendInt(fmtbuf, int64(len(pkt)), 10)
fmtbuf = append(fmtbuf, ' ')
fmtbuf, err = stack.pfmt.FormatFrames(fmtbuf, stack.frms, pkt)
}
fmtbuf = append(fmtbuf, '\n')
-4
View File
@@ -1,4 +0,0 @@
go test fuzz v1
int64(36)
int64(-34)
int64(-32)
-4
View File
@@ -1,4 +0,0 @@
go test fuzz v1
int64(692)
int64(-66)
int64(-115)
-4
View File
@@ -1,4 +0,0 @@
go test fuzz v1
int64(28)
int64(2)
int64(3)
-4
View File
@@ -1,4 +0,0 @@
go test fuzz v1
int64(-1)
int64(0)
int64(94)
-4
View File
@@ -1,4 +0,0 @@
go test fuzz v1
int64(-36)
int64(2)
int64(3)
-4
View File
@@ -1,4 +0,0 @@
go test fuzz v1
int64(296)
int64(-16)
int64(-12)
+3
View File
@@ -0,0 +1,3 @@
go test fuzz v1
int64(676827762285163398)
int64(1141027023543727980)
-4
View File
@@ -1,4 +0,0 @@
go test fuzz v1
int64(-774)
int64(-138)
int64(-206)
-4
View File
@@ -1,4 +0,0 @@
go test fuzz v1
int64(12)
int64(-1)
int64(115)
-4
View File
@@ -1,4 +0,0 @@
go test fuzz v1
int64(-53)
int64(0)
int64(94)
+360 -240
View File
@@ -1,7 +1,10 @@
package xnet
import (
"fmt"
"math/rand/v2"
"net/netip"
"os"
"testing"
"github.com/soypat/lneto"
@@ -182,258 +185,375 @@ func fixIPTCPCRCs(pkt []byte) (fixable bool) {
}
func FuzzStackSeeded(f *testing.F) {
f.Add(int64(1), int64(2), int64(3))
var pmut ltesto.PacketMut
f.Fuzz(func(t *testing.T, seed1, seed2, seedAction int64) {
if seed1 == 0 {
seed1++
}
if seed2 == 0 {
seed2++
}
const mtu = 1500
const mfl = mtu + 14 // frame length includes ethernet header
var buf [mfl]byte
var s1, s2 StackAsync
v1, v2 := byte(seed1), byte(seed2)
cfg1 := StackConfig{
Hostname: "s1",
StaticAddress: netip.AddrFrom4([4]byte{1, 0, 0, v1}),
RandSeed: seed1,
MaxActiveTCPPorts: 1,
MaxActiveUDPPorts: 1,
ICMPQueueLimit: 1 + int(v1%4),
MTU: mtu,
HardwareAddress: [6]byte{0x1, 0, 0, 0, 0, v1},
AcceptMulticast: v1%2 == 0,
}
err := s1.Reset(cfg1)
if err != nil {
t.Fatal(err, cfg1)
}
cfg2 := StackConfig{
Hostname: "s2",
StaticAddress: netip.AddrFrom4([4]byte{1, 0, 0, v2}),
RandSeed: seed2,
MaxActiveTCPPorts: 1,
MaxActiveUDPPorts: 1,
ICMPQueueLimit: 1 + int(v2%4),
MTU: mtu,
HardwareAddress: [6]byte{0x2, 0, 0, 0, 0, v2},
AcceptMulticast: v2%2 == 0,
}
err = s2.Reset(cfg2)
if err != nil {
t.Fatal(err, cfg2)
}
const maxActions = 100
const (
actionUDP = iota
actionTCP
actionICMP
actionARP
actionNone
actionLim
)
const (
pingMinPayload = 8
port1 = 8080
port2 = 80
bufsize = 64
)
var udp1, udp2 udp.Conn
var tcp1, tcp2 tcp.Conn
err = tcp1.Configure(tcp.ConnConfig{
RxBuf: make([]byte, bufsize),
TxBuf: make([]byte, bufsize),
TxPacketQueueSize: 1 + int(s1.Prand32())%10,
})
if err != nil {
t.Fatal(err)
}
err = tcp2.Configure(tcp.ConnConfig{
RxBuf: make([]byte, bufsize),
TxBuf: make([]byte, bufsize),
TxPacketQueueSize: 1 + int(s2.Prand32())%10,
})
if err != nil {
t.Fatal(err)
}
err = udp1.Configure(udp.ConnConfig{
RxBuf: make([]byte, bufsize),
TxBuf: make([]byte, bufsize),
RxQueueSize: int(1 + s1.Prand32()%10),
TxQueueSize: int(1 + s1.Prand32()%10),
})
if err != nil {
t.Fatal(err)
}
err = udp2.Configure(udp.ConnConfig{
RxBuf: make([]byte, bufsize),
TxBuf: make([]byte, bufsize),
RxQueueSize: int(1 + s2.Prand32()%10),
TxQueueSize: int(1 + s2.Prand32()%10),
})
if err != nil {
t.Fatal(err)
}
icmpEnabled := false
udpOrder := 0
betsAreOff := false // When a packet is mutated all bets on which error can be returned are off.
for i := 0; i < maxActions; i++ {
action1 := s1.Prand32()
switch action1 % actionLim {
case actionTCP:
state1 := tcp1.State()
state2 := tcp2.State()
if state1 == 0 && state2 == 0 {
err = s1.DialTCP(&tcp1, port1, netip.AddrPortFrom(s2.Addr(), port2))
if err != nil {
t.Fatal(i, err)
}
err = s2.ListenTCP(&tcp2, port2)
if err != nil {
t.Fatal(i, err)
}
} else if state1 == tcp.StateEstablished && state2 == tcp.StateEstablished {
// For now just close after established.
if s1.Prand32()%2 == 0 {
tcp1.Close()
} else {
tcp2.Close()
}
}
case actionUDP:
// Ensure connections open.
if !udp1.IsOpen() {
err = s1.DialUDP(&udp1, port1, netip.AddrPortFrom(s2.Addr(), port2))
if err != nil {
t.Fatal(i, err)
}
}
if !udp2.IsOpen() {
err = s2.DialUDP(&udp2, port2, netip.AddrPortFrom(s1.Addr(), port1))
if err != nil {
t.Fatal(i, err)
}
}
udpOrder++
action := s1.Prand32() % 8
switch action {
case 0:
if udp1.FreeOutput() > 0 {
udp1.Write([]byte{byte(udpOrder)})
}
case 1:
if udp1.BufferedInput() > 0 {
udp1.Read(buf[:])
}
case 2:
if udp2.FreeOutput() > 0 {
udp2.Write([]byte{byte(udpOrder)})
}
case 3:
if udp2.BufferedInput() > 0 {
udp2.Read(buf[:])
}
case 4:
udp1.Close()
case 5:
udp2.Close()
}
case actionICMP:
if !icmpEnabled {
err = s1.EnableICMP(true)
if err != nil {
t.Fatal(i, err)
}
err = s2.EnableICMP(true)
if err != nil {
t.Fatal(i, err)
}
icmpEnabled = true
}
if s1.Prand32()%2 == 0 {
s1.icmp.Reset()
_, err = s1.icmp.PingStart(s2.Addr().As4(), buf[:pingMinPayload], pingMinPayload+uint16(s1.Prand32())%pingMinPayload)
if err != nil {
t.Fatal(i, err)
}
} else {
s2.icmp.Reset()
_, err = s2.icmp.PingStart(s1.Addr().As4(), buf[:pingMinPayload], pingMinPayload+uint16(s2.Prand32())%pingMinPayload)
if err != nil {
t.Fatal(i, err)
}
}
case actionARP:
action := s1.Prand32() % 6
switch action {
case 0: // s1 queries s2 address.
s1.StartResolveHardwareAddress6(s2.Addr())
case 1: // s2 queries s1 address.
s2.StartResolveHardwareAddress6(s1.Addr())
case 2: // s1 checks query result for s2.
s1.ResultResolveHardwareAddress6(s2.Addr())
case 3: // s2 checks query result for s1.
s2.ResultResolveHardwareAddress6(s1.Addr())
case 4: // s1 discards pending query.
s1.DiscardResolveHardwareAddress6(s2.Addr())
case 5: // s2 discards pending query.
s2.DiscardResolveHardwareAddress6(s1.Addr())
}
}
// Exchange data while checking stack does not enter runaway infinite frame send loop.
first, second := &s1, &s2
if s1.Prand32()%2 == 0 {
first, second = second, first
}
// TODO(soypat): add specialized packet mutation by detecting protocol and modifying specific packet fields.
const maxConsecutivePackets = 6
mut := s1.Prand32()
f.Add(int64(1), int64(2))
// Numbers below taken from ANU QRNG https://qrng.anu.edu.au/random-hex/
f.Add(int64(0x5b38810084b73b78), int64(0xfbc7243ac2c4a84))
f.Add(int64(0x78b75e43c6fb1336), int64(0x09f9c425438dd42a))
f.Add(int64(0xf63789e3a0750ed), int64(0xd4d3df265f09358))
f.Add(int64(0x9649343892132dc), int64(0xfd5be085171f904))
for k := 0; k < maxConsecutivePackets; k++ {
n, err := first.EgressEthernet(buf[:])
if err != nil {
t.Fatal(i, k, err)
} else if n > 0 {
if mut&1 == 1 {
pmut.MutateEthernet(buf[:n], int64(s1.Prand32())|int64(s1.Prand32())<<32, int64(s1.Prand32())|int64(s1.Prand32())<<32)
betsAreOff = true
}
err = second.IngressEthernet(buf[:n])
if err != nil && !betsAreOff && err != lneto.ErrPacketDrop && err != lneto.ErrExhausted {
t.Fatal(i, k, err)
}
mut >>= 1
// Set to the values of the fuzz case that is crashing to enable verbose debugging logs.
f.Fuzz(testStackSeeded)
}
// fuzz test printing facilities.
var (
fzppr CapturePrinter
fzpmut ltesto.PacketMut
fzoutput = os.Stdout
)
func init() {
fzppr.Configure(fzoutput, CapturePrinterConfig{
NamespaceWidth: 3,
})
}
const (
printFuzz = false
printSeed1 = 676827762285163398
printSeed2 = 1141027023543727980
)
// Debugging facility.
func TestStackSeeded(t *testing.T) {
testStackSeeded(t, printSeed1, printSeed2)
}
func testStackSeeded(t *testing.T, seed1, seed2 int64) {
if seed1 == 0 {
seed1++
}
if seed2 == 0 {
seed2++
}
verbose := printFuzz && printSeed1 == seed1 && printSeed2 == seed2
const (
actionUDP = iota
actionTCP
actionICMP
actionARP
actionLim
)
const maxActions = 32
const maxConsecutivePackets = 6
var actions [maxActions]struct {
Action int64
Rand int64
Mutation [maxConsecutivePackets]struct {
Seed1, Seed2 int64
MutBits1, MutBits2 int64
IsMut int64
}
}
// Fuzz tests are supposed to be predictable and repeatable.
// We only generate the randomness in one place and in same order of
// rng.Int64 calls. We cannot add new calls into the for loop but we
// can add a new for loops when we need more fields filled in.
// Be wary of invalidating the entire fuzz corpus we have.
{
rng := rand.New(rand.NewPCG(uint64(seed1), uint64(seed2)))
for i := range actions {
// DO NOT ADD CALLS TO rng API IN HERE! Read comment above.
actions[i].Action = rng.Int64() % actionLim
actions[i].Rand = rng.Int64()
for k := range maxConsecutivePackets {
mut := &actions[i].Mutation[k]
mut.IsMut = rng.Int64()
mut.Seed1 = rng.Int64()
mut.Seed2 = rng.Int64()
mut.MutBits1 = rng.Int64()
mut.MutBits2 = rng.Int64()
}
}
}
const mtu = 1500
const mfl = mtu + 14 // frame length includes ethernet header
var buf [mfl]byte
var s1, s2 StackAsync
v1, v2 := byte(seed1), byte(seed2)
cfg1 := StackConfig{
Hostname: "s1",
StaticAddress: netip.AddrFrom4([4]byte{1, 0, 0, v1}),
RandSeed: seed1,
MaxActiveTCPPorts: 1,
MaxActiveUDPPorts: 1,
ICMPQueueLimit: 1 + int(v1%4),
MTU: mtu,
HardwareAddress: [6]byte{0x1, 0, 0, 0, 0, v1},
AcceptMulticast: v1%2 == 0,
}
err := s1.Reset(cfg1)
if err != nil {
t.Fatal(err, cfg1)
}
cfg2 := StackConfig{
Hostname: "s2",
StaticAddress: netip.AddrFrom4([4]byte{1, 0, 0, v2}),
RandSeed: seed2,
MaxActiveTCPPorts: 1,
MaxActiveUDPPorts: 1,
ICMPQueueLimit: 1 + int(v2%4),
MTU: mtu,
HardwareAddress: [6]byte{0x2, 0, 0, 0, 0, v2},
AcceptMulticast: v2%2 == 0,
}
err = s2.Reset(cfg2)
if err != nil {
t.Fatal(err, cfg2)
}
const (
pingMinPayload = 8
port1 = 8080
port2 = 80
bufsize = 64
)
var udp1, udp2 udp.Conn
var tcp1, tcp2 tcp.Conn
err = tcp1.Configure(tcp.ConnConfig{
RxBuf: make([]byte, bufsize),
TxBuf: make([]byte, bufsize),
TxPacketQueueSize: 1 + int(uint16(seed1)%10),
})
if err != nil {
t.Fatal(err)
}
err = tcp2.Configure(tcp.ConnConfig{
RxBuf: make([]byte, bufsize),
TxBuf: make([]byte, bufsize),
TxPacketQueueSize: 1 + int(uint16(seed2)%10),
})
if err != nil {
t.Fatal(err)
}
err = udp1.Configure(udp.ConnConfig{
RxBuf: make([]byte, bufsize),
TxBuf: make([]byte, bufsize),
RxQueueSize: int(1 + uint16(seed1>>32)%10),
TxQueueSize: int(1 + uint16(seed1>>32)%10),
})
if err != nil {
t.Fatal(err)
}
err = udp2.Configure(udp.ConnConfig{
RxBuf: make([]byte, bufsize),
TxBuf: make([]byte, bufsize),
RxQueueSize: int(1 + uint16(seed2>>32)%10),
TxQueueSize: int(1 + uint16(seed2>>32)%10),
})
if err != nil {
t.Fatal(err)
}
icmpEnabled := false
udpOrder := 0
betsAreOff := false // When a packet is mutated all bets on which error can be returned are off.
for i, action := range actions {
switch action.Action {
case actionTCP:
state1 := tcp1.State()
state2 := tcp2.State()
if state1 == 0 && state2 == 0 {
if verbose {
fmt.Fprintln(fzoutput, "TCP dial")
}
n, err = second.EgressEthernet(buf[:])
err = s1.DialTCP(&tcp1, port1, netip.AddrPortFrom(s2.Addr(), port2))
if err != nil {
t.Fatal(i, k, err)
} else if n > 0 {
if mut&1 == 1 {
pmut.MutateEthernet(buf[:n], int64(s1.Prand32())|int64(s1.Prand32())<<32, int64(s1.Prand32())|int64(s1.Prand32())<<32)
betsAreOff = true
}
mut >>= 1
err = first.IngressEthernet(buf[:n])
if err != nil && !betsAreOff && err != lneto.ErrPacketDrop && err != lneto.ErrExhausted {
t.Fatal(i, k, err)
}
t.Fatal(i, err)
}
err = s2.ListenTCP(&tcp2, port2)
if err != nil {
t.Fatal(i, err)
}
} else if state1 == tcp.StateEstablished && state2 == tcp.StateEstablished {
// For now just close after established.
closeNum := 1 + action.Rand%2
if verbose {
fmt.Fprintln(fzoutput, "TCP close", closeNum)
}
switch closeNum {
case 1:
tcp1.Close()
case 2:
tcp2.Close()
}
}
case actionUDP:
// Ensure connections open.
if !udp1.IsOpen() {
if verbose {
fmt.Fprintln(fzoutput, "UDP dial 1")
}
err = s1.DialUDP(&udp1, port1, netip.AddrPortFrom(s2.Addr(), port2))
if err != nil {
t.Fatal(i, err)
}
}
if !udp2.IsOpen() {
if verbose {
fmt.Fprintln(fzoutput, "UDP dial 2")
}
err = s2.DialUDP(&udp2, port2, netip.AddrPortFrom(s1.Addr(), port1))
if err != nil {
t.Fatal(i, err)
}
}
udpOrder++
action := action.Rand % 8
if verbose {
fmt.Fprintln(fzoutput, "UDP action", action)
}
switch action {
case 0:
if udp1.FreeOutput() > 0 {
udp1.Write([]byte{byte(udpOrder)})
}
case 1:
if udp1.BufferedInput() > 0 {
udp1.Read(buf[:])
}
case 2:
if udp2.FreeOutput() > 0 {
udp2.Write([]byte{byte(udpOrder)})
}
case 3:
if udp2.BufferedInput() > 0 {
udp2.Read(buf[:])
}
case 4:
udp1.Close()
case 5:
udp2.Close()
}
case actionICMP:
icmpaction := action.Rand % 2
if verbose {
fmt.Fprintln(fzoutput, "ICMP action", icmpaction, "enabled", icmpEnabled)
}
if !icmpEnabled {
err = s1.EnableICMP(true)
if err != nil {
t.Fatal(i, err)
}
err = s2.EnableICMP(true)
if err != nil {
t.Fatal(i, err)
}
icmpEnabled = true
}
switch icmpaction {
case 0:
s1.icmp.Reset()
_, err = s1.icmp.PingStart(s2.Addr().As4(), buf[:pingMinPayload], pingMinPayload+uint16(action.Rand)%pingMinPayload)
if err != nil {
t.Fatal(i, err)
}
case 1:
s2.icmp.Reset()
_, err = s2.icmp.PingStart(s1.Addr().As4(), buf[:pingMinPayload], pingMinPayload+uint16(action.Rand)%pingMinPayload)
if err != nil {
t.Fatal(i, err)
}
}
case actionARP:
action := action.Rand % 6
if verbose {
fmt.Fprintln(fzoutput, "ARP action", action)
}
switch action {
case 0: // s1 queries s2 address.
s1.StartResolveHardwareAddress6(s2.Addr())
case 1: // s2 queries s1 address.
s2.StartResolveHardwareAddress6(s1.Addr())
case 2: // s1 checks query result for s2.
s1.ResultResolveHardwareAddress6(s2.Addr())
case 3: // s2 checks query result for s1.
s2.ResultResolveHardwareAddress6(s1.Addr())
case 4: // s1 discards pending query.
s1.DiscardResolveHardwareAddress6(s2.Addr())
case 5: // s2 discards pending query.
s2.DiscardResolveHardwareAddress6(s1.Addr())
}
}
// Exchange data while checking stack does not enter runaway infinite frame send loop.
first, second := &s1, &s2
if (action.Rand>>32)%2 == 0 {
first, second = second, first
}
// TODO(soypat): add specialized packet mutation by detecting protocol and modifying specific packet fields.
for k, mut := range action.Mutation {
n, err := first.EgressEthernet(buf[:])
if err != nil {
t.Fatal(i, "expected no errors after maxconsecutive", err)
t.Fatal(i, k, err)
} else if n > 0 {
t.Fatal(i, "expected no more data after max consecutive")
if mut.IsMut&1 != 0 {
if verbose {
fmt.Fprintln(fzoutput, "mutate tx", first.Hostname())
}
fzpmut.MutateEthernet(buf[:n], mut.Seed1, mut.MutBits1)
betsAreOff = true
}
if verbose {
fzppr.PrintPacket(first.Hostname(), buf[:n])
}
err = second.IngressEthernet(buf[:n])
if err != nil && !betsAreOff && err != lneto.ErrPacketDrop && err != lneto.ErrExhausted {
t.Fatal(i, k, err)
} else if verbose && err != nil {
fmt.Fprintln(fzoutput, "err rx", second.Hostname(), err.Error())
}
}
n, err = second.EgressEthernet(buf[:])
if err != nil {
t.Fatal(i, "expected no errors after maxconsecutive", err)
t.Fatal(i, k, err)
} else if n > 0 {
t.Fatal(i, "expected no more data after max consecutive")
if mut.IsMut&1 != 0 {
if verbose {
fmt.Fprintln(fzoutput, "mutate tx", second.Hostname())
}
fzpmut.MutateEthernet(buf[:n], mut.Seed2, mut.MutBits2)
betsAreOff = true
}
if verbose {
fzppr.PrintPacket(second.Hostname(), buf[:n])
}
err = first.IngressEthernet(buf[:n])
if err != nil && !betsAreOff && err != lneto.ErrPacketDrop && err != lneto.ErrExhausted {
t.Fatal(i, k, err)
} else if verbose && err != nil {
fmt.Fprintln(fzoutput, "err rx", first.Hostname(), err.Error())
}
}
}
})
// Drain any remaining packets (retransmits from mutation).
// Hard ceiling prevents infinite send loops from passing silently.
// Also send drained packet to other stack to also catch infinite feedback loops.
const drainLimit = 8
for d := 0; d < drainLimit; d++ {
limit := d == drainLimit-1
n, err := first.EgressEthernet(buf[:])
if (err != nil || n > 0) && limit {
fzppr.PrintPacket(first.Hostname(), buf[:n])
t.Fatal(i, "stuck in data/error loop:", err)
} else if n > 0 {
if verbose {
fzppr.PrintPacket(first.Hostname(), buf[:n])
}
second.IngressEthernet(buf[:n])
}
n, err = second.EgressEthernet(buf[:])
if (err != nil || n > 0) && limit {
fzppr.PrintPacket("(2) ", buf[:n])
t.Fatal(i, "stuck in data/error loop:", err)
} else if n > 0 {
if verbose {
fzppr.PrintPacket(second.Hostname(), buf[:n])
}
first.IngressEthernet(buf[:n])
}
}
}
}