add tests for TCP and lneto along with TCB control logic

This commit is contained in:
soypat
2024-12-27 17:27:28 -03:00
parent 7f03c1c0a6
commit ba81dfac58
13 changed files with 2177 additions and 22 deletions
+7
View File
@@ -0,0 +1,7 @@
package internal
import "log/slog"
const (
LevelTrace slog.Level = slog.LevelDebug - 2
)
+60
View File
@@ -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
}
}
+19
View File
@@ -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...)
}
}
+13
View File
@@ -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
+58 -10
View File
@@ -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].
+237
View File
@@ -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)
}
+460 -1
View File
@@ -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
}
+107
View File
@@ -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
}
+59
View File
@@ -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)),
)
}
}
+38 -1
View File
@@ -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
+193
View File
@@ -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)
}
+912
View File
@@ -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 --> <SEQ=100><CTL=SYN> --> SYN-RECEIVED
3. ESTABLISHED <-- <SEQ=300><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED
4. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK> --> ESTABLISHED
5. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK><DATA> --> 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 --> <SEQ=100><CTL=SYN> ...
3. SYN-RECEIVED <-- <SEQ=300><CTL=SYN> <-- SYN-SENT
4. ... <SEQ=100><CTL=SYN> --> SYN-RECEIVED
5. SYN-RECEIVED --> <SEQ=100><ACK=301><CTL=SYN,ACK> ...
6. ESTABLISHED <-- <SEQ=300><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED
7. ... <SEQ=100><ACK=301><CTL=SYN,ACK> --> 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 --> <SEQ=100><CTL=SYN> ...
3. (duplicate) ... <SEQ=90><CTL=SYN> --> SYN-RECEIVED
4. SYN-SENT <-- <SEQ=300><ACK=91><CTL=SYN,ACK> <-- SYN-RECEIVED
5. SYN-SENT --> <SEQ=91><CTL=RST> --> LISTEN
6. ... <SEQ=100><CTL=SYN> --> SYN-RECEIVED
7. ESTABLISHED <-- <SEQ=400><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED
8. ESTABLISHED --> <SEQ=101><ACK=401><CTL=ACK> --> 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 --> <SEQ=100><ACK=300><CTL=FIN,ACK> --> CLOSE-WAIT
3. FIN-WAIT-2 <-- <SEQ=300><ACK=101><CTL=ACK> <-- CLOSE-WAIT
4. (Close)
TIME-WAIT <-- <SEQ=300><ACK=101><CTL=FIN,ACK> <-- LAST-ACK
5. TIME-WAIT --> <SEQ=101><ACK=301><CTL=ACK> --> 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 --> <SEQ=100><ACK=300><CTL=FIN,ACK> ... FIN-WAIT-1
<-- <SEQ=300><ACK=100><CTL=FIN,ACK> <--
... <SEQ=100><ACK=300><CTL=FIN,ACK> -->
3. CLOSING --> <SEQ=101><ACK=301><CTL=ACK> ... CLOSING
<-- <SEQ=301><ACK=101><CTL=ACK> <--
... <SEQ=101><ACK=301><CTL=ACK> -->
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
})
}
+14 -10
View File
@@ -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
}