Tcp rst handling (#40)

* claude suggests a way forward

* add timing to capture printer

* add pcap.Flags

* fix ICMP CRC calculation and add test

* bugfix: still send data on half-close state(close-wait)

* fix pcap test
This commit is contained in:
Pat Whittingslow
2026-02-23 13:50:13 +01:00
committed by GitHub
parent 08423d0dba
commit 7d323aae19
10 changed files with 386 additions and 46 deletions
+68
View File
@@ -8,6 +8,7 @@ import (
"github.com/soypat/lneto"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/ipv4"
"github.com/soypat/lneto/ipv4/icmpv4"
"github.com/soypat/lneto/tcp"
)
@@ -160,6 +161,73 @@ func (gen *PacketGen) AppendRandomIPv4TCPPacket(dst []byte, rng *rand.Rand, seg
return dst
}
// ICMPEchoConfig configures an ICMP echo request packet.
type ICMPEchoConfig struct {
Identifier uint16
SequenceNumber uint16
Payload []byte
}
// AppendIPv4ICMPEcho builds and appends a complete Ethernet+IPv4+ICMP echo request packet to dst.
// The packet has valid Ethernet, IPv4, and ICMP checksums.
func (gen *PacketGen) AppendIPv4ICMPEcho(dst []byte, cfg ICMPEchoConfig) []byte {
const icmpHdrLen = 8
ethsize := sizeHeaderEthNoVLAN
if gen.EnableVLAN {
ethsize += 4
}
totalPayload := icmpHdrLen + len(cfg.Payload)
off := len(dst)
dst = append(dst, make([]byte, ethsize+sizeHeaderIPv4+totalPayload)...)
pkt := dst[off:]
// Ethernet header.
efrm, err := ethernet.NewFrame(pkt)
if err != nil {
panic(err)
}
*efrm.DestinationHardwareAddr() = gen.DstMAC
*efrm.SourceHardwareAddr() = gen.SrcMAC
efrm.SetEtherType(ethernet.TypeIPv4)
// IPv4 header.
ethernetPayload := efrm.Payload()
ifrm, err := ipv4.NewFrame(ethernetPayload)
if err != nil {
panic(err)
}
ifrm.SetVersionAndIHL(4, 5)
ifrm.SetTotalLength(uint16(sizeHeaderIPv4 + totalPayload))
ifrm.SetID(0)
ifrm.SetFlags(0)
ifrm.SetTTL(64)
ifrm.SetProtocol(lneto.IPProtoICMP)
*ifrm.SourceAddr() = gen.SrcIPv4
*ifrm.DestinationAddr() = gen.DstIPv4
ifrm.SetCRC(0)
ifrm.SetCRC(ifrm.CalculateHeaderCRC())
// ICMP echo request.
icmpData := ifrm.Payload()
icmpFrm, err := icmpv4.NewFrame(icmpData)
if err != nil {
panic(err)
}
icmpFrm.SetType(icmpv4.TypeEcho)
icmpFrm.SetCode(0)
echo := icmpv4.FrameEcho{Frame: icmpFrm}
echo.SetIdentifier(cfg.Identifier)
echo.SetSequenceNumber(cfg.SequenceNumber)
copy(echo.Data(), cfg.Payload)
// ICMP checksum covers the entire ICMP message (no pseudo header).
icmpFrm.SetCRC(0)
var crc lneto.CRC791
icmpFrm.SetCRC(crc.PayloadSum16(icmpData[:totalPayload]))
return dst
}
func sizeWord(l int) uint8 {
return uint8((l + 3) / 4)
}
+21 -14
View File
@@ -603,10 +603,21 @@ type FrameField struct {
FrameBitOffset int
BitLength int
SubFields []FrameField
RightAligned bool
Legacy bool
Flags Flags
}
// Flags stores frame field interpretation bits.
type Flags uint32
const (
FlagRightAligned Flags = 1 << iota
FlagLegacy
)
func (ff Flags) IsLegacy() bool { return ff&FlagLegacy != 0 }
func (ff Flags) IsRightAligned() bool { return ff&FlagRightAligned != 0 }
type Frame struct {
Protocol any
Fields []FrameField
@@ -653,7 +664,7 @@ func (frm Frame) FieldAsUint(fieldIdx int, pkt []byte) (uint64, error) {
return badUint64, errors.New("invalid field index")
}
field := frm.Fields[fieldIdx]
return fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.RightAligned)
return fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.Flags.IsRightAligned())
}
// AppendField appends the binary on-the-wire representation of the field and aligns the field so it starts at the first bit of appended data.
@@ -662,7 +673,7 @@ func (frm Frame) AppendField(dst []byte, fieldIdx int, pkt []byte) ([]byte, erro
return dst, errors.New("invalid field index")
}
field := frm.Fields[fieldIdx]
return appendField(dst, pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.RightAligned)
return appendField(dst, pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.Flags.IsRightAligned())
}
func fieldAsUint(pkt []byte, fieldBitStart, bitlen int, rightAligned bool) (uint64, error) {
@@ -860,14 +871,14 @@ var baseIPv6Fields = [...]FrameField{
Class: FieldClassFlags,
FrameBitOffset: 4,
BitLength: 1 * octet,
RightAligned: true,
Flags: FlagRightAligned,
},
{
Name: "Flow Label",
Class: FieldClassID,
FrameBitOffset: 12,
BitLength: 20,
RightAligned: true,
Flags: FlagRightAligned,
},
{
Name: "Total Length",
@@ -928,11 +939,6 @@ var baseIPv4Fields = [...]FrameField{
FrameBitOffset: 4 * octet,
BitLength: 2 * octet,
},
{
Class: FieldClassID,
FrameBitOffset: 4 * octet,
BitLength: 2 * octet,
},
{
Class: FieldClassFlags,
FrameBitOffset: 6 * octet,
@@ -1000,7 +1006,7 @@ var baseTCPFields = [...]FrameField{
Class: FieldClassFlags,
FrameBitOffset: 12*octet + 4,
BitLength: 12,
RightAligned: true,
Flags: FlagRightAligned,
},
{
Name: "Window",
@@ -1018,6 +1024,7 @@ var baseTCPFields = [...]FrameField{
Class: 0,
FrameBitOffset: 18 * octet,
BitLength: 2 * octet,
Flags: FlagLegacy,
},
}
@@ -1199,14 +1206,14 @@ var baseDHCPv4Fields = [...]FrameField{
Class: FieldClassBinaryText,
FrameBitOffset: (28 + 6) * octet, // Part of Client Hardware Address(16 bytes) but unused.
BitLength: 10 * octet,
Legacy: true,
Flags: FlagLegacy,
},
{
Name: "BOOTP",
Class: FieldClassBinaryText,
FrameBitOffset: (28 + 16) * octet,
BitLength: (dhcpv4.OptionsOffset - (28 + 16)) * octet,
Legacy: true,
Flags: FlagLegacy,
},
}
+7 -7
View File
@@ -474,15 +474,15 @@ func ExampleFormatter_dhcp() {
fmt.Println(string(out))
// Output:
// Ethernet len=14; destination=ff:ff:ff:ff:ff:ff; source=de:ad:be:ef:ca:fe; protocol=0x0800
// IPv4 len=20; version=0x04; (Header Length)=5; (Type of Service)=0x00; (Total Length)=312; identification=0x6043; identification=0x6043; flags=0x4000; (Time to live)=0x40; protocol=0x11; checksum=0xd972; source=0.0.0.0; destination=255.255.255.255
// IPv4 len=20; version=0x04; (Header Length)=5; (Type of Service)=0x00; (Total Length)=312; identification=0x6043; flags=0x4000; (Time to live)=0x40; protocol=0x11; checksum=0xd972; source=0.0.0.0; destination=255.255.255.255
// UDP [RFC768] len=8; (Source port)=68; (Destination port)=67; size=292; checksum=0x0000
// DHCPv4 len=240; op=1; (Hardware Address Type)=0x01; (Hardware Address Length)=6; Hops=0x00; (Transaction ID)=0xdeadbeef; (Start Time)=0x0001; Flags=0x0000; (Client Address)=0.0.0.0; (Offered Address)=0.0.0.0; (Server Next Address)=255.255.255.255; (Relay Agent Address)=0.0.0.0; (Client Hardware Address)=de:ad:be:ef:ca:fe; options
// (DHCP message type.)=1
// (Parameter request list)=0x0102031a1c060f2a
// (DHCP maximum message size)=558
// (Requested IP address)=192.168.1.100
// (Client identifier)="lneto-test"
// (Hostname string)="myhost"
// (DHCP message type.)=1
// (Parameter request list)=0x0102031a1c060f2a
// (DHCP maximum message size)=558
// (Requested IP address)=192.168.1.100
// (Client identifier)="lneto-test"
// (Hostname string)="myhost"
}
func writeOpt(dst []byte, opt dhcpv4.OptNum, data ...byte) int {
dst[0] = byte(opt)
+4 -4
View File
@@ -68,7 +68,7 @@ func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, er
if field.Class == FieldClassFlags && frm.Protocol == lneto.IPProtoTCP {
// TCP flags pretty print special case.
dst = append(dst, "flags="...)
v, err := fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.RightAligned)
v, err := fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.Flags.IsRightAligned())
if err != nil {
return dst, err
}
@@ -95,7 +95,7 @@ func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, er
func (f *Formatter) filterField(field FrameField) bool {
return f.FilterClasses != nil && !slices.Contains(f.FilterClasses, field.Class) ||
(field.Legacy && !f.DisableLegacyFilter)
(field.Flags.IsLegacy() && !f.DisableLegacyFilter)
}
func (f *Formatter) FormatField(dst []byte, pktStartOff int, field FrameField, pkt []byte) (_ []byte, err error) {
@@ -133,7 +133,7 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
dst = append(dst, '=')
f.mubuf.Lock()
defer f.mubuf.Unlock()
f.buf, err = appendField(f.buf[:0], pkt, field.FrameBitOffset+pktStartOff, field.BitLength, field.RightAligned)
f.buf, err = appendField(f.buf[:0], pkt, field.FrameBitOffset+pktStartOff, field.BitLength, field.Flags.IsRightAligned())
if err != nil {
return dst, err
}
@@ -158,7 +158,7 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
case FieldClassDst, FieldClassSrc, FieldClassSize, FieldClassAddress, FieldClassOperation:
// IP, MAC addresses and ports.
if field.BitLength <= 16 {
v, err := fieldAsUint(pkt, fieldBitStart, field.BitLength, field.RightAligned)
v, err := fieldAsUint(pkt, fieldBitStart, field.BitLength, field.Flags.IsRightAligned())
if err != nil {
return dst, err
}
+3 -4
View File
@@ -98,7 +98,7 @@ func (sb *StackIP) Demux(carrierData []byte, offset int) error {
totalLen := ifrm.TotalLength()
proto := ifrm.Protocol()
if proto == lneto.IPProtoICMP {
return sb.recvicmp(ifrm.RawData(), ifrm.HeaderLength())
return sb.recvicmp(ifrm.Payload())
}
node := sb.handlers.nodeByProto(uint16(proto))
// nodeIdx := getNodeByProto(sb.handlers, uint16(proto))
@@ -205,10 +205,9 @@ func (sb *StackIP) Register(h StackNode) error {
return sb.handlers.registerByPortProto(nodeFromStackNode(h, h.LocalPort(), proto, nil))
}
func (sb *StackIP) recvicmp(carrierData []byte, offset int) error {
frameData := carrierData[offset:]
func (sb *StackIP) recvicmp(icmpData []byte) error {
var crc lneto.CRC791
if crc.PayloadSum16(frameData) != 0 {
if crc.PayloadSum16(icmpData) != 0 {
return errors.New("ICMP CRC mismatch")
}
return nil
+16 -14
View File
@@ -183,8 +183,9 @@ func (tcb *ControlBlock) PendingSegment(payloadLen int) (_ Segment, ok bool) {
}
pending := tcb.pending[0]
established := tcb._state == StateEstablished
if !established && tcb._state != StateCloseWait {
payloadLen = 0 // Can't send data if not established.
canSendData := established || tcb._state == StateCloseWait
if !canSendData {
payloadLen = 0 // Can't send data if not established or close-wait.
}
if pending == 0 && payloadLen == 0 {
return Segment{}, false // No pending segment.
@@ -206,7 +207,7 @@ func (tcb *ControlBlock) PendingSegment(payloadLen int) (_ Segment, ok bool) {
pending |= FlagPSH // By default ensure all data flushed to destination application immediately on receive.
}
if established {
if canSendData {
pending |= FlagACK // ACK is always set in established state. Not in RFC9293 but somehow expected?
} else {
payloadLen = 0 // Can't send data if not established.
@@ -420,7 +421,7 @@ func (tcb *ControlBlock) validateIncomingSegment(seg Segment) (err error) {
case checkSEQ && !seg.Last().InWindow(tcb.rcv.NXT, tcb.rcv.WND) && !zeroWindowOK:
err = errLastNotInWindow
case checkSEQ && seg.SEQ != tcb.rcv.NXT:
case checkSEQ && !flags.HasAny(FlagRST) && seg.SEQ != tcb.rcv.NXT:
// This part diverts from TCB as described in RFC 9293. We want to support
// only sequential segments to keep implementation simple and maintainable. See SHLD-31.
err = errRequireSequential
@@ -485,22 +486,23 @@ func (tcb *ControlBlock) resetRcv(localWND Size, remoteISS Value) {
func (tcb *ControlBlock) handleRST(seq Value) error {
tcb.debug("rcv:RST", slog.String("state", tcb._state.String()))
if seq != tcb.rcv.NXT {
// See RFC9293: If the RST bit is set and the sequence number does not exactly match the next expected sequence value, yet is within the current receive window, TCP endpoints MUST send an acknowledgment (challenge ACK).
tcb.challengeAck = true
tcb.pending[0] |= FlagACK
return errDropSegment
}
if tcb._state.IsPreestablished() {
// RFC 9293 §3.5.3: non-synchronized states accept RST if SEQ is in window.
// No challenge ACK for non-synchronized states. Return to LISTEN.
tcb.pending[0] = 0
tcb._state = StateListen
tcb.resetSnd(tcb.snd.ISS+tcb.rstJump(), tcb.snd.WND)
tcb.resetRcv(tcb.rcv.WND, 3_14159_2653^tcb.rcv.IRS)
} else {
tcb.Abort() // Enter closed state and return.
return net.ErrClosed
return errDropSegment
}
return errDropSegment
// Synchronized states: exact match required, challenge ACK for in-window non-exact.
if seq != tcb.rcv.NXT {
tcb.challengeAck = true
tcb.pending[0] |= FlagACK
return errDropSegment
}
tcb.Abort()
return net.ErrClosed
}
func (tcb *ControlBlock) rstJump() Value {
+152
View File
@@ -468,6 +468,158 @@ func TestWindowUpdateSWSAvoidance(t *testing.T) {
}
}
// TestWriteAfterRemoteFIN verifies that when a remote peer sends FIN (entering
// CLOSE_WAIT on our side), we can still write and send data before closing.
// This is a regression test for a panic in sentlist.AddPacket caused by
// PendingSegment returning DATALEN=0 while Handler.Send calls MakePacket with
// available > 0, creating degenerate zero-data packets in the sent queue.
//
// The sequence that triggers the panic:
// 1. Connection established
// 2. Remote sends FIN,ACK → local enters CLOSE_WAIT
// 3. Application writes data to TX buffer
// 4. Handler.Send() is called: PendingSegment sets PSH because payloadLen>0,
// then zeroes payloadLen because !established → DATALEN=0 but ok=true
// 5. MakePacket called with zero-length buffer → creates {off:0,end:0} entry
// 6. Handler.Send() called again → same thing → AddPacket panics because
// off=0 but lastPkt.end=0 != bufsize
func TestWriteAfterRemoteFIN(t *testing.T) {
const mtu = 1500
const maxpackets = 3
rng := rand.New(rand.NewSource(11))
client, server := newHandler(t, mtu, maxpackets), newHandler(t, mtu, maxpackets)
setupClientServer(t, rng, client, server)
var rawbuf [mtu]byte
establish(t, client, server, rawbuf[:])
if server.State() != StateEstablished {
t.Fatal("server not established:", server.State())
}
// Client initiates close (sends FIN).
err := client.Close()
if err != nil {
t.Fatal("client close:", err)
}
clear(rawbuf[:])
n, err := client.Send(rawbuf[:])
if err != nil {
t.Fatal("client sending FIN:", err)
}
if n < sizeHeaderTCP {
t.Fatal("expected FIN packet")
}
if client.State() != StateFinWait1 {
t.Fatal("client not in FIN_WAIT_1:", client.State())
}
// Server receives FIN → enters CLOSE_WAIT.
err = server.Recv(rawbuf[:n])
if err != nil {
t.Fatal("server receiving FIN:", err)
}
if server.State() != StateCloseWait {
t.Fatal("server not in CLOSE_WAIT:", server.State())
}
// Application writes data (like an HTTP 404 response).
responseData := []byte("HTTP/1.1 404 Not Found\r\n\r\n")
nw, err := server.Write(responseData)
if err != nil {
t.Fatal("server write:", err)
}
if nw != len(responseData) {
t.Fatal("short write:", nw)
}
// Server sends response — this should include the data, not panic.
// The bug causes a panic on the second Send() call because the first
// creates a degenerate zero-data packet in the sentlist.
clear(rawbuf[:])
n, err = server.Send(rawbuf[:])
if err != nil {
t.Fatal("server send 1:", err)
}
clear(rawbuf[:])
n, err = server.Send(rawbuf[:])
if err != nil {
t.Fatal("server send 2:", err)
}
}
// TestRSTinSynReceived verifies that a RST received during the SYN-RECEIVED
// state correctly reverts the connection to LISTEN per RFC 9293 §3.5.3.
// This is a regression test for a bug where RST segments in non-synchronized
// states were blocked by errRequireSequential, causing connection pool leaks.
func TestRSTinSynReceived(t *testing.T) {
const mtu = 1500
const maxpackets = 3
rng := rand.New(rand.NewSource(2))
client, server := newHandler(t, mtu, maxpackets), newHandler(t, mtu, maxpackets)
setupClientServer(t, rng, client, server)
var rawbuf [mtu]byte
// Client sends SYN.
clear(rawbuf[:])
n, err := client.Send(rawbuf[:])
if err != nil {
t.Fatal("client sending SYN:", err)
}
if client.State() != StateSynSent {
t.Fatal("client not in SynSent:", client.State())
}
// Server receives SYN → transitions to SYN-RECEIVED.
err = server.Recv(rawbuf[:n])
if err != nil {
t.Fatal("server receiving SYN:", err)
}
if server.State() != StateSynRcvd {
t.Fatal("server not in SynRcvd:", server.State())
}
// Server sends SYN,ACK.
clear(rawbuf[:])
n, err = server.Send(rawbuf[:])
if err != nil {
t.Fatal("server sending SYN,ACK:", err)
}
if n < sizeHeaderTCP {
t.Fatal("expected SYN,ACK packet")
}
synackFrm, _ := NewFrame(rawbuf[:n])
synackSeg := synackFrm.Segment(0)
// Construct RST packet from client perspective (as if the remote peer
// rejected the connection). SEQ = ACK from SYN,ACK, no ACK flag, no payload.
clear(rawbuf[:])
rstFrm, err := NewFrame(rawbuf[:])
if err != nil {
t.Fatal("new frame:", err)
}
rstSeg := Segment{
SEQ: synackSeg.ACK, // SEQ = server's ACK value = in window.
Flags: FlagRST,
}
rstFrm.SetSourcePort(client.localPort)
rstFrm.SetDestinationPort(server.localPort)
rstFrm.SetSegment(rstSeg, 5)
rstFrm.SetUrgentPtr(0)
// Server receives RST → should revert to LISTEN per RFC 9293 §3.5.3.
err = server.Recv(rawbuf[:sizeHeaderTCP])
if !IsDroppedErr(err) {
t.Fatal("expected drop segment error from RST recv, got:", err)
}
if server.State() != StateListen {
t.Fatalf("expected server LISTEN after RST in SYN-RECEIVED, got %s", server.State())
}
if server.scb.HasPending() {
t.Fatal("server should have no pending segments after RST")
}
}
// TestBufferNotClearedOnPassiveClose tests that data remains readable after
// the TCP connection is closed by the remote peer. This is a regression test
// for a bug where the receive buffer was cleared when the connection transitioned
+5 -3
View File
@@ -264,8 +264,9 @@ func (listener *Listener) maintainConns() {
continue
}
state := conn.State()
if state > StateEstablished || state.IsClosed() {
// Something went wrong in handshake or pool aborted/closed the connection.
if state > StateEstablished || state.IsClosed() || state == StateListen {
// Something went wrong in handshake, pool aborted/closed the connection,
// or RST reverted the connection to LISTEN (RFC 9293 §3.5.3).
listener.returnIncoming(i)
}
}
@@ -289,7 +290,8 @@ func getConn(conns []handler, remotePort uint16, remoteAddr []byte) int {
func (listener *Listener) maintainConn(conns []handler, idx int, err error) error {
if err == net.ErrClosed {
listener.returnAccepted(idx)
listener.poolReturn(conns[idx].conn)
conns[idx] = handler{}
return nil // avoid closing listener entirely.
}
return err
+30
View File
@@ -3,12 +3,19 @@ package xnet
import (
"io"
"strconv"
"time"
"github.com/soypat/lneto/internet/pcap"
)
type CapturePrinterConfig struct {
NamespaceWidth int
// TimePrecision if non-zero is used to print timestamp
// at which the packet was received. By default the amount of
// seconds since configuration is printed.
TimePrecision int
// Now returns the current time.
Now func() time.Time
}
// CapturePrinter prints internet packets using the [pcap.PacketBreakdown] and [pcap.Formatter] types.
@@ -20,9 +27,18 @@ type CapturePrinter struct {
fmtPcapBuf []byte
// minimum length of namespace on print.
namespaceminwidth int
timeprec int
origin time.Time
now func() time.Time
}
func (stack *CapturePrinter) Configure(writer io.Writer, cfg CapturePrinterConfig) error {
stack.timeprec = cfg.TimePrecision
stack.now = cfg.Now
if stack.printTimestamps() {
stack.origin = cfg.Now()
}
stack.namespaceminwidth = cfg.NamespaceWidth
stack.write = writer.Write
return nil
@@ -36,9 +52,19 @@ func (stack *CapturePrinter) Formatter() *pcap.Formatter {
func (stack *CapturePrinter) PrintPacket(prefix string, pkt []byte) {
fmtbuf := stack.fmtPcapBuf[:0]
useTimestamps := stack.printTimestamps()
var captime time.Time
if useTimestamps {
captime = stack.now()
}
var err error
stack.frms, err = stack.cap.CaptureEthernet(stack.frms[:0], pkt, 0)
if err == nil {
if useTimestamps {
diff := captime.Sub(stack.origin)
fmtbuf = strconv.AppendFloat(fmtbuf, diff.Seconds(), 'f', stack.timeprec, 32)
fmtbuf = append(fmtbuf, ' ')
}
fmtbuf = append(fmtbuf, prefix...)
// Ensure minimum width of packet length display for less jitter in log viewline.
prevlen := len(prefix)
@@ -60,3 +86,7 @@ func (stack *CapturePrinter) PrintPacket(prefix string, pkt []byte) {
stack.write(fmtbuf)
stack.fmtPcapBuf = fmtbuf[:0] // Reuse buffer if allocated at larger size.
}
func (stack *CapturePrinter) printTimestamps() bool {
return stack.timeprec > 0 && stack.now != nil
}
+80
View File
@@ -11,6 +11,7 @@ import (
"github.com/soypat/lneto/arp"
"github.com/soypat/lneto/ethernet"
"github.com/soypat/lneto/internal/ltesto"
"github.com/soypat/lneto/internet/pcap"
"github.com/soypat/lneto/ipv4"
"github.com/soypat/lneto/tcp"
@@ -909,3 +910,82 @@ func TestTCPConn_BufferNotClearedOnPassiveClose(t *testing.T) {
t.Fatalf("read wrong data: got %q, want %q", readBuf[:n], sendData)
}
}
func TestStackAsync_ICMPEchoChecksum(t *testing.T) {
const MTU = 1500
stackAddr := netip.AddrFrom4([4]byte{192, 168, 1, 99})
stackMAC := [6]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}
routerAddr := [4]byte{192, 168, 1, 1}
routerMAC := [6]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}
stack := new(StackAsync)
err := stack.Reset(StackConfig{
Hostname: "ICMPTest",
RandSeed: 42,
StaticAddress: stackAddr,
HardwareAddress: stackMAC,
MTU: MTU,
})
if err != nil {
t.Fatal(err)
}
gen := ltesto.PacketGen{
SrcMAC: routerMAC,
DstMAC: stackMAC,
SrcIPv4: routerAddr,
DstIPv4: stackAddr.As4(),
}
icmpPayload := []byte("abcdefghijklmnopqrstuvwxyz012345") // 32 bytes, typical ping payload.
// Test 1: Valid ICMP echo request should be accepted.
pkt := gen.AppendIPv4ICMPEcho(nil, ltesto.ICMPEchoConfig{
Identifier: 0x1234,
SequenceNumber: 1,
Payload: icmpPayload,
})
err = stack.Demux(pkt, 0)
if err != nil {
t.Fatalf("valid ICMP echo rejected: %v", err)
}
// Test 2: Valid ICMP with trailing FCS bytes (simulates real PIO hardware capture).
// This is a regression test for the bug where recvicmp checksummed ifrm.RawData()
// instead of ifrm.Payload(), causing the 4 trailing FCS bytes to corrupt the checksum.
pkt = gen.AppendIPv4ICMPEcho(nil, ltesto.ICMPEchoConfig{
Identifier: 0x1234,
SequenceNumber: 2,
Payload: icmpPayload,
})
pkt = append(pkt, 0xDE, 0xAD, 0xBE, 0xEF) // Simulate Ethernet FCS.
err = stack.Demux(pkt, 0)
if err != nil {
t.Fatalf("valid ICMP with trailing FCS rejected: %v", err)
}
// Test 3: Corrupted ICMP checksum should be rejected.
pkt = gen.AppendIPv4ICMPEcho(nil, ltesto.ICMPEchoConfig{
Identifier: 0x1234,
SequenceNumber: 3,
Payload: icmpPayload,
})
pkt[len(pkt)-1] ^= 0xFF // Flip bits in last payload byte to corrupt ICMP checksum.
err = stack.Demux(pkt, 0)
if err == nil {
t.Fatal("corrupted ICMP accepted, expected CRC error")
}
// Test 4: Corrupted ICMP with trailing FCS should also be rejected.
pkt = gen.AppendIPv4ICMPEcho(nil, ltesto.ICMPEchoConfig{
Identifier: 0x1234,
SequenceNumber: 4,
Payload: icmpPayload,
})
pkt[len(pkt)-1] ^= 0xFF // Corrupt ICMP payload.
pkt = append(pkt, 0xDE, 0xAD, 0xBE, 0xEF) // Simulate Ethernet FCS.
err = stack.Demux(pkt, 0)
if err == nil {
t.Fatal("corrupted ICMP with FCS accepted, expected CRC error")
}
}