diff --git a/internal/debug.go b/internal/debug.go new file mode 100644 index 0000000..90ae9f9 --- /dev/null +++ b/internal/debug.go @@ -0,0 +1,7 @@ +package internal + +import "log/slog" + +const ( + LevelTrace slog.Level = slog.LevelDebug - 2 +) diff --git a/internal/debug_heaplog.go b/internal/debug_heaplog.go new file mode 100644 index 0000000..4fb70f9 --- /dev/null +++ b/internal/debug_heaplog.go @@ -0,0 +1,60 @@ +//go:build debugheaplog + +package internal + +import ( + "log/slog" + "runtime" + "time" + "unsafe" +) + +const ( + HeapAllocDebugging = true + timefmt = "[01-02 15:04:05.000]" +) + +var ( + memstats runtime.MemStats + lastAllocs uint64 + + timebuf [len(timefmt) * 2]byte +) + +func LogAttrs(_ *slog.Logger, level slog.Level, msg string, attrs ...slog.Attr) { + now := time.Now() + n := len(now.AppendFormat(timebuf[:0], timefmt)) + runtime.ReadMemStats(&memstats) + if memstats.TotalAlloc != lastAllocs { + print("[ALLOC] inc=", int64(memstats.TotalAlloc)-int64(lastAllocs)) + print(" tot=", memstats.TotalAlloc, " seqs") + println() + } + print("time=", unsafe.String(&timebuf[0], n), " ") + if level == LevelTrace { + print("TRACE ") + } else if level < slog.LevelDebug { + print("SEQS ") + } else { + print(level.String(), " ") + } + print(msg) + + for _, a := range attrs { + switch a.Value.Kind() { + case slog.KindString: + print(" ", a.Key, "=", a.Value.String()) + case slog.KindInt64: + print(" ", a.Key, "=", a.Value.Int64()) + case slog.KindUint64: + print(" ", a.Key, "=", a.Value.Uint64()) + case slog.KindBool: + print(" ", a.Key, "=", a.Value.Bool()) + } + } + println() + runtime.ReadMemStats(&memstats) + if memstats.TotalAlloc != lastAllocs { + lastAllocs = memstats.TotalAlloc + } +} diff --git a/internal/debug_noheaplog.go b/internal/debug_noheaplog.go new file mode 100644 index 0000000..4b4f3b2 --- /dev/null +++ b/internal/debug_noheaplog.go @@ -0,0 +1,19 @@ +//go:build !debugheaplog + +package internal + +import ( + "context" + "log/slog" +) + +const HeapAllocDebugging = false + +// LogAttrs is a helper function that is used by all package loggers and that +// can be switched out with the `debugheaplog` build tag for a non-allocating +// logger that prints out when heap allocations occur. +func LogAttrs(l *slog.Logger, level slog.Level, msg string, attrs ...slog.Attr) { + if l != nil { + l.LogAttrs(context.Background(), level, msg, attrs...) + } +} diff --git a/lneto/definitions.go b/lneto/definitions.go index 8e34fec..4d542a8 100644 --- a/lneto/definitions.go +++ b/lneto/definitions.go @@ -56,6 +56,19 @@ const ( minEthPayload = 46 ) +// VLANTag holds priority (PCP) Drop indicator (DEI) and VLAN ID bits of the VLAN tag field. +type VLANTag uint16 + +// DropEligibleIndicator returns true if the DEI bit is set. +// DEI may be used separately or in conjunction with PCP to indicate frames eligible to be dropped in the presence of congestion. +func (vt VLANTag) DropEligibleIndicator() bool { return vt&(1<<3) != 0 } + +// PriorityCodePoint is 3-bit field which refers to the IEEE 802.1p class of service (CoS) and maps to the frame priority level. Different PCP values can be used to prioritize different classes of traffic +func (vt VLANTag) PriorityCodePoint() uint8 { return uint8(vt & 0b111) } + +// VLANIdentifier 12 bit field which specifies which VLAN the frame belongs to. Values of 0 and 4095 are reserved. +func (vt VLANTag) VLANIdentifier() uint16 { return uint16(vt) >> 4 } + // IPToS represents the Traffic Class (a.k.a Type of Service). type IPToS uint8 diff --git a/lneto/frames.go b/lneto/frames.go index 76d8e30..9c3867b 100644 --- a/lneto/frames.go +++ b/lneto/frames.go @@ -3,6 +3,8 @@ package lneto import ( "encoding/binary" "errors" + "fmt" + "math" "github.com/soypat/tseq/lneto/tcp" ) @@ -97,6 +99,10 @@ func (efrm EthFrame) HeaderLength() int { // Payload returns the data portion of the ethernet packet with handling of VLAN packets. func (efrm EthFrame) Payload() []byte { hl := efrm.HeaderLength() + et := efrm.EtherTypeOrSize() + if et.IsSize() { + return efrm.buf[hl:et] + } return efrm.buf[hl:] } @@ -121,6 +127,22 @@ func (efrm EthFrame) SetEtherType(v EtherType) { binary.BigEndian.PutUint16(efrm.buf[12:14], uint16(v)) } +// VLANTag returns the VLAN tag field following the TPID=0x8100. See [VLANTag]. Call [EthFrame.ValidateSize] to ensure this function does not panic. +func (efrm EthFrame) VLANTag() VLANTag { return VLANTag(binary.BigEndian.Uint16(efrm.buf[14:16])) } + +// SetVLANTag sets the VLAN tag field of the Ethernet Header. See [VLANTag]. Call [EthFrame.ValidateSize] to ensure this function does not panic. +func (efrm EthFrame) SetVLANTag(vt VLANTag) { binary.BigEndian.PutUint16(efrm.buf[14:16], uint16(vt)) } + +// VLANEtherType returns the [EtherType] for a VLAN ethernet frame (octet position 16). Call [EthFrame.ValidateSize] to ensure this function does not panic. +func (efrm EthFrame) VLANEtherType() EtherType { + return EtherType(binary.BigEndian.Uint16(efrm.buf[16:18])) +} + +// SetVLANEtherType sets the [EtherType] for a VLAN ethernet frame (octet position 16). Call [EthFrame.ValidateSize] to ensure this function does not panic. +func (efrm EthFrame) SetVLANEtherType(vt EtherType) { + binary.BigEndian.PutUint16(efrm.buf[16:18], uint16(vt)) +} + // IsVLAN returns true if the SizeOrEtherType is set to the VLAN tag 0x8100. This // indicates the EthernetHeader is invalid as-is and instead of EtherType the field // contains the first two octets of a 4 octet 802.1Q VLAN tag. In this case 4 more bytes @@ -246,17 +268,17 @@ func (ifrm IPv4Frame) HeaderLength() int { } func (ifrm IPv4Frame) ihl() uint8 { - return ifrm.buf[0] >> 4 + return ifrm.buf[0] & 0xf } // VersionAndIHL returns the version and IHL fields in the IPv4 header. Version should always be 4. func (ifrm IPv4Frame) VersionAndIHL() (version, IHL uint8) { v := ifrm.buf[0] - return v & 0xf, v >> 4 + return v >> 4, v & 0xf } // SetVersionAndIHL sets the version and IHL fields in the IPv4 header. Version should always be 4. -func (ifrm IPv4Frame) SetVersionAndIHL(version, IHL uint8) { ifrm.buf[0] = version&0xf | IHL<<4 } +func (ifrm IPv4Frame) SetVersionAndIHL(version, IHL uint8) { ifrm.buf[0] = version<<4 | IHL&0xf } // ToS (Type of Service) contains Differential Services Code Point (DSCP) and // Explicit Congestion Notification (ECN) union data. @@ -376,6 +398,13 @@ func (ifrm IPv4Frame) Payload() []byte { return ifrm.buf[off:l] } +// Options returns the options portion of the IPv4 header. May be zero lengthed. +// Be sure to call [IPv4Frame.ValidateSize] beforehand to avoid panic. +func (ifrm IPv4Frame) Options() []byte { + off := ifrm.HeaderLength() + return ifrm.buf[sizeHeaderIPv4:off] +} + // ClearHeader zeros out the fixed(non-variable) header contents. func (frm IPv4Frame) ClearHeader() { for i := range frm.buf[:sizeHeaderIPv4] { @@ -555,6 +584,11 @@ func (tfrm TCPFrame) HeaderLength() (tcpWords int) { return 4 * int(offset) } +func (tfrm TCPFrame) WindowSize() uint16 { return binary.BigEndian.Uint16(tfrm.buf[14:16]) } +func (tfrm TCPFrame) SetWindowSize(v uint16) { + binary.BigEndian.PutUint16(tfrm.buf[14:16], v) +} + // CRC returns the checksum field in the TCP header. func (tfrm TCPFrame) CRC() uint16 { return binary.BigEndian.Uint16(tfrm.buf[16:18]) @@ -595,13 +629,8 @@ func (tfrm TCPFrame) crcWrite(crc *CRC791) { crc.Write(tfrm.buf[18:]) } -func (tfrm TCPFrame) SetUrgentPtr(up uint16) { - binary.BigEndian.PutUint16(tfrm.buf[18:20], up) -} - -func (tfrm TCPFrame) UrgentPtr() uint16 { - return binary.BigEndian.Uint16(tfrm.buf[18:20]) -} +func (tfrm TCPFrame) UrgentPtr() uint16 { return binary.BigEndian.Uint16(tfrm.buf[18:20]) } +func (tfrm TCPFrame) SetUrgentPtr(up uint16) { binary.BigEndian.PutUint16(tfrm.buf[18:20], up) } // Payload returns the payload content section of the TCP packet (not including TCP options). // Be sure to call [TCPFrame.ValidateSize] beforehand to avoid panic. @@ -609,6 +638,20 @@ func (tfrm TCPFrame) Payload() []byte { return tfrm.buf[tfrm.HeaderLength():] } +// Segment returns the [tcp.Segment] representation of the TCP header and data length. +func (tfrm TCPFrame) Segment(payloadSize int) tcp.Segment { + if payloadSize > math.MaxUint32 { + panic("TCP overflow payload size") + } + return tcp.Segment{ + SEQ: tfrm.Seq(), + ACK: tfrm.Ack(), + WND: tcp.Size(tfrm.WindowSize()), + DATALEN: tcp.Size(payloadSize), + Flags: tcp.Flags(binary.BigEndian.Uint16(tfrm.buf[12:14])).Mask(), + } +} + // Options returns the TCP option buffer portion of the frame. The returned slice may be zero length. // Be sure to call [TCPFrame.ValidateSize] beforehand to avoid panic. func (tfrm TCPFrame) Options() []byte { @@ -622,6 +665,11 @@ func (frm TCPFrame) ClearHeader() { } } +func (tfrm TCPFrame) String() string { + seg := tfrm.Segment(len(tfrm.Payload())) + return fmt.Sprintf("%+v", seg) +} + // UDPFrame encapsulates the raw data of a UDP datagram // and provides methods for manipulating, validating and // retrieving fields and payload data. See [RFC768]. diff --git a/lneto/lneto_test.go b/lneto/lneto_test.go new file mode 100644 index 0000000..dcfece9 --- /dev/null +++ b/lneto/lneto_test.go @@ -0,0 +1,237 @@ +package lneto + +import ( + "bytes" + "math/rand" + "testing" + + "github.com/soypat/tseq/lneto/tcp" +) + +func TestTCPMarshalUnmarshal(t *testing.T) { + rng := rand.New(rand.NewSource(1)) + var gen packetGen + gen.randomizeAddrs(rng) + const maxSize = 4096 + src := make([]byte, maxSize) + dst := make([]byte, maxSize) + for i := 0; i < 512; i++ { + src = gen.appendRandomIPv4TCPPacket(src[:0], rng) + dst = dst[:len(src)] + testMoveTCPPacket(t, src, dst) + if !bytes.Equal(src, dst) { + t.Fatal("mismatching data") + } + } +} + +func testMoveTCPPacket(t *testing.T, src, dst []byte) { + if len(src) != len(dst) { + panic("expect src and dst same length") + } + efrm, err := NewEthFrame(src) + if err != nil { + t.Fatal(err) + } + epl := efrm.Payload() + ifrm, err := NewIPv4Frame(epl) + if err != nil { + t.Fatal(err) + } + ipl := ifrm.Payload() + tfrm, err := NewTCPFrame(ipl) + if err != nil { + t.Fatal(err) + } + + efrm2, _ := NewEthFrame(dst) + *efrm2.DestinationHardwareAddr() = *efrm.DestinationHardwareAddr() + *efrm2.SourceHardwareAddr() = *efrm.SourceHardwareAddr() + efrm2.SetEtherType(efrm.EtherTypeOrSize()) + if efrm.EtherTypeOrSize() == EtherTypeVLAN { + efrm2.SetVLANTag(efrm.VLANTag()) + efrm2.SetVLANEtherType(efrm.VLANEtherType()) + } + + ifrm2, _ := NewIPv4Frame(efrm2.Payload()) + ifrm2.SetVersionAndIHL(ifrm.VersionAndIHL()) + ifrm2.SetToS(ifrm.ToS()) + ifrm2.SetFlags(ifrm.Flags()) + ifrm2.SetTotalLength(ifrm.TotalLength()) + ifrm2.SetID(ifrm.ID()) + ifrm2.SetTTL(ifrm.TTL()) + ifrm2.SetProtocol(ifrm.Protocol()) + ifrm2.SetCRC(ifrm.CRC()) + *ifrm2.SourceAddr() = *ifrm.SourceAddr() + *ifrm2.DestinationAddr() = *ifrm.DestinationAddr() + + tfrm2, _ := NewTCPFrame(ifrm2.Payload()) + tfrm2.SetSourcePort(tfrm.SourcePort()) + tfrm2.SetDestinationPort(tfrm.DestinationPort()) + tfrm2.SetSeq(tfrm.Seq()) + tfrm2.SetAck(tfrm.Ack()) + tfrm2.SetOffsetAndFlags(tfrm.OffsetAndFlags()) + tfrm2.SetWindowSize(tfrm.WindowSize()) + tfrm2.SetCRC(tfrm.CRC()) + tfrm2.SetUrgentPtr(tfrm.UrgentPtr()) + + copy(ifrm2.Options(), ifrm.Options()) + copy(tfrm2.Options(), tfrm.Options()) + copy(tfrm2.Payload(), tfrm.Payload()) + + elen := efrm.HeaderLength() + if !bytes.Equal(src[:elen], dst[:elen]) { + t.Fatalf("Ethernet header mismatch\n%x\n%x", src[:elen], dst[:elen]) + } + ilen := ifrm.HeaderLength() + if !bytes.Equal(src[elen:elen+20], dst[elen:elen+20]) { + t.Fatalf("IPv4 header mismatch\n%x\n%x", src[elen:elen+20], dst[elen:elen+20]) + } + ipoptLen := len(ifrm.Options()) + if !bytes.Equal(ifrm.Options(), ifrm2.Options()) { + t.Fatalf("IPv4 options mismatch\n%x\n%x", ifrm.Options(), ifrm2.Options()) + } else if ipoptLen > 0 && &ifrm.Options()[0] != &src[elen+20] { + t.Fatal("IPv4 options start pointer mismatch") + } + + tlen := tfrm.HeaderLength() + toff := elen + ilen + ipoptLen + if !bytes.Equal(src[toff:toff+tlen], dst[toff:toff+tlen]) { + t.Fatalf("TCP header mismatch\n%x\n%x", src[toff:toff+tlen], dst[toff:toff+tlen]) + } + payload := tfrm.Payload() + + if !bytes.Equal(payload, tfrm2.Payload()) { + t.Fatalf("payload mismatch %d %d", len(payload), len(tfrm2.Payload())) + } +} + +type packetGen struct { + srcMAC, dstMAC [6]byte // hardware address + srcIPv4, dstIPv4 [4]byte // address + srcTCP, dstTCP uint16 // ports +} + +func (gen *packetGen) randomizeAddrs(rng *rand.Rand) { + rng.Read(gen.srcMAC[:]) + rng.Read(gen.dstMAC[:]) + rng.Read(gen.srcIPv4[:]) + rng.Read(gen.dstIPv4[:]) + ports := rng.Uint32() + gen.srcTCP = uint16(ports) + gen.dstTCP = uint16(ports >> 16) +} + +func (gen *packetGen) appendRandomIPv4TCPPacket(dst []byte, rng *rand.Rand) []byte { + ri := rng.Int() + var ( + isVLAN = ri&(1<<0) != 0 + hasIPOpt = ri&(1<<1) != 0 + hasTCPOpt = ri&(1<<2) != 0 + hasPayload = ri&(1<<3) != 0 + ) + var etherType EtherType = EtherTypeIPv4 + var ipOpts []byte + if hasIPOpt { + ipOpts = []byte{1, 2, 3, 4} + } + ethsize := 14 + if isVLAN { + etherType = EtherTypeVLAN + ethsize = 18 + } + var tcpOpts []byte + if hasTCPOpt { + tcpOpts = []byte{byte(tcp.OptSACKPermitted), 0, 1, 0} + } + var payloadLen int + if hasPayload { + payloadLen = (ri >> 16) % 1024 + } + ipOptWLen := sizeWord(len(ipOpts)) + tcpOptWlen := sizeWord(len(tcpOpts)) + off := len(dst) + dst = append(dst, make([]byte, ethsize+sizeHeaderIPv4+4*int(ipOptWLen)+sizeHeaderTCP+4*int(tcpOptWlen)+payloadLen)...) + efrm, err := NewEthFrame(dst[off:]) + if err != nil { + panic(err) + } + *efrm.DestinationHardwareAddr() = gen.dstMAC + *efrm.SourceHardwareAddr() = gen.srcMAC + + efrm.SetEtherType(etherType) + if isVLAN { + efrm.SetVLANEtherType(EtherTypeIPv4) + efrm.SetVLANTag(1 << 4) + } + ethernetPayload := efrm.Payload() + ifrm, err := NewIPv4Frame(ethernetPayload) + if err != nil { + panic(err) + } + ifrm.SetVersionAndIHL(4, sizeWord(20+len(ipOpts))) + ifrm.SetToS(192) + ifrm.SetTotalLength(uint16(len(ethernetPayload))) + ifrm.SetID(uint16(rng.Uint32())) + ifrm.SetFlags(0x4001) // Don't fragment. + ifrm.SetTTL(64) + ifrm.SetProtocol(IPProtoTCP) + *ifrm.SourceAddr() = gen.srcIPv4 + *ifrm.DestinationAddr() = gen.dstIPv4 + ifrm.SetCRC(ifrm.CalculateHeaderCRC()) + + ipPayload := ifrm.Payload() + tfrm, err := NewTCPFrame(ipPayload) + if err != nil { + panic(err) + } + tfrm.SetSourcePort(gen.srcTCP) + tfrm.SetDestinationPort(gen.dstTCP) + tfrm.SetSeq(tcp.Value(rng.Uint32())) + tfrm.SetAck(tcp.Value(rng.Uint32())) + wlen := sizeWord(sizeHeaderTCP + len(tcpOpts)) + tfrm.SetOffsetAndFlags(wlen, tcp.Flags(rng.Uint32())) + tfrm.SetWindowSize(uint16(rng.Uint32())) + urgPtr := uint16(rng.Uint32()) + tfrm.SetUrgentPtr(urgPtr) + tcpPayload := tfrm.Payload() + var firstPayloadByte byte + if len(tcpPayload) > 0 { + rng.Read(tcpPayload) + firstPayloadByte = tcpPayload[0] + } + // Set Variable section of data. + copy(ifrm.Options(), ipOpts) + copy(tfrm.Options(), tcpOpts) + switch { + case gen.srcTCP != tfrm.SourcePort(): + panic("IP options overwrite TCP header") + case !bytes.Equal(ifrm.Options(), ipOpts): + panic("bad ip options written, ensure ip options length is multiple of 4") + case !bytes.Equal(tfrm.Options(), tcpOpts): + panic("bad tcp options written, ensure tcp options length is multiple of 4") + case *ifrm.DestinationAddr() != gen.dstIPv4: + panic("IP options overwrite own header") + case tfrm.UrgentPtr() != urgPtr: + panic("TCP options overwrite urgent pointer field?") + case len(tcpPayload) > 0 && firstPayloadByte != tcpPayload[0]: + panic("TCP options overwrite payload") + } + err = efrm.ValidateSize() + if err != nil { + panic(err) + } + err = ifrm.ValidateSize() + if err != nil { + panic(err) + } + err = tfrm.ValidateSize() + if err != nil { + panic(err) + } + return dst +} + +func sizeWord(l int) uint8 { + return uint8((l + 3) / 4) +} diff --git a/lneto/tcp/control.go b/lneto/tcp/control.go index 581c498..ad890eb 100644 --- a/lneto/tcp/control.go +++ b/lneto/tcp/control.go @@ -1,6 +1,13 @@ package tcp -import "log/slog" +import ( + "io" + "log/slog" + "math" + "net" + + "github.com/soypat/tseq/internal" +) // ControlBlock is a partial Transmission Control Block (TCB) implementation as // per RFC 9293 in section 3.3.1. In contrast with the description in RFC9293, @@ -49,6 +56,61 @@ type ControlBlock struct { log *slog.Logger } +// State returns the current state of the TCP connection. +func (tcb *ControlBlock) State() State { return tcb.state } + +// RecvNext returns the next sequence number expected to be received from remote. +// This implementation will reject segments that are not the next expected sequence. +// RecvNext returns 0 before StateSynRcvd. +func (tcb *ControlBlock) RecvNext() Value { return tcb.rcv.NXT } + +// RecvWindow returns the receive window size. If connection is closed will return 0. +func (tcb *ControlBlock) RecvWindow() Size { return tcb.rcv.WND } + +// ISS returns the initial sequence number of the connection that was defined on a call to Open by user. +func (tcb *ControlBlock) ISS() Value { return tcb.snd.ISS } + +// MaxInFlightData returns the maximum size of a segment that can be sent by taking into account +// the send window size and the unacked data. Returns 0 before StateSynRcvd. +func (tcb *ControlBlock) MaxInFlightData() Size { + if !tcb.state.hasIRS() { + return 0 // SYN not yet received. + } + unacked := Sizeof(tcb.snd.UNA, tcb.snd.NXT) + return tcb.snd.WND - unacked - 1 // TODO: is this -1 supposed to be here? +} + +// SetWindow sets the local receive window size. This represents the maximum amount of data +// that is permitted to be in flight. +func (tcb *ControlBlock) SetRecvWindow(wnd Size) { + tcb.rcv.WND = wnd +} + +// SetLogger sets the logger to be used by the ControlBlock. +func (tcb *ControlBlock) SetLogger(log *slog.Logger) { + tcb.log = log +} + +// IncomingIsKeepalive checks if an incoming segment is a keepalive segment. +// Segments which are keepalives should not be passed into Recv or Send methods. +func (tcb *ControlBlock) IncomingIsKeepalive(incomingSegment Segment) bool { + return incomingSegment.SEQ == tcb.rcv.NXT-1 && + incomingSegment.Flags == FlagACK && + incomingSegment.ACK == tcb.snd.NXT && incomingSegment.DATALEN == 0 +} + +// MakeKeepalive creates a TCP keepalive segment. This segment +// should not be passed into Recv or Send methods. +func (tcb *ControlBlock) MakeKeepalive() Segment { + return Segment{ + SEQ: tcb.snd.NXT - 1, + ACK: tcb.rcv.NXT, + Flags: FlagACK, + WND: tcb.rcv.WND, + DATALEN: 0, + } +} + // 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 @@ -75,3 +137,400 @@ type recvSpace struct { NXT Value // receive next. seqs before this have been acked. this seq and up to NXT+WND-1 are allowed to be sent. Corresponds to remote data. WND Size // receive window defined by local. Permitted number of remote unacked octets in flight. } + +// Open implements a passive/active opening of a connection. +// state must be StateListen or StateSynSent. +func (tcb *ControlBlock) Open(iss Value, wnd Size, state State) (err error) { + switch { + case tcb.state != StateClosed && tcb.state != StateListen: + err = errTCBNotClosed + case state != StateListen && state != StateSynSent: + err = errInvalidState + case wnd > math.MaxUint16: + err = errWindowTooLarge + } + if err != nil { + tcb.logerr("tcb:open", slog.String("err", err.Error())) + return err + } + tcb.state = state + tcb.resetRcv(wnd, 0) + tcb.resetSnd(iss, 1) + tcb.pending = [2]Flags{} + if state == StateSynSent { + tcb.pending[0] = FlagSYN + } + tcb.trace("tcb:open", slog.String("state", tcb.state.String())) + return nil +} + +// 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 } + +// PendingSegment calculates a suitable next segment to send from a payload length. +// It does not modify the ControlBlock state or pending segment queue. +func (tcb *ControlBlock) PendingSegment(payloadLen int) (_ Segment, ok bool) { + if tcb.challengeAck { + tcb.challengeAck = false + return Segment{SEQ: tcb.snd.NXT, ACK: tcb.rcv.NXT, Flags: FlagACK, WND: tcb.rcv.WND}, true + } + pending := tcb.pending[0] + established := tcb.state == StateEstablished + if !established && tcb.state != StateCloseWait { + payloadLen = 0 // Can't send data if not established. + } + if pending == 0 && payloadLen == 0 { + return Segment{}, false // No pending segment. + } + + // Limit payload to what send window allows. + inFlight := tcb.snd.inFlight() + _ = inFlight + maxPayload := tcb.snd.maxSend() + if payloadLen > int(maxPayload) { + if maxPayload == 0 && !tcb.pending[0].HasAny(FlagFIN|FlagRST|FlagSYN) { + return Segment{}, false + } else if maxPayload > tcb.snd.WND { + panic("seqs: bad calculation") + } + payloadLen = int(maxPayload) + } + + if established { + 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. + } + + var ack Value + if pending.HasAny(FlagACK) { + ack = tcb.rcv.NXT + } + + var seq Value = tcb.snd.NXT + if pending.HasAny(FlagRST) { + seq = tcb.rstPtr + } + + seg := Segment{ + SEQ: seq, + ACK: ack, + WND: tcb.rcv.WND, + Flags: pending, + DATALEN: Size(payloadLen), + } + tcb.traceSeg("tcb:pending-out", seg) + return seg, true +} + +// Recv processes a segment that is being received from the network. It updates the TCB +// if there is no error. The ControlBlock can only receive segments that are the next +// expected sequence number which means the caller must handle the out-of-order case +// and buffering that comes with it. +func (tcb *ControlBlock) Recv(seg Segment) (err error) { + err = tcb.validateIncomingSegment(seg) + if err != nil { + tcb.traceRcv("tcb:rcv.reject") + tcb.traceSeg("tcb:rcv.reject", seg) + tcb.logerr("tcb:rcv.reject", slog.String("err", err.Error())) + return err + } + + prevNxt := tcb.snd.NXT + var pending Flags + switch tcb.state { + case StateListen: + pending, err = tcb.rcvListen(seg) + case StateSynSent: + pending, err = tcb.rcvSynSent(seg) + case StateSynRcvd: + pending, err = tcb.rcvSynRcvd(seg) + case StateEstablished: + pending, err = tcb.rcvEstablished(seg) + case StateFinWait1: + pending, err = tcb.rcvFinWait1(seg) + case StateFinWait2: + pending, err = tcb.rcvFinWait2(seg) + case StateCloseWait: + case StateLastAck: + if seg.Flags.HasAny(FlagACK) { + tcb.close() + } + case StateClosing: + // Thanks to @knieriem for finding and reporting this bug. + if seg.Flags.HasAny(FlagACK) { + tcb.state = StateTimeWait + } + default: + panic("unexpected recv state:" + tcb.state.String()) + } + if err != nil { + return err + } + + tcb.pending[0] |= pending + if prevNxt != 0 && tcb.snd.NXT != prevNxt && tcb.logenabled(slog.LevelDebug) { + tcb.debug("tcb:snd.nxt-change", slog.String("state", tcb.state.String()), + slog.Uint64("seg.ack", uint64(seg.ACK)), slog.Uint64("snd.nxt", uint64(tcb.snd.NXT)), + slog.Uint64("prevnxt", uint64(prevNxt)), slog.Uint64("seg.seq", uint64(seg.SEQ))) + } + + // We accept the segment and update TCB state. + tcb.snd.WND = seg.WND + if seg.Flags.HasAny(FlagACK) { + tcb.snd.UNA = seg.ACK + } + seglen := seg.LEN() + tcb.rcv.NXT.UpdateForward(seglen) + + if tcb.logenabled(internal.LevelTrace) { + tcb.traceRcv("tcb:rcv") + tcb.traceSeg("recv:seg", seg) + } + return err +} + +// Send processes a segment that is being sent to the network. It updates the TCB +// if there is no error. +func (tcb *ControlBlock) Send(seg Segment) error { + err := tcb.validateOutgoingSegment(seg) + if err != nil { + tcb.traceSnd("tcb:snd.reject") + tcb.traceSeg("tcb:snd.reject", seg) + tcb.logerr("tcb:snd.reject", slog.String("err", err.Error())) + return err + } + + hasFIN := seg.Flags.HasAny(FlagFIN) + hasACK := seg.Flags.HasAny(FlagACK) + var newPending Flags + switch tcb.state { + case StateSynRcvd: + if hasFIN { + tcb.state = StateFinWait1 // RFC 9293: 3.10.4 CLOSE call. + } + case StateClosing: + if hasACK { + tcb.state = StateTimeWait + } + case StateEstablished: + if hasFIN { + tcb.state = StateFinWait1 + } + case StateCloseWait: + if hasFIN { + tcb.state = StateLastAck + } else if hasACK { + newPending = finack // Queue finack. + } + } + + // Advance pending flags queue. + tcb.pending[0] &^= seg.Flags + if tcb.pending[0] == 0 { + // Ensure we don't queue a FINACK if we have already sent a FIN. + tcb.pending = [2]Flags{tcb.pending[1] &^ (seg.Flags & (FlagFIN)), 0} + } + tcb.pending[0] |= newPending + + // The segment is valid, we can update TCB state. + seglen := seg.LEN() + tcb.snd.NXT.UpdateForward(seglen) + tcb.rcv.WND = seg.WND + + if tcb.logenabled(internal.LevelTrace) { + tcb.traceSnd("tcb:snd") + tcb.traceSeg("tcb:snd", seg) + } + + return nil +} + +func (tcb *ControlBlock) validateOutgoingSegment(seg Segment) (err error) { + hasAck := seg.Flags.HasAny(FlagACK) + checkSeq := !seg.Flags.HasAny(FlagRST) + seglast := seg.Last() + // Extra check for when send Window is zero and no data is being sent. + zeroWindowOK := tcb.snd.WND == 0 && seg.DATALEN == 0 && seg.SEQ == tcb.snd.NXT + outOfWindow := checkSeq && !InWindow(seg.SEQ, tcb.snd.NXT, tcb.snd.WND) && + !zeroWindowOK + switch { + case tcb.state == StateClosed: + err = io.ErrClosedPipe + case seg.WND > math.MaxUint16: + err = errWindowTooLarge + case hasAck && seg.ACK != tcb.rcv.NXT: + err = errAckNotNext + + case outOfWindow: + if tcb.snd.WND == 0 { + err = errZeroWindow + } else { + err = errSeqNotInWindow + } + + case seg.DATALEN > 0 && (tcb.state == StateFinWait1 || tcb.state == StateFinWait2): + err = errConnectionClosing // Case 1: No further SENDs from the user will be accepted by the TCP implementation. + + case checkSeq && tcb.snd.WND == 0 && seg.DATALEN > 0 && seg.SEQ == tcb.snd.NXT: + err = errZeroWindow + + case checkSeq && !InWindow(seglast, tcb.snd.NXT, tcb.snd.WND) && !zeroWindowOK: + err = errLastNotInWindow + } + return err +} + +func (tcb *ControlBlock) validateIncomingSegment(seg Segment) (err error) { + flags := seg.Flags + hasAck := flags.HasAll(FlagACK) + // Short circuit SEQ checks if SYN present since the incoming segment initialize1s connection. + checkSEQ := !flags.HasAny(FlagSYN) + established := tcb.state == StateEstablished + preestablished := tcb.state.IsPreestablished() + acksOld := hasAck && !LessThan(tcb.snd.UNA, seg.ACK) + acksUnsentData := hasAck && !LessThanEq(seg.ACK, tcb.snd.NXT) + ctlOrDataSegment := established && (seg.DATALEN > 0 || flags.HasAny(FlagFIN|FlagRST)) + zeroWindowOK := tcb.rcv.WND == 0 && seg.DATALEN == 0 && seg.SEQ == tcb.rcv.NXT + // See section 3.4 of RFC 9293 for more on these checks. + switch { + case seg.WND > math.MaxUint16: + err = errWindowOverflow + case tcb.state == StateClosed: + err = io.ErrClosedPipe + + case checkSEQ && tcb.rcv.WND == 0 && seg.DATALEN > 0 && seg.SEQ == tcb.rcv.NXT: + err = errZeroWindow + + case checkSEQ && !InWindow(seg.SEQ, tcb.rcv.NXT, tcb.rcv.WND) && !zeroWindowOK: + err = errSeqNotInWindow + + case checkSEQ && !InWindow(seg.Last(), tcb.rcv.NXT, tcb.rcv.WND) && !zeroWindowOK: + err = errLastNotInWindow + + case checkSEQ && 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 + } + if err != nil { + return err + } + if flags.HasAny(FlagRST) { + return tcb.handleRST(seg.SEQ) + } + + isDebug := tcb.logenabled(slog.LevelDebug) + // Drop-segment checks. + switch { + // Special treatment of duplicate ACKs on established connection and of ACKs of unsent data. + // https://www.rfc-editor.org/rfc/rfc9293.html#section-3.10.7.4-2.5.2.2.2.3.2.1 + case established && acksOld && !ctlOrDataSegment: + err = errDropSegment + tcb.pending[0] &= FlagFIN // Completely ignore duplicate ACKs but do not erase fin bit. + if isDebug { + tcb.debug("rcv:ACK-dup", slog.String("state", tcb.state.String()), + slog.Uint64("seg.ack", uint64(seg.ACK)), slog.Uint64("snd.una", uint64(tcb.snd.UNA))) + } + + case established && acksUnsentData: + err = errDropSegment + tcb.pending[0] = FlagACK // Send ACK for unsent data. + if isDebug { + tcb.debug("rcv:ACK-unsent", slog.String("state", tcb.state.String()), + slog.Uint64("seg.ack", uint64(seg.ACK)), slog.Uint64("snd.nxt", uint64(tcb.snd.NXT))) + } + + case preestablished && (acksOld || acksUnsentData): + err = errDropSegment + tcb.pending[0] = FlagRST + tcb.rstPtr = seg.ACK + tcb.resetSnd(tcb.snd.ISS, seg.WND) + if isDebug { + tcb.debug("rcv:RST-old", slog.String("state", tcb.state.String()), slog.Uint64("ack", uint64(seg.ACK))) + } + } + return err +} + +func (tcb *ControlBlock) resetSnd(localISS Value, remoteWND Size) { + tcb.snd = sendSpace{ + ISS: localISS, + UNA: localISS, + NXT: localISS, + WND: remoteWND, + // UP, WL1, WL2 defaults to zero values. + } +} + +func (tcb *ControlBlock) resetRcv(localWND Size, remoteISS Value) { + tcb.rcv = recvSpace{ + IRS: remoteISS, + NXT: remoteISS, + WND: localWND, + } +} + +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() { + 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.close() // Enter closed state and return. + return net.ErrClosed + } + return errDropSegment +} + +func (tcb *ControlBlock) rstJump() Value { + return 100 +} + +// close sets ControlBlock state to closed and resets all sequence numbers and pending flag. +func (tcb *ControlBlock) close() { + tcb.state = StateClosed + tcb.pending = [2]Flags{} + tcb.resetRcv(0, 0) + tcb.resetSnd(0, 0) + tcb.debug("tcb:close") +} + +// Close implements a passive/active closing of a connection. It does not immediately +// delete the TCB but initiates the process so that pending outgoing segments initiate +// the closing process. After a call to Close users should not send more data. +// Close returns an error if the connection is already closed or closing. +func (tcb *ControlBlock) Close() (err error) { + // See RFC 9293: 3.10.4 CLOSE call. + switch tcb.state { + case StateClosed: + err = errConnNotexist + case StateCloseWait: + tcb.state = StateLastAck + tcb.pending = [2]Flags{FlagFIN, FlagACK} + case StateListen, StateSynSent: + tcb.close() + case StateSynRcvd, StateEstablished: + // We suppose user has no more pending data to send, so we flag FIN to be sent. + // Users of this API should call Close only when they have no more data to send. + tcb.pending[0] = (tcb.pending[0] & FlagACK) | FlagFIN + case StateFinWait2, StateTimeWait: + err = errConnectionClosing + default: + err = errInvalidState + } + if err == nil { + tcb.trace("tcb:close", slog.String("state", tcb.state.String())) + } else { + tcb.logerr("tcb:close", slog.String("err", err.Error())) + } + return err +} diff --git a/lneto/tcp/control_rcvhandlers.go b/lneto/tcp/control_rcvhandlers.go new file mode 100644 index 0000000..4d23e07 --- /dev/null +++ b/lneto/tcp/control_rcvhandlers.go @@ -0,0 +1,107 @@ +package tcp + +func (tcb *ControlBlock) rcvListen(seg Segment) (pending Flags, err error) { + switch { + case !seg.Flags.HasAll(FlagSYN): + err = errExpectedSYN + } + if err != nil { + return 0, err + } + // Initialize all connection state: + tcb.resetSnd(tcb.snd.ISS, seg.WND) + tcb.resetRcv(tcb.rcv.WND, seg.SEQ) + + // We must respond with SYN|ACK frame after receiving SYN in listen state (three way handshake). + tcb.pending[0] = synack + tcb.state = StateSynRcvd + return synack, nil +} + +func (tcb *ControlBlock) rcvSynSent(seg Segment) (pending Flags, err error) { + hasSyn := seg.Flags.HasAny(FlagSYN) + hasAck := seg.Flags.HasAny(FlagACK) + switch { + case !hasSyn: + err = errExpectedSYN + + case hasAck && seg.ACK != tcb.snd.UNA+1: + err = errBadSegack + } + if err != nil { + return 0, err + } + + if hasAck { + tcb.state = StateEstablished + pending = FlagACK + tcb.resetRcv(tcb.rcv.WND, seg.SEQ) + } else { + // Simultaneous connection sync edge case. + pending = synack + tcb.state = StateSynRcvd + tcb.resetSnd(tcb.snd.ISS, seg.WND) + tcb.resetRcv(tcb.rcv.WND, seg.SEQ) + } + return pending, nil +} + +func (tcb *ControlBlock) rcvSynRcvd(seg Segment) (pending Flags, err error) { + switch { + // case !seg.Flags.HasAll(FlagACK): + // err = errors.New("rcvSynRcvd: expected ACK") + case seg.ACK != tcb.snd.UNA+1: + err = errBadSegack + } + if err != nil { + return 0, err + } + tcb.state = StateEstablished + return 0, nil +} + +func (tcb *ControlBlock) rcvEstablished(seg Segment) (pending Flags, err error) { + flags := seg.Flags + + dataToAck := seg.DATALEN > 0 + hasFin := flags.HasAny(FlagFIN) + if dataToAck || hasFin { + pending = FlagACK + if hasFin { + // See Figure 5: TCP Connection State Diagram of RFC 9293. + tcb.state = StateCloseWait + tcb.pending[1] = FlagFIN // Queue FIN for after the CloseWait ACK. + } + } + + return pending, nil +} + +func (tcb *ControlBlock) rcvFinWait1(seg Segment) (pending Flags, err error) { + flags := seg.Flags + hasFin := flags&FlagFIN != 0 + hasAck := flags&FlagACK != 0 + switch { + case hasFin && hasAck && seg.ACK == tcb.snd.NXT: + // Special case: Server sent a FINACK response to our FIN so we enter TimeWait directly. + // We have to check ACK against send NXT to avoid simultaneous close sequence edge case. + tcb.state = StateTimeWait + case hasFin: + tcb.state = StateClosing + case hasAck: + // TODO(soypat): Check if this branch does NOT need ACK queued. Online flowcharts say not needed. + tcb.state = StateFinWait2 + default: + return 0, errFinwaitExpectedACK + } + pending = FlagACK + return pending, nil +} + +func (tcb *ControlBlock) rcvFinWait2(seg Segment) (pending Flags, err error) { + if !seg.Flags.HasAll(finack) { + return pending, errFinwaitExpectedFinack + } + tcb.state = StateTimeWait + return FlagACK, nil +} diff --git a/lneto/tcp/debug.go b/lneto/tcp/debug.go new file mode 100644 index 0000000..1c7889f --- /dev/null +++ b/lneto/tcp/debug.go @@ -0,0 +1,59 @@ +package tcp + +import ( + "context" + "log/slog" + + "github.com/soypat/tseq/internal" +) + +func (tcb *ControlBlock) logenabled(lvl slog.Level) bool { + return internal.HeapAllocDebugging || (tcb.log != nil && tcb.log.Handler().Enabled(context.Background(), lvl)) +} + +func (tcb *ControlBlock) logattrs(lvl slog.Level, msg string, attrs ...slog.Attr) { + internal.LogAttrs(tcb.log, lvl, msg, attrs...) +} + +func (tcb *ControlBlock) debug(msg string, attrs ...slog.Attr) { + tcb.logattrs(slog.LevelDebug, msg, attrs...) +} + +func (tcb *ControlBlock) trace(msg string, attrs ...slog.Attr) { + tcb.logattrs(internal.LevelTrace, msg, attrs...) +} + +func (tcb *ControlBlock) logerr(msg string, attrs ...slog.Attr) { + tcb.logattrs(slog.LevelError, msg, attrs...) +} + +func (tcb *ControlBlock) traceSnd(msg string) { + tcb.trace(msg, + slog.String("state", tcb.state.String()), + slog.Uint64("pend", uint64(tcb.pending[0])), + slog.Uint64("snd.nxt", uint64(tcb.snd.NXT)), + slog.Uint64("snd.una", uint64(tcb.snd.UNA)), + slog.Uint64("snd.wnd", uint64(tcb.snd.WND)), + ) +} + +func (tcb *ControlBlock) traceRcv(msg string) { + tcb.trace(msg, + 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), + ) +} + +func (tcb *ControlBlock) traceSeg(msg string, seg Segment) { + if tcb.logenabled(internal.LevelTrace) { + tcb.trace(msg, + slog.Uint64("seg.seq", uint64(seg.SEQ)), + slog.Uint64("seg.ack", uint64(seg.ACK)), + slog.Uint64("seg.wnd", uint64(seg.WND)), + slog.String("seg.flags", seg.Flags.String()), + slog.Uint64("seg.data", uint64(seg.DATALEN)), + ) + } +} diff --git a/lneto/tcp/definitions.go b/lneto/tcp/definitions.go index f290d80..6a95ab8 100644 --- a/lneto/tcp/definitions.go +++ b/lneto/tcp/definitions.go @@ -11,6 +11,38 @@ import ( //go:generate stringer -type=State,OptionKind -linecomment -output stringers.go . +var ( + // errDropSegment is a flag that signals to drop a segment silently. + errDropSegment = errors.New("drop segment") + errWindowTooLarge = errors.New("invalid window size > 2**16") + + errTCBNotClosed = errors.New("TCB not closed") + errInvalidState = errors.New("invalid state") + errConnNotexist = errors.New("connection does not exist") + errConnectionClosing = errors.New("connection closing") + errExpectedSYN = errors.New("seqs:expected SYN") + errBadSegack = errors.New("seqs:bad segack") + errFinwaitExpectedACK = errors.New("seqs:finwait1 expected ACK") + errFinwaitExpectedFinack = errors.New("seqs:finwait2 expected FINACK") + + errWindowOverflow = newRejectErr("wnd > 2**16") + errSeqNotInWindow = newRejectErr("seq not in snd/rcv.wnd") + errZeroWindow = newRejectErr("zero window") + errLastNotInWindow = newRejectErr("last not in snd/rcv.wnd") + errRequireSequential = newRejectErr("seq != rcv.nxt (require sequential segments)") + errAckNotNext = newRejectErr("ack != snd.nxt") +) + +func newRejectErr(err string) *RejectError { return &RejectError{err: "reject in/out seg: " + err} } + +// RejectError represents an error that arises during admission of a segment into the +// Transmission Control Block logic in which the packet cannot be processed by the TCB. +type RejectError struct { + err string +} + +func (e *RejectError) Error() string { return e.err } + // Segment represents an incoming/outgoing TCP segment in the sequence space. type Segment struct { SEQ Value // sequence number of first octet of segment. If SYN is set it is the initial sequence number (ISN) and the first data octet is ISN+1. @@ -243,7 +275,12 @@ func (s State) IsSynchronized() bool { // IsDataOpen returns true if the connection allows sending and receiving of data. func (s State) isOpen() bool { - return s != StateClosed && s != StateTimeWait // TODO: is this api ok? + return !s.IsClosed() +} + +// hasIRS checks if the ControlBlock has received a valid initial sequence number (IRS). +func (s State) hasIRS() bool { + return s.isOpen() && s != StateSynSent && s != StateListen } type OptionKind uint8 diff --git a/lneto/tcp/internal_test.go b/lneto/tcp/internal_test.go new file mode 100644 index 0000000..ea998ff --- /dev/null +++ b/lneto/tcp/internal_test.go @@ -0,0 +1,193 @@ +package tcp + +import ( + "errors" + "fmt" + "testing" +) + +// Here we define internal testing helpers that may be used in any *_test.go file +// but are not exported. + +// Exchange represents a single exchange of segments. +type Exchange struct { + Outgoing *Segment + Incoming *Segment + WantPending *Segment // Expected pending segment. If nil not checked. + WantState State // Expected end state. + WantPeerState State // Expected end state of peer. Not necessary when calling HelperExchange but can aid with logging information. +} + +func (tcb *ControlBlock) HelperExchange(t *testing.T, exchange []Exchange) { + t.Helper() + var i int + var ex Exchange + defer func() { + if t.Failed() { + t.Errorf("exchange failed:\nwant: %s\ngot: %s", + ex.RFC9293String(ex.WantState, ex.WantPeerState), + ex.RFC9293String(tcb.state, ex.WantPeerState), + ) + } + }() + const pfx = "exchange" + t.Log(tcb.state, "Exchange start") + for i, ex = range exchange { + if ex.Outgoing != nil && ex.Incoming != nil { + t.Fatalf(pfx+"[%d] cannot send and receive in the same exchange, please split into two exchanges.", i) + } else if ex.Outgoing == nil && ex.Incoming == nil { + t.Fatalf(pfx+"[%d] must send or receive a segment.", i) + } + if ex.Outgoing != nil { + prevInflight := tcb.snd.inFlight() + err := tcb.Send(*ex.Outgoing) + gotSent := tcb.snd.inFlight() - prevInflight + if err != nil { + t.Fatalf(pfx+"[%d] snd: %s\nseg=%+v\nrcv=%+v\nsnd=%+v", i, err, *ex.Outgoing, tcb.rcv, tcb.snd) + } else if gotSent != ex.Outgoing.LEN() { + t.Fatalf(pfx+"[%d] snd: expected %d data sent, calculated inflight %d", i, ex.Outgoing.LEN(), gotSent) + } + } + if ex.Incoming != nil { + err := tcb.Recv(*ex.Incoming) + if err != nil { + msg := fmt.Sprintf(pfx+"[%d] rcv: %s\nseg=%+v\nrcv=%+v\nsnd=%+v", i, err, *ex.Incoming, tcb.rcv, tcb.snd) + if IsDroppedErr(err) { + t.Log(msg) + } else { + t.Fatal(msg) + } + } + } + + t.Log(ex.RFC9293String(tcb.state, ex.WantPeerState)) + + state := tcb.State() + if state != ex.WantState { + t.Errorf(pfx+"[%d] unexpected state:\n got=%s\nwant=%s", i, state, ex.WantState) + } + pending, ok := tcb.PendingSegment(0) + if !ok && ex.WantPending != nil { + t.Fatalf(pfx+"[%d] pending:got none, want=%+v", i, *ex.WantPending) + } else if ex.WantPending != nil && pending != *ex.WantPending { + t.Fatalf(pfx+"[%d] pending:\n got=%+v\nwant=%+v", i, pending, *ex.WantPending) + } else if ok && ex.WantPending == nil { + t.Fatalf(pfx+"[%d] pending:\n got=%+v\nwant=none", i, pending) + } + } +} + +func (tcb *ControlBlock) HelperInitState(state State, localISS, localNXT Value, localWindow Size) { + tcb.state = state + tcb.snd = sendSpace{ + ISS: localISS, + UNA: localISS, + NXT: localNXT, + WND: 1, // 1 byte window, so we can test the SEQ field. + // UP, WL1, WL2 defaults to zero values. + } + tcb.rcv = recvSpace{ + WND: localWindow, + } +} + +func (tcb *ControlBlock) HelperInitRcv(irs, nxt Value, remoteWindow Size) { + tcb.rcv.IRS = irs + tcb.rcv.NXT = nxt + tcb.snd.WND = remoteWindow +} + +func (tcb *ControlBlock) RelativeSendSpace() sendSpace { + snd := tcb.snd + snd.NXT -= snd.ISS + snd.UNA -= snd.ISS + snd.ISS = 0 + return snd +} + +func (tcb *ControlBlock) RelativeRecvSpace() recvSpace { + rcv := tcb.rcv + rcv.NXT -= rcv.IRS + rcv.IRS = 0 + return rcv +} + +func (tcb *ControlBlock) RelativeRecvSegment(seg Segment) Segment { + seg.SEQ -= tcb.rcv.IRS + seg.ACK -= tcb.snd.ISS + return seg +} + +func (tcb *ControlBlock) RelativeSendSegment(seg Segment) Segment { + seg.SEQ -= tcb.snd.ISS + seg.ACK -= tcb.rcv.IRS + return seg +} + +func (tcb *ControlBlock) RelativeAutoSegment(seg Segment) Segment { + rcv := tcb.RelativeRecvSegment(seg) + snd := tcb.RelativeSendSegment(seg) + if rcv.SEQ > snd.SEQ { + return snd + } + return rcv +} + +func (tcb *ControlBlock) HelperPrintSegment(t *testing.T, isReceive bool, seg Segment) { + const fmtmsg = "\nSeg=%+v\nRcvSpace=%s\nSndSpace=%s" + rcv := tcb.RelativeRecvSpace() + rcvStr := rcv.RelativeGoString() + snd := tcb.RelativeSendSpace() + sndStr := snd.RelativeGoString() + t.Helper() + if isReceive { + t.Logf("RECV:"+fmtmsg, seg.RelativeGoString(tcb.rcv.IRS, tcb.snd.ISS), rcvStr, sndStr) + } else { + t.Logf("SEND:"+fmtmsg, seg.RelativeGoString(tcb.snd.ISS, tcb.rcv.IRS), rcvStr, sndStr) + } +} + +func (rcv recvSpace) RelativeGoString() string { + return fmt.Sprintf("{NXT:%d} ", rcv.NXT-rcv.IRS) +} + +func (rcv sendSpace) RelativeGoString() string { + nxt := rcv.NXT - rcv.ISS + una := rcv.UNA - rcv.ISS + unaLen := Sizeof(una, nxt) + if unaLen != 0 { + return fmt.Sprintf("{NXT:%d UNA:%d} (%d unacked)", nxt, una, unaLen) + } + return fmt.Sprintf("{NXT:%d UNA:%d}", nxt, una) +} + +func (seg Segment) RelativeGoString(iseq, iack Value) string { + seglen := seg.LEN() + if seglen != seg.DATALEN { + // If SYN/FIN is set print out the length of the segment. + return fmt.Sprintf("{SEQ:%d ACK:%d DATALEN:%d Flags:%s} (LEN:%d)", seg.SEQ-iseq, seg.ACK-iack, seg.DATALEN, seg.Flags, seglen) + } + return fmt.Sprintf("{SEQ:%d ACK:%d DATALEN:%d Flags:%s} ", seg.SEQ-iseq, seg.ACK-iack, seg.DATALEN, seg.Flags) +} + +// https://datatracker.ietf.org/doc/html/rfc9293#section-3.8.6.2.1 +func (tcb *ControlBlock) UsableWindow() Size { + return Sizeof(tcb.snd.NXT, tcb.snd.UNA) + tcb.snd.WND +} + +func IsDroppedErr(err error) bool { + return err != nil && errors.Is(err, errDropSegment) +} + +func (ex *Exchange) RFC9293String(A, B State) string { + var seg Segment + sentByA := ex.Outgoing != nil + if sentByA { + seg = *ex.Outgoing + } else if ex.Incoming != nil { + seg = *ex.Incoming + } else { + return "" + } + return StringExchange(seg, A, B, !sentByA) +} diff --git a/lneto/tcp/tcp_test.go b/lneto/tcp/tcp_test.go new file mode 100644 index 0000000..fc1c52e --- /dev/null +++ b/lneto/tcp/tcp_test.go @@ -0,0 +1,912 @@ +package tcp_test + +import ( + "math/rand" + "strconv" + "testing" + + "github.com/soypat/tseq/lneto" + "github.com/soypat/tseq/lneto/tcp" +) + +const ( + SYNACK = tcp.FlagSYN | tcp.FlagACK + FINACK = tcp.FlagFIN | tcp.FlagACK + PSHACK = tcp.FlagPSH | tcp.FlagACK +) + +/* + Section 3.5 of RFC 9293: Basic 3-way handshake for connection synchronization. + TCP Peer A TCP Peer B + + 1. CLOSED LISTEN + + 2. SYN-SENT --> --> SYN-RECEIVED + + 3. ESTABLISHED <-- <-- SYN-RECEIVED + + 4. ESTABLISHED --> --> ESTABLISHED + + 5. ESTABLISHED --> --> ESTABLISHED +*/ +func TestExchange_rfc9293_figure6(t *testing.T) { + const issA, issB, windowA, windowB = 100, 300, 1000, 1000 + exchangeA := []tcp.Exchange{ + { // A sends SYN to B. + Outgoing: &tcp.Segment{SEQ: issA, Flags: tcp.FlagSYN, WND: windowA}, + WantState: tcp.StateSynSent, + WantPeerState: tcp.StateSynRcvd, + }, + { // A receives SYNACK from B thus establishing the connection on A's side. + Incoming: &tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: SYNACK, WND: windowB}, + WantState: tcp.StateEstablished, + WantPending: &tcp.Segment{SEQ: issA + 1, ACK: issB + 1, Flags: tcp.FlagACK, WND: windowA}, + WantPeerState: tcp.StateSynRcvd, + }, + { // A sends ACK to B, which leaves connection established on their side. Three way handshake complete by now. + Outgoing: &tcp.Segment{SEQ: issA + 1, ACK: issB + 1, Flags: tcp.FlagACK, WND: windowA}, + WantState: tcp.StateEstablished, + WantPeerState: tcp.StateEstablished, + }, + } + var tcbA tcp.ControlBlock + tcbA.HelperInitState(tcp.StateSynSent, issA, issA, windowA) + tcbA.HelperExchange(t, exchangeA) + segA, ok := tcbA.PendingSegment(0) + if ok { + t.Error("unexpected Client pending segment after establishment: ", segA) + } + exchangeB := reverseExchange(exchangeA) + + var tcbB tcp.ControlBlock + tcbB.HelperInitState(tcp.StateListen, issB, issB, windowB) + tcbB.HelperExchange(t, exchangeB) // TODO remove [:3] after snd.UNA bugfix + segB, ok := tcbB.PendingSegment(0) + if ok { + t.Error("unexpected Listener pending segment after establishment: ", segB) + } +} + +/* + Section 3.5 of RFC 9293: Simultaneous Connection Synchronization (SYN). + TCP Peer A TCP Peer B + + 1. CLOSED CLOSED + + 2. SYN-SENT --> ... + + 3. SYN-RECEIVED <-- <-- SYN-SENT + + 4. ... --> SYN-RECEIVED + + 5. SYN-RECEIVED --> ... + + 6. ESTABLISHED <-- <-- SYN-RECEIVED + + 7. ... --> ESTABLISHED +*/ +func TestExchange_rfc9293_figure7(t *testing.T) { + const issA, issB, windowA, windowB = 100, 300, 1000, 1000 + exchangeA := []tcp.Exchange{ + 0: { // A sends SYN to B. + Outgoing: &tcp.Segment{SEQ: issA, Flags: tcp.FlagSYN, WND: windowA}, + WantState: tcp.StateSynSent, + }, + 1: { // A receives a SYN with no ACK from B. + Incoming: &tcp.Segment{SEQ: issB, Flags: tcp.FlagSYN, WND: windowB}, + WantState: tcp.StateSynRcvd, + WantPending: &tcp.Segment{SEQ: issA, ACK: issB + 1, Flags: SYNACK, WND: windowA}, + }, + 2: { // A sends SYNACK to B. + Outgoing: &tcp.Segment{SEQ: issA, ACK: issB + 1, Flags: SYNACK, WND: windowA}, + WantState: tcp.StateSynRcvd, + }, + 3: { // A receives ACK from B. + Incoming: &tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: SYNACK, WND: windowA}, + WantState: tcp.StateEstablished, + }, + } + var tcbA tcp.ControlBlock + tcbA.HelperInitState(tcp.StateSynSent, issA, issA, windowA) + tcbA.HelperExchange(t, exchangeA) +} + +/* + Recovery from Old Duplicate SYN + TCP Peer A TCP Peer B + + 1. CLOSED LISTEN + + 2. SYN-SENT --> ... + + 3. (duplicate) ... --> SYN-RECEIVED + + 4. SYN-SENT <-- <-- SYN-RECEIVED + + 5. SYN-SENT --> --> LISTEN + + 6. ... --> SYN-RECEIVED + + 7. ESTABLISHED <-- <-- SYN-RECEIVED + + 8. ESTABLISHED --> --> ESTABLISHED +*/ +func TestExchange_rfc9293_figure8(t *testing.T) { + const issA, issB, windowA, windowB = 100, 300, 1000, 1000 + const issAold = 90 + const issBNew = issB + 100 + exchangeA := []tcp.Exchange{ + 0: { // A sends new SYN to B (which is not received). + Outgoing: &tcp.Segment{SEQ: issA, Flags: tcp.FlagSYN, WND: windowA}, + WantState: tcp.StateSynSent, + WantPeerState: tcp.StateSynRcvd, + }, + 1: { // Receive SYN from B acking an old "duplicate" SYN. + Incoming: &tcp.Segment{SEQ: issB, ACK: issAold + 1, Flags: SYNACK, WND: windowB}, + WantState: tcp.StateSynSent, + WantPending: &tcp.Segment{SEQ: issAold + 1, Flags: tcp.FlagRST, WND: windowA}, + WantPeerState: tcp.StateSynRcvd, + }, + 2: { // A sends RST to B and makes segment believable by using the old SEQ. + Outgoing: &tcp.Segment{SEQ: issAold + 1, Flags: tcp.FlagRST, WND: windowA}, + WantState: tcp.StateSynSent, + WantPeerState: tcp.StateListen, + }, + 3: { // A sends a duplicate SYN to B. + Outgoing: &tcp.Segment{SEQ: issA, Flags: tcp.FlagSYN, WND: windowA}, + WantState: tcp.StateSynSent, + WantPeerState: tcp.StateSynRcvd, + }, + 4: { // B SYNACKs new SYN. + Incoming: &tcp.Segment{SEQ: issBNew, ACK: issA + 1, Flags: SYNACK, WND: windowB}, + WantState: tcp.StateEstablished, + WantPending: &tcp.Segment{SEQ: issA + 1, ACK: issBNew + 1, Flags: tcp.FlagACK, WND: windowA}, + WantPeerState: tcp.StateSynRcvd, + }, + 5: { // B receives ACK from A. + Outgoing: &tcp.Segment{SEQ: issA + 1, ACK: issBNew + 1, Flags: tcp.FlagACK, WND: windowA}, + WantState: tcp.StateEstablished, + WantPeerState: tcp.StateEstablished, + }, + } + var tcbA tcp.ControlBlock + tcbA.HelperInitState(tcp.StateSynSent, issA, issA, windowA) + tcbA.HelperExchange(t, exchangeA) + + exchangeB := []tcp.Exchange{ + 0: { // B receives old SYN from A. + Incoming: &tcp.Segment{SEQ: issAold, Flags: tcp.FlagSYN, WND: windowA}, + WantState: tcp.StateSynRcvd, + WantPending: &tcp.Segment{SEQ: issB, ACK: issAold + 1, Flags: SYNACK, WND: windowB}, + }, + 1: { // B SYNACKs old SYN. + Outgoing: &tcp.Segment{SEQ: issB, ACK: issAold + 1, Flags: SYNACK, WND: windowB}, + WantState: tcp.StateSynRcvd, + }, + 2: { // B receives RST from A. + Incoming: &tcp.Segment{SEQ: issAold + 1, Flags: tcp.FlagRST, WND: windowA}, + WantState: tcp.StateListen, + }, + 3: { // B receives new SYN from A. + Incoming: &tcp.Segment{SEQ: issA, Flags: tcp.FlagSYN, WND: windowA}, + WantState: tcp.StateSynRcvd, + WantPending: &tcp.Segment{SEQ: issBNew, ACK: issA + 1, Flags: SYNACK, WND: windowB}, + }, + 4: { // B SYNACKs new SYN. + Outgoing: &tcp.Segment{SEQ: issBNew, ACK: issA + 1, Flags: SYNACK, WND: windowB}, + WantState: tcp.StateSynRcvd, + }, + 5: { // B receives ACK from A. + Incoming: &tcp.Segment{SEQ: issA + 1, ACK: issBNew + 1, Flags: tcp.FlagACK, WND: windowA}, + WantState: tcp.StateEstablished, + }, + } + var tcbB tcp.ControlBlock + tcbB.HelperInitState(tcp.StateListen, issB, issB, windowB) + tcbB.HelperExchange(t, exchangeB) +} + +/* + Figure 12: Normal Close Sequence + TCP Peer A TCP Peer B + 1. ESTABLISHED ESTABLISHED + + 2. (Close) + FIN-WAIT-1 --> --> CLOSE-WAIT + + 3. FIN-WAIT-2 <-- <-- CLOSE-WAIT + + 4. (Close) + TIME-WAIT <-- <-- LAST-ACK + + 5. TIME-WAIT --> --> CLOSED + + 6. (2 MSL) + CLOSED +*/ +func TestExchange_rfc9293_figure12(t *testing.T) { + const issA, issB, windowA, windowB = 100, 300, 1000, 1000 + exchangeA := []tcp.Exchange{ + 0: { // A sends FIN|ACK to B to begin closing connection. + Outgoing: &tcp.Segment{SEQ: issA, ACK: issB, Flags: FINACK, WND: windowA}, + WantState: tcp.StateFinWait1, + WantPeerState: tcp.StateCloseWait, + }, + 1: { // A receives ACK from B. + Incoming: &tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: tcp.FlagACK, WND: windowB}, + WantState: tcp.StateFinWait2, + WantPeerState: tcp.StateCloseWait, + // TODO(soypat): WantPending should be nil here? Perhaps fix test by modifying rcvFinWait1 pending result. + WantPending: &tcp.Segment{SEQ: issA + 1, ACK: issB, Flags: tcp.FlagACK, WND: windowA}, + }, + 2: { // A receives FIN|ACK from B. + Incoming: &tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: FINACK, WND: windowB}, + WantState: tcp.StateTimeWait, + WantPending: &tcp.Segment{SEQ: issA + 1, ACK: issB + 1, Flags: tcp.FlagACK, WND: windowA}, + WantPeerState: tcp.StateLastAck, + }, + 3: { // A sends ACK to B. + Outgoing: &tcp.Segment{SEQ: issA + 1, ACK: issB + 1, Flags: tcp.FlagACK, WND: windowA}, + WantState: tcp.StateTimeWait, // Technically we should be in TimeWait here. + WantPeerState: tcp.StateClosed, + }, + } + var tcbA tcp.ControlBlock + tcbA.HelperInitState(tcp.StateEstablished, issA, issA, windowA) + tcbA.HelperInitRcv(issB, issB, windowB) + tcbA.HelperExchange(t, exchangeA) + // tcbA.HelperExchange(t, exchangeA[:1]) + // tcbA.HelperExchange(t, exchangeA[1:2]) + // tcbA.HelperExchange(t, exchangeA[2:]) + + return + exchangeB := reverseExchange(exchangeA) + exchangeB[1].WantPending = &tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: FINACK, WND: windowB} + var tcbB tcp.ControlBlock + tcbB.HelperInitState(tcp.StateEstablished, issB, issB, windowB) + tcbB.HelperInitRcv(issA, issA, windowA) + tcbB.HelperExchange(t, exchangeB) +} + +/* + Figure 12: Simultaneous Close Sequence + TCP Peer A TCP Peer B + + 1. ESTABLISHED ESTABLISHED + + 2. (Close) (Close) + FIN-WAIT-1 --> ... FIN-WAIT-1 + <-- <-- + ... --> + + 3. CLOSING --> ... CLOSING + <-- <-- + ... --> + + 4. TIME-WAIT TIME-WAIT + (2 MSL) (2 MSL) + CLOSED CLOSED +*/ +func TestExchange_rfc9293_figure13(t *testing.T) { + const issA, issB, windowA, windowB = 100, 300, 1000, 1000 + exchangeA := []tcp.Exchange{ + 0: { // A sends FIN|ACK to B to begin closing connection. + Outgoing: &tcp.Segment{SEQ: issA, ACK: issB, Flags: FINACK, WND: windowA}, + WantState: tcp.StateFinWait1, + }, + 1: { // A receives FIN|ACK from B, who sent packet before receiving A's FINACK. + Incoming: &tcp.Segment{SEQ: issB, ACK: issA, Flags: FINACK, WND: windowB}, + WantState: tcp.StateClosing, + WantPending: &tcp.Segment{SEQ: issA + 1, ACK: issB + 1, Flags: tcp.FlagACK, WND: windowA}, + }, + 2: { // A sends ACK to B. + Outgoing: &tcp.Segment{SEQ: issA + 1, ACK: issB + 1, Flags: tcp.FlagACK, WND: windowA}, + WantState: tcp.StateTimeWait, + }, + } + var tcbA tcp.ControlBlock + tcbA.HelperInitState(tcp.StateEstablished, issA, issA, windowA) + tcbA.HelperInitRcv(issB, issB, windowB) + tcbA.HelperExchange(t, exchangeA) + + // No need to test B since exchange is completely symmetric. +} + +// Check no duplicate ack is sent during establishment. +func TestExchange_noDupAckDuringEstablished(t *testing.T) { + var tcbA tcp.ControlBlock + const issA, issB, windowA, windowB = 300, 334222749, 256, 64240 + err := tcbA.Open(issA, issA, tcp.StateSynSent) + tcbA.SetRecvWindow(windowA) + if err != nil { + t.Fatal(err) + } + establishA := []tcp.Exchange{ + 0: { // B sends SYN to A. + Incoming: &tcp.Segment{SEQ: issB, ACK: 0, WND: windowB, Flags: tcp.FlagSYN}, + WantPending: &tcp.Segment{SEQ: issA, ACK: issB + 1, WND: windowA, Flags: SYNACK}, + WantState: tcp.StateSynRcvd, + }, + 1: { // Send SYNACK to B. + Outgoing: &tcp.Segment{SEQ: issA, ACK: issB + 1, WND: windowA, Flags: SYNACK}, + WantState: tcp.StateSynRcvd, + }, + 2: { // B ACKs SYNACK, thus establishing the connection on both sides. + Incoming: &tcp.Segment{SEQ: issB + 1, ACK: issA + 1, WND: windowB, Flags: tcp.FlagACK}, + WantState: tcp.StateEstablished, + }, + } + tcbA.HelperExchange(t, establishA) + if tcbA.State() != tcp.StateEstablished { + t.Fatal("expected established state") + } + checkNoPending(t, &tcbA) + const datasize = 5 + dataExA := []tcp.Exchange{ + 0: { // B sends PSH|ACK to A with data. + Incoming: &tcp.Segment{SEQ: issB + 1, ACK: issA + 1, WND: windowB, Flags: PSHACK, DATALEN: datasize}, + WantPending: &tcp.Segment{SEQ: issA + 1, ACK: issB + 1 + datasize, WND: windowA, Flags: tcp.FlagACK}, + WantState: tcp.StateEstablished, + }, + 1: { // A ACKs B's data. + Outgoing: &tcp.Segment{SEQ: issA + 1, ACK: issB + 1 + datasize, WND: windowA, Flags: tcp.FlagACK}, + WantState: tcp.StateEstablished, + }, + 2: { // A sends PSH|ACK to B with data, same amount, as if echoing. + Outgoing: &tcp.Segment{SEQ: issA + 1, ACK: issB + 1 + datasize, WND: windowA, Flags: PSHACK, DATALEN: datasize}, + WantState: tcp.StateEstablished, + }, + // 3: { // B ACKs A's data. + // Incoming: &tcp.Segment{SEQ: issB + 1 + datasize, ACK: issA + 1 + datasize, WND: windowB, Flags: tcp.FlagACK}, + // WantPending: nil, + // WantState: tcp.StateEstablished, + // }, + } + tcbA.HelperExchange(t, dataExA) + checkNoPending(t, &tcbA) + tcbA.Recv(tcp.Segment{SEQ: issB + 1 + datasize, ACK: issA + 1 + datasize, WND: windowB, Flags: tcp.FlagACK}) + checkNoPending(t, &tcbA) +} + +// This test reenacts a full client-server interaction in the sending and receiving +// of the 12 byte message "hello world\n" over TCP. +func TestExchange_helloworld(t *testing.T) { + // Client Transmission Control Block. + var tcbA tcp.ControlBlock + const windowA, windowB = 502, 4096 + const issA, issB = 0x5e722b7d, 0xbe6e4c0f + const datalen = 12 + + exchangeA := []tcp.Exchange{ + 0: { // A sends SYN to B. + Outgoing: &tcp.Segment{SEQ: issA, Flags: tcp.FlagSYN, WND: windowA}, + WantState: tcp.StateSynSent, + WantPeerState: tcp.StateSynRcvd, + }, + 1: { // A receives SYNACK from B. + Incoming: &tcp.Segment{SEQ: issB, ACK: issA + 1, Flags: SYNACK, WND: windowB}, + WantState: tcp.StateEstablished, + WantPending: &tcp.Segment{SEQ: issA + 1, ACK: issB + 1, Flags: tcp.FlagACK, WND: windowA}, + WantPeerState: tcp.StateSynRcvd, + }, + 2: { // A sends ACK to B thus establishing connection. + Outgoing: &tcp.Segment{SEQ: issA + 1, ACK: issB + 1, Flags: tcp.FlagACK, WND: windowA}, + WantState: tcp.StateEstablished, + WantPeerState: tcp.StateEstablished, + }, + 3: { // A sends PSH|ACK to B with 12 byte message: "hello world\n" + Outgoing: &tcp.Segment{SEQ: issA + 1, ACK: issB + 1, Flags: PSHACK, WND: windowA, DATALEN: datalen}, + WantState: tcp.StateEstablished, + WantPeerState: tcp.StateEstablished, + }, + 4: { // A receives ACK from B of last message. + Incoming: &tcp.Segment{SEQ: issB + 1, ACK: issA + 1 + datalen, Flags: tcp.FlagACK, WND: windowB}, + WantState: tcp.StateEstablished, + WantPeerState: tcp.StateEstablished, + }, + 5: { // A receives PSH|ACK from B with echoed 12 byte message: "hello world\n" + Incoming: &tcp.Segment{SEQ: issB + 1, ACK: issA + 1 + datalen, Flags: PSHACK, WND: windowB, DATALEN: datalen}, + WantState: tcp.StateEstablished, + WantPending: &tcp.Segment{SEQ: issA + 1 + datalen, ACK: issB + 1 + datalen, Flags: tcp.FlagACK, WND: windowA}, + WantPeerState: tcp.StateEstablished, + }, + 6: { // A ACKs B's message. + Outgoing: &tcp.Segment{SEQ: issA + 1 + datalen, ACK: issB + 1 + datalen, Flags: tcp.FlagACK, WND: windowA}, + WantState: tcp.StateEstablished, + WantPeerState: tcp.StateEstablished, + }, + 7: { // A sends PSH|ACK to B with SECOND 12 byte message. + Outgoing: &tcp.Segment{SEQ: issA + 1 + datalen, ACK: issB + 1 + datalen, Flags: PSHACK, WND: windowA, DATALEN: datalen}, + WantState: tcp.StateEstablished, + WantPeerState: tcp.StateEstablished, + }, + 8: { // A receives PSH|ACK that acks last message and contains echoed of SECOND 12 byte message. + Incoming: &tcp.Segment{SEQ: issB + 1 + datalen, ACK: issA + 1 + 2*datalen, Flags: PSHACK, WND: windowB, DATALEN: datalen}, + WantState: tcp.StateEstablished, + WantPending: &tcp.Segment{SEQ: issA + 1 + 2*datalen, ACK: issB + 1 + 2*datalen, Flags: tcp.FlagACK, WND: windowA}, + WantPeerState: tcp.StateEstablished, + }, + 9: { // A ACKs B's SECOND message. + Outgoing: &tcp.Segment{SEQ: issA + 1 + 2*datalen, ACK: issB + 1 + 2*datalen, Flags: tcp.FlagACK, WND: windowA}, + WantState: tcp.StateEstablished, + WantPeerState: tcp.StateEstablished, + }, + 10: { // A sends FIN|ACK to B to close connection. + Outgoing: &tcp.Segment{SEQ: issA + 1 + 2*datalen, ACK: issB + 1 + 2*datalen, Flags: FINACK, WND: windowA}, + WantState: tcp.StateFinWait1, + WantPeerState: tcp.StateCloseWait, + }, + 11: { // A receives B's ACK of FIN. + Incoming: &tcp.Segment{SEQ: issB + 1 + 2*datalen, ACK: issA + 2 + 2*datalen, Flags: tcp.FlagACK, WND: windowB}, + WantState: tcp.StateFinWait2, + WantPending: &tcp.Segment{SEQ: issA + 2 + 2*datalen, ACK: issB + 1 + 2*datalen, Flags: tcp.FlagACK, WND: windowA}, + WantPeerState: tcp.StateCloseWait, + }, + } + // The client starts in the SYN_SENT state with a random sequence number. + gotServerSeg, _ := parseSegment(t, exchangeHelloWorld[0]) + tcbA.HelperInitState(tcp.StateSynSent, gotServerSeg.SEQ, gotServerSeg.SEQ, windowB) + tcbA.HelperExchange(t, exchangeA) + + // TODO(soypat): fix exchange reversal. + return + exchangeB := reverseExchange(exchangeA) + + exchangeB[7].WantPending = nil // Is an unpredicable action. + var tcbB tcp.ControlBlock + tcbB.HelperInitState(tcp.StateListen, issB, issB, windowB) + tcbB.HelperInitRcv(issA, issA, windowA) + tcbB.HelperExchange(t, exchangeB) +} + +func TestResetEstablished(t *testing.T) { + var tcb tcp.ControlBlock + const windowA, windowB = 502, 4096 + const issA, issB = 0x5e722b7d, 0xbe6e4c0f + tcb.HelperInitState(tcp.StateEstablished, issA, issA, windowA) + tcb.HelperInitRcv(issB, issB, windowB) + + err := tcb.Recv(tcp.Segment{SEQ: issB, ACK: issA, Flags: tcp.FlagRST, WND: windowB}) + if err == nil { + t.Fatal("expected error") + } + if tcb.State() != tcp.StateClosed { + t.Error("expected closed state; got ", tcb.State().String()) + } + checkNoPending(t, &tcb) +} + +func TestFinackClose(t *testing.T) { + var tcb tcp.ControlBlock + const windowA, windowB = 502, 4096 + const issA, issB = 100, 200 + tcb.HelperInitState(tcp.StateEstablished, issA, issA, windowA) + tcb.HelperInitRcv(issB, issB, windowB) + // Start closing process. + err := tcb.Close() + if err != nil { + t.Fatal(err) + } + seg, ok := tcb.PendingSegment(0) + if !ok { + t.Fatal("expected pending segment") + } + if !seg.Flags.HasAll(tcp.FlagFIN | tcp.FlagACK) { + t.Fatalf("expected FIN|ACK; got %s", seg.Flags.String()) + } + err = tcb.Send(seg) + if err != nil { + t.Fatal(err) + } + if tcb.State() != tcp.StateFinWait1 { + t.Fatalf("expected FinWait1; got %s", tcb.State().String()) + } + // Special case where we receive FINACK all together, we can streamline and go into TimeWait. + err = tcb.Recv(tcp.Segment{ + SEQ: issB, + ACK: issA + 1, + WND: windowB, + Flags: FINACK, + }) + if err != nil { + t.Fatal(err) + } + if tcb.State() != tcp.StateTimeWait { + t.Fatalf("expected TimeWait after FINACK; got %s", tcb.State().String()) + } +} + +func TestExchange_helloworld_client(t *testing.T) { + return + // Client Transmission Control Block. + var tcb tcp.ControlBlock + // The client starts in the SYN_SENT state with a random sequence number. + gotClientSeg, _ := parseSegment(t, exchangeHelloWorld[0]) + + // We add the SYN state to the client. + tcb.HelperInitState(tcp.StateSynSent, gotClientSeg.SEQ, gotClientSeg.SEQ, gotClientSeg.WND) + err := tcb.Send(gotClientSeg) + if err != nil { + + t.Fatal(err) + } + tcb.HelperPrintSegment(t, false, gotClientSeg) + + segString := func(seg tcp.Segment) string { + return tcb.RelativeAutoSegment(seg).RelativeGoString(0, 0) + } + for i, packet := range exchangeHelloWorld { + if i == 0 { + continue // we already processed first packet. + } + seg, payload := parseSegment(t, packet) + if seg.DATALEN > 0 { + t.Logf("seg[%d] <%s> payload: %q", i, tcb.State(), string(payload)) + } else { + t.Logf("seg[%d] <%s>", i, tcb.State()) + } + isClient := packet[0] == 0x28 + if isClient { + isPSH := seg.Flags&tcp.FlagPSH != 0 + gotClientSeg.Flags |= seg.Flags & (tcp.FlagPSH | tcp.FlagFIN) // Can't predict when client will send FIN. + if isPSH { + gotClientSeg.DATALEN = seg.DATALEN + } + + gotClientSeg.WND = seg.WND // Ignore window field, not a core part of control flow. + if gotClientSeg != seg { + t.Fatalf("client:\n got=%+v\nwant=%+v", segString(gotClientSeg), segString(seg)) + } + err := tcb.Send(gotClientSeg) + if err != nil { + t.Fatalf("incoming %s:\nseg[%d]=%s\nrcv=%+v\nsnd=%+v", err, i, segString(gotClientSeg), tcb.RelativeRecvSpace(), tcb.RelativeSendSpace()) + } + tcb.HelperPrintSegment(t, false, gotClientSeg) + continue // we only pass server packets to the client. + } + err = tcb.Recv(seg) + if err != nil { + t.Fatalf("%s:\nseg[%d]=%s\nrcv=%+v\nsnd=%+v", err, i, segString(seg), tcb.RelativeRecvSpace(), tcb.RelativeSendSpace()) + } + tcb.HelperPrintSegment(t, true, seg) + var ok bool + gotClientSeg, ok = tcb.PendingSegment(0) + if !ok { + t.Fatalf("[%d]: got no segment state=%s", i, tcb.State()) + } + } +} + +func parseSegment(t *testing.T, b []byte) (tcp.Segment, []byte) { + t.Helper() + efrm, err := lneto.NewEthFrame(b) + if err != nil { + t.Fatal(err) + } + if efrm.EtherTypeOrSize() != lneto.EtherTypeIPv4 { + t.Fatalf("not IPv4") + } + err = efrm.ValidateSize() + if err != nil { + t.Fatal(err) + } + ifrm, err := lneto.NewIPv4Frame(efrm.Payload()) + if err != nil { + t.Fatal(err) + } + if ifrm.Protocol() != 6 { + t.Fatalf("not TCP") + } + v, _ := ifrm.VersionAndIHL() + if v != 4 { + t.Fatal("invalid IP version", v) + } + err = ifrm.ValidateSize() + if err != nil { + t.Fatal(err) + } + + ipl := ifrm.Payload() + tfrm, err := lneto.NewTCPFrame(ipl) + if err != nil { + t.Fatal(err) + } else if err = tfrm.ValidateSize(); err != nil { + t.Fatal(err) + } + _ = tfrm.String() + payload := tfrm.Payload() + return tfrm.Segment(len(payload)), payload +} + +func reverseExchange(exchange []tcp.Exchange) []tcp.Exchange { + if len(exchange) == 0 { + panic("len(exchange) != len(states) or empty exchange: " + strconv.Itoa(len(exchange))) + } + firstIsIn := exchange[0].Incoming != nil + if firstIsIn { + panic("please start with an outgoing segment to reverse exchange for best test results") + } + out := make([]tcp.Exchange, len(exchange)) + for i := range exchange { + isLast := i == len(exchange)-1 + isOut := exchange[i].Outgoing != nil + out[i].WantState, out[i].WantPeerState = exchange[i].WantPeerState, exchange[i].WantState + if isOut { + out[i].Incoming = exchange[i].Outgoing + if !isLast { + out[i].WantPending = exchange[i+1].Incoming + } + } else { + out[i].Outgoing = exchange[i].Incoming + } + } + return out +} + +func checkNoPending(t *testing.T, tcb *tcp.ControlBlock) bool { + t.Helper() + // We extensively test the API for inadvertent state modification in a HasPending or PendingSegment call. + hasPD := tcb.HasPending() + pd, ok := tcb.PendingSegment(0) + hasPD2 := tcb.HasPending() + if hasPD || ok || hasPD2 { + t.Errorf("unexpected pending segment: %+v (%v,%v,%v)", pd, hasPD, ok, hasPD2) + return false + } + if hasPD != ok || hasPD != hasPD2 { + t.Fatalf("inconsistent pending segment: (%v,%v,%v)", hasPD, ok, hasPD2) + } + if !ok && pd != (tcp.Segment{}) { + t.Fatalf("inconsistent pending segment: %+v (%v,%v,%v)", pd, hasPD, ok, hasPD2) + } + return true +} + +// Full client-server interaction in the sending of "hello world" over TCP in order. +var exchangeHelloWorld = [][]byte{ + // client SYN1 + 0: []byte("\x28\xcd\xc1\x05\x4d\xbb\xd8\x5e\xd3\x43\x03\xeb\x08\x00\x45\x00\x00\x3c\x71\xac\x40\x00\x40\x06\x44\x9b\xc0\xa8\x01\x93\xc0\xa8\x01\x91\x84\x96\x04\xd2\x5e\x72\x2b\x7d\x00\x00\x00\x00\xa0\x02\xfa\xf0\x27\x6d\x00\x00\x02\x04\x05\xb4\x04\x02\x08\x0a\x07\x8b\x86\x4a\x00\x00\x00\x00\x01\x03\x03\x07"), + // server SYNACK + 1: []byte("\xd8\x5e\xd3\x43\x03\xeb\x28\xcd\xc1\x05\x4d\xbb\x08\x00\x45\x00\x00\x34\x00\x00\x40\x00\x40\x06\xb6\x4f\xc0\xa8\x01\x91\xc0\xa8\x01\x93\x04\xd2\x84\x96\xbe\x6e\x4c\x0f\x5e\x72\x2b\x7e\x80\x12\x10\x00\xc0\xbb\x00\x00\x02\x04\x05\xb4\x03\x03\x00\x04\x02\x00\x00\x00"), + // client ACK1 + 2: []byte("\x28\xcd\xc1\x05\x4d\xbb\xd8\x5e\xd3\x43\x03\xeb\x08\x00\x45\x00\x00\x28\x71\xad\x40\x00\x40\x06\x44\xae\xc0\xa8\x01\x93\xc0\xa8\x01\x91\x84\x96\x04\xd2\x5e\x72\x2b\x7e\xbe\x6e\x4c\x10\x50\x10\x01\xf6\x0b\x92\x00\x00"), + // client PSHACK0 + 3: []byte("\x28\xcd\xc1\x05\x4d\xbb\xd8\x5e\xd3\x43\x03\xeb\x08\x00\x45\x00\x00\x34\x71\xae\x40\x00\x40\x06\x44\xa1\xc0\xa8\x01\x93\xc0\xa8\x01\x91\x84\x96\x04\xd2\x5e\x72\x2b\x7e\xbe\x6e\x4c\x10\x50\x18\x01\xf6\x79\xa5\x00\x00\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x0a"), + // server ACK1 + 4: []byte("\xd8\x5e\xd3\x43\x03\xeb\x28\xcd\xc1\x05\x4d\xbb\x08\x00\x45\x00\x00\x28\x00\x00\x40\x00\x40\x06\xb6\x5b\xc0\xa8\x01\x91\xc0\xa8\x01\x93\x04\xd2\x84\x96\xbe\x6e\x4c\x10\x5e\x72\x2b\x8a\x50\x10\x0f\xf4\xfd\x87\x00\x00\x00\x00\x00\x00\x00\x00"), + // server PSHACK1 + 5: []byte("\xd8\x5e\xd3\x43\x03\xeb\x28\xcd\xc1\x05\x4d\xbb\x08\x00\x45\x00\x00\x34\x00\x00\x40\x00\x40\x06\xb6\x4f\xc0\xa8\x01\x91\xc0\xa8\x01\x93\x04\xd2\x84\x96\xbe\x6e\x4c\x10\x5e\x72\x2b\x8a\x50\x18\x10\x00\x6b\x8f\x00\x00\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x0a"), + // client ACK2 + 6: []byte("\x28\xcd\xc1\x05\x4d\xbb\xd8\x5e\xd3\x43\x03\xeb\x08\x00\x45\x00\x00\x28\x71\xaf\x40\x00\x40\x06\x44\xac\xc0\xa8\x01\x93\xc0\xa8\x01\x91\x84\x96\x04\xd2\x5e\x72\x2b\x8a\xbe\x6e\x4c\x1c\x50\x10\x01\xf6\x0b\x7a\x00\x00"), + // client PSHACK1 + 7: []byte("\x28\xcd\xc1\x05\x4d\xbb\xd8\x5e\xd3\x43\x03\xeb\x08\x00\x45\x00\x00\x34\x71\xb0\x40\x00\x40\x06\x44\x9f\xc0\xa8\x01\x93\xc0\xa8\x01\x91\x84\x96\x04\xd2\x5e\x72\x2b\x8a\xbe\x6e\x4c\x1c\x50\x18\x01\xf6\x79\x8d\x00\x00\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x0a"), + // server PSHACK2 + 8: []byte("\xd8\x5e\xd3\x43\x03\xeb\x28\xcd\xc1\x05\x4d\xbb\x08\x00\x45\x00\x00\x34\x00\x00\x40\x00\x40\x06\xb6\x4f\xc0\xa8\x01\x91\xc0\xa8\x01\x93\x04\xd2\x84\x96\xbe\x6e\x4c\x1c\x5e\x72\x2b\x96\x50\x18\x10\x00\x6b\x77\x00\x00\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x0a"), + // client ACK3 + 9: []byte("\x28\xcd\xc1\x05\x4d\xbb\xd8\x5e\xd3\x43\x03\xeb\x08\x00\x45\x00\x00\x28\x71\xb1\x40\x00\x40\x06\x44\xaa\xc0\xa8\x01\x93\xc0\xa8\x01\x91\x84\x96\x04\xd2\x5e\x72\x2b\x96\xbe\x6e\x4c\x28\x50\x10\x01\xf6\x0b\x62\x00\x00"), + // client FINACK + 10: []byte("\x28\xcd\xc1\x05\x4d\xbb\xd8\x5e\xd3\x43\x03\xeb\x08\x00\x45\x00\x00\x28\x71\xb2\x40\x00\x40\x06\x44\xa9\xc0\xa8\x01\x93\xc0\xa8\x01\x91\x84\x96\x04\xd2\x5e\x72\x2b\x96\xbe\x6e\x4c\x28\x50\x11\x01\xf6\x0b\x61\x00\x00"), + // server ACK + 11: []byte("\xd8\x5e\xd3\x43\x03\xeb\x28\xcd\xc1\x05\x4d\xbb\x08\x00\x45\x00\x00\x28\x00\x00\x40\x00\x40\x06\xb6\x5b\xc0\xa8\x01\x91\xc0\xa8\x01\x93\x04\xd2\x84\x96\xbe\x6e\x4c\x28\x5e\x72\x2b\x97\x50\x10\x10\x00\xfd\x56\x00\x00\x00\x00\x00\x00\x00\x00"), +} + +func TestUnexpectedStateClosing(t *testing.T) { + // TCB is a server which returns an HTTP response and receives a FINACK. + var tcb tcp.ControlBlock + const httpLen = 1192 + const issA, issB, windowA, windowB = 1, 127, 2000, 2000 + tcb.HelperInitState(tcp.StateEstablished, issA, issA, windowA) + tcb.HelperInitRcv(issB, issB, windowB) + + ex := []tcp.Exchange{ + 0: { // Server sends HTTP response. + Outgoing: &tcp.Segment{SEQ: issA, ACK: issB, Flags: PSHACK, WND: windowA, DATALEN: httpLen}, + WantState: tcp.StateEstablished, + }, + 1: { // Client sends an ACK to server. + Incoming: &tcp.Segment{SEQ: issB, ACK: issA + httpLen, Flags: tcp.FlagACK, WND: windowB}, + WantState: tcp.StateEstablished, + }, + 2: { // Client sends FIN|ACK to server. + Incoming: &tcp.Segment{SEQ: issB, ACK: issA + httpLen, Flags: FINACK, WND: windowB}, + WantPending: &tcp.Segment{SEQ: issA + httpLen, ACK: issB + 1, Flags: tcp.FlagACK, WND: windowA}, + WantState: tcp.StateCloseWait, + }, + 3: { // Server sends out FINACK. + Outgoing: &tcp.Segment{SEQ: issA + httpLen, ACK: issB + 1, Flags: FINACK, WND: windowA}, + WantState: tcp.StateLastAck, + }, + 4: { // Client sends back ACK. + Incoming: &tcp.Segment{SEQ: issB + 1, ACK: issA + httpLen + 1, Flags: tcp.FlagACK, WND: windowB}, + WantState: tcp.StateClosed, + }, + } + tcb.HelperExchange(t, ex[:]) +} + +// This corresponds to https://github.com/soypat/seqs/issues/19 +// The bug consisted of a panic condition encountered when using wget client with a seqs based server. +// Thanks to @knieriem for finding this and the detailed report they submitted. +func TestIssue19(t *testing.T) { + var tcb tcp.ControlBlock + assertState := func(state tcp.State) { + t.Helper() + if tcb.State() != state { + t.Fatalf("want state %s; got %s", state.String(), tcb.State().String()) + } + } + const httpLen = 1192 + const issA, issB, windowA, windowB = 1, 0, 2000, 2000 + tcb.HelperInitState(tcp.StateEstablished, issA, issA, windowA) + tcb.HelperInitRcv(issB, issB, windowB) + + // Send out HTTP request and close connection. + err := tcb.Send(tcp.Segment{SEQ: issA, ACK: issB, Flags: PSHACK, WND: windowA, DATALEN: httpLen}) + if err != nil { + t.Fatal(err) + } + err = tcb.Close() + if err != nil { + t.Fatal(err) + } + assertState(tcp.StateEstablished) + + pending, ok := tcb.PendingSegment(0) + if !ok { + t.Fatal("expected pending segment") + } else if pending.Flags != FINACK { + t.Fatalf("expected FINACK; got %s", pending.Flags.String()) + } + + // Receive ACK of HTTP segment. + err = tcb.Recv(tcp.Segment{SEQ: issB, ACK: issA + httpLen, Flags: tcp.FlagACK, WND: windowB}) + if err != nil { + t.Fatal(err) + } + assertState(tcp.StateEstablished) + err = tcb.Close() + if err != nil { + t.Fatal(err) + } + pending, ok = tcb.PendingSegment(0) + if !ok { + t.Fatal("expected pending segment") + } else if pending.Flags != FINACK { + t.Fatalf("expected FINACK; got %s", pending.Flags.String()) + } + + // Send out FINACK. + err = tcb.Send(pending) + if err != nil { + t.Fatal(err) + } + assertState(tcp.StateFinWait1) + + // Receive FINACK response from client. + err = tcb.Recv(tcp.Segment{SEQ: issB, ACK: issA + httpLen, Flags: FINACK, WND: windowB}) + if err != nil { + t.Fatal(err) + } + assertState(tcp.StateClosing) + pending, ok = tcb.PendingSegment(0) + if !ok { + t.Fatal("expected pending segment") + } else if pending.Flags != tcp.FlagACK { + t.Fatalf("expected ACK; got %s", pending.Flags.String()) + } + + // Before responding we receive an ACK from client. This is where panic is triggered. + err = tcb.Recv(tcp.Segment{SEQ: issB + 1, ACK: issA + httpLen + 1, Flags: tcp.FlagACK, WND: windowB}) + if err != nil { + t.Fatal(err) + } + assertState(tcp.StateTimeWait) + + // Check we still need to send an ACK. + pending, ok = tcb.PendingSegment(0) + if !ok { + t.Fatal("expected pending segment") + } else if pending.Flags != tcp.FlagACK { + t.Fatalf("expected ACK; got %s", pending.Flags.String()) + } + // Prepare response to client. + err = tcb.Send(pending) + if err != nil { + t.Fatal(err) + } +} + +func FuzzTCBActions(f *testing.F) { + const mtu = 2048 + const ( + actionRecv = iota + actionSend + actionClose + actionMax + ) + f.Add( + 0x2313_2313, + []byte{actionSend, actionRecv, actionSend, actionRecv, actionSend, actionRecv}, + ) + f.Add( + 0x2fefe_feefe, + []byte{actionSend, actionRecv, actionSend, actionClose, actionSend, actionRecv}, + ) + f.Add( + 0x2fefe_feefe, + []byte{actionClose, actionRecv, actionSend, actionClose, actionSend, actionRecv}, + ) + recvsendSize := func(rng *rand.Rand) int { + return rng.Int() % mtu + } + f.Fuzz(func(t *testing.T, seed int, actions []byte) { + if len(actions) == 0 || len(actions) > 100 { + t.SkipNow() + } + rng := rand.New(rand.NewSource(int64(seed))) + var clientISS tcp.Value = tcp.Value(rng.Int31()) + var serverISS tcp.Value = tcp.Value(rng.Int31()) + + var client tcp.ControlBlock + client.HelperInitState(tcp.StateEstablished, clientISS, clientISS, mtu) + client.HelperInitRcv(serverISS, serverISS, mtu) + + var server tcp.ControlBlock + server.HelperInitState(tcp.StateEstablished, serverISS, serverISS, mtu) + server.HelperInitRcv(clientISS, clientISS, mtu) + var closeCalled bool + // logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + // Level: slog.LevelDebug - 2, + // })) + // client.SetLogger(logger.WithGroup("client")) + // server.SetLogger(logger.WithGroup("server")) + // var exchanges []tcp.Exchange + // hasPanicked := true + // defer func() { + // if hasPanicked { + // for _, ex := range exchanges { + // t.Log(ex.RFC9293String(tcp.StateEstablished, tcp.StateEstablished)) + // } + // } + // }() + for _, action := range actions { + v := recvsendSize(rng) + switch action % actionMax { + case actionSend: + seg, ok := client.PendingSegment(v % mtu) + if ok { + // exchanges = append(exchanges, tcp.Exchange{Outgoing: &seg}) + err := client.Send(seg) + if err != nil { + panic(err) + } + err = server.Recv(seg) + if err != nil { + panic(err) + } + } + case actionRecv: + seg, ok := server.PendingSegment(v % mtu) + if ok { + // exchanges = append(exchanges, tcp.Exchange{Incoming: &seg}) + err := server.Send(seg) + if err != nil { + panic(err) + } + err = client.Recv(seg) + if err != nil && !closeCalled { + panic(err) + } + } + case actionClose: + err := client.Close() + if err != nil && !closeCalled { + panic(err) + } + closeCalled = true + return + } + } + // hasPanicked = false + }) +} diff --git a/lneto/validation.go b/lneto/validation.go index f763463..08ca8f7 100644 --- a/lneto/validation.go +++ b/lneto/validation.go @@ -3,16 +3,17 @@ package lneto import "errors" var ( - errShortEth = errors.New("ethernet length exceeds frame") - errShortVLAN = errors.New("ethernet length too short for VLAN") - errShortUDP = errors.New("UDP length exceeds frame") - errBadUDPLen = errors.New("UDP length invalid") - errShortIPv4 = errors.New("IPv4 total length exceeds frame") - errBadIPv4TL = errors.New("IPv4 short total length") - errShortIPv6 = errors.New("IPv6 payload length exceeds frame") - errShortARP = errors.New("bad ARP size") - errShortTCP = errors.New("TCP offset exceeds frame") - errBadTCPOff = errors.New("TCP offset invalid") + errShortEth = errors.New("ethernet length exceeds frame") + errShortVLAN = errors.New("ethernet length too short for VLAN") + errShortUDP = errors.New("UDP length exceeds frame") + errBadUDPLen = errors.New("UDP length invalid") + errShortIPv4 = errors.New("IPv4 total length exceeds frame") + errBadIPv4TL = errors.New("IPv4 short total length") + errBadIPv4IHL = errors.New("IPv4 bad IHL (<5)") + errShortIPv6 = errors.New("IPv6 payload length exceeds frame") + errShortARP = errors.New("bad ARP size") + errShortTCP = errors.New("TCP offset exceeds frame") + errBadTCPOff = errors.New("TCP offset invalid") ) // ValidateSize checks the frame's size fields and compares with the actual buffer @@ -54,11 +55,14 @@ func (ufrm UDPFrame) ValidateSize() error { // ValidateSize checks the frame's size fields and compares with the actual buffer // the frame. It returns a non-nil error on finding an inconsistency. func (ifrm IPv4Frame) ValidateSize() error { + ihl := ifrm.ihl() tl := ifrm.TotalLength() if tl < sizeHeaderIPv4 { return errBadIPv4TL } else if int(tl) > len(ifrm.RawData()) { return errShortIPv4 + } else if ihl < 5 { + return errBadIPv4IHL } return nil }