diff --git a/.gitignore b/.gitignore index 7c7feca..230204e 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/tcp/control.go b/tcp/control.go index 9c6d6a8..c316dde 100644 --- a/tcp/control.go +++ b/tcp/control.go @@ -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= 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 + } +} diff --git a/tcp/control_test.go b/tcp/control_test.go index a8c0813..dec52e5 100644 --- a/tcp/control_test.go +++ b/tcp/control_test.go @@ -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") +} diff --git a/tcp/debug.go b/tcp/debug.go index 962b4c0..4d9c618 100644 --- a/tcp/debug.go +++ b/tcp/debug.go @@ -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()), ) } diff --git a/tcp/handler_test.go b/tcp/handler_test.go index 1ef927f..6ddd0b3 100644 --- a/tcp/handler_test.go +++ b/tcp/handler_test.go @@ -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 { diff --git a/x/xnet/pcap-printer.go b/x/xnet/pcap-printer.go index 29a5912..5517782 100644 --- a/x/xnet/pcap-printer.go +++ b/x/xnet/pcap-printer.go @@ -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') diff --git a/x/xnet/testdata/fuzz/FuzzStackSeeded/1ec6190fe833c850 b/x/xnet/testdata/fuzz/FuzzStackSeeded/1ec6190fe833c850 deleted file mode 100644 index 2af52e5..0000000 --- a/x/xnet/testdata/fuzz/FuzzStackSeeded/1ec6190fe833c850 +++ /dev/null @@ -1,4 +0,0 @@ -go test fuzz v1 -int64(36) -int64(-34) -int64(-32) diff --git a/x/xnet/testdata/fuzz/FuzzStackSeeded/22d285196933f231 b/x/xnet/testdata/fuzz/FuzzStackSeeded/22d285196933f231 deleted file mode 100644 index b8cfe74..0000000 --- a/x/xnet/testdata/fuzz/FuzzStackSeeded/22d285196933f231 +++ /dev/null @@ -1,4 +0,0 @@ -go test fuzz v1 -int64(692) -int64(-66) -int64(-115) diff --git a/x/xnet/testdata/fuzz/FuzzStackSeeded/49001aa89de31c64 b/x/xnet/testdata/fuzz/FuzzStackSeeded/49001aa89de31c64 deleted file mode 100644 index 463c57c..0000000 --- a/x/xnet/testdata/fuzz/FuzzStackSeeded/49001aa89de31c64 +++ /dev/null @@ -1,4 +0,0 @@ -go test fuzz v1 -int64(28) -int64(2) -int64(3) diff --git a/x/xnet/testdata/fuzz/FuzzStackSeeded/712619020c8b32bf b/x/xnet/testdata/fuzz/FuzzStackSeeded/712619020c8b32bf deleted file mode 100644 index f3bff73..0000000 --- a/x/xnet/testdata/fuzz/FuzzStackSeeded/712619020c8b32bf +++ /dev/null @@ -1,4 +0,0 @@ -go test fuzz v1 -int64(-1) -int64(0) -int64(94) diff --git a/x/xnet/testdata/fuzz/FuzzStackSeeded/c00a793bc91dac1e b/x/xnet/testdata/fuzz/FuzzStackSeeded/c00a793bc91dac1e deleted file mode 100644 index 15a734e..0000000 --- a/x/xnet/testdata/fuzz/FuzzStackSeeded/c00a793bc91dac1e +++ /dev/null @@ -1,4 +0,0 @@ -go test fuzz v1 -int64(-36) -int64(2) -int64(3) diff --git a/x/xnet/testdata/fuzz/FuzzStackSeeded/cacdc4b2826c9060 b/x/xnet/testdata/fuzz/FuzzStackSeeded/cacdc4b2826c9060 deleted file mode 100644 index 0db6a0b..0000000 --- a/x/xnet/testdata/fuzz/FuzzStackSeeded/cacdc4b2826c9060 +++ /dev/null @@ -1,4 +0,0 @@ -go test fuzz v1 -int64(296) -int64(-16) -int64(-12) diff --git a/x/xnet/testdata/fuzz/FuzzStackSeeded/d34b33e85cb03829 b/x/xnet/testdata/fuzz/FuzzStackSeeded/d34b33e85cb03829 new file mode 100644 index 0000000..d918c9a --- /dev/null +++ b/x/xnet/testdata/fuzz/FuzzStackSeeded/d34b33e85cb03829 @@ -0,0 +1,3 @@ +go test fuzz v1 +int64(676827762285163398) +int64(1141027023543727980) diff --git a/x/xnet/testdata/fuzz/FuzzStackSeeded/db37a1ff29a9f7a7 b/x/xnet/testdata/fuzz/FuzzStackSeeded/db37a1ff29a9f7a7 deleted file mode 100644 index 49db0bf..0000000 --- a/x/xnet/testdata/fuzz/FuzzStackSeeded/db37a1ff29a9f7a7 +++ /dev/null @@ -1,4 +0,0 @@ -go test fuzz v1 -int64(-774) -int64(-138) -int64(-206) diff --git a/x/xnet/testdata/fuzz/FuzzStackSeeded/de928dab3ccde888 b/x/xnet/testdata/fuzz/FuzzStackSeeded/de928dab3ccde888 deleted file mode 100644 index 9cc9abe..0000000 --- a/x/xnet/testdata/fuzz/FuzzStackSeeded/de928dab3ccde888 +++ /dev/null @@ -1,4 +0,0 @@ -go test fuzz v1 -int64(12) -int64(-1) -int64(115) diff --git a/x/xnet/testdata/fuzz/FuzzStackSeeded/f9c4f8ae6e04a93c b/x/xnet/testdata/fuzz/FuzzStackSeeded/f9c4f8ae6e04a93c deleted file mode 100644 index c6ae437..0000000 --- a/x/xnet/testdata/fuzz/FuzzStackSeeded/f9c4f8ae6e04a93c +++ /dev/null @@ -1,4 +0,0 @@ -go test fuzz v1 -int64(-53) -int64(0) -int64(94) diff --git a/x/xnet/xnet_fuzz_test.go b/x/xnet/xnet_fuzz_test.go index 08cb676..97a7770 100644 --- a/x/xnet/xnet_fuzz_test.go +++ b/x/xnet/xnet_fuzz_test.go @@ -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]) + } + } + } }