Files
lneto/tcp/handler.go
T
Pat Whittingslow faae7ab076 TCP retransmission fixes and fuzz failure fixes (#59)
* fix for #58

* add fixes for both retransmit and fuzz failure found

* fix infinite challenge ack due to RST+SYN+FIN corrupt packet in SYNRCV state

* fuzz: previous recovery ack path led to infinite transmit

* fuzz: calculate CRCs when fuzzing to reduce search space to non-CRC error cases

* add local fuzz corpus to tests

* add more cases to fuzz corpus
2026-03-20 10:25:31 -03:00

535 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package tcp
import (
"io"
"net"
"log/slog"
"github.com/soypat/lneto"
"github.com/soypat/lneto/internal"
)
// Handler is a low level TCP handling data structure. It implements logic
// related to data buffering, frame sequencing and connection state handling.
// Does NOT implement IP related logic, so no CRC calculation/validation or pseudo header logic.
//
// See [Conn] for a higher level abstraction of a TCP connection, and see [ControlBlock] for the lower level bits of a TCP connection.
type Handler struct {
connid uint64
scb ControlBlock
bufTx ringTx
bufRx internal.Ring
logger
validator lneto.Validator
localPort uint16
remotePort uint16
// connid is a connection counter that is incremented each time a new
// connection is established via Open calls. This disambiguates whether
// Read and Write calls belong to the current connection.
optcodec OptionCodec
closing bool
// dupACKs counts consecutive duplicate ACKs for fast retransmit (RFC 5681 §3.2).
dupACKs uint8
// nRetx counts consecutive retransmissions for exponential backoff (RFC 6298 §5.5).
nRetx uint8
// Retransmission timer state — all uint32 milliseconds, no time package needed.
// rto is the current retransmission timeout in ms; starts at 1000 per RFC 6298 §2.1.
rto uint32
// now is the current time in ms, set by Conn before Send/Recv via SetNow.
now uint32
// lastACK is the last ACK value seen, for duplicate ACK detection (RFC 5681 §3.2).
lastACK Value
// retransmitNXT is the pre-rewind value of snd.NXT, saved when fast retransmit
// fires. A cumulative ACK with seg.ACK <= retransmitNXT is valid even if it
// exceeds the rewound snd.NXT. Zero means not in recovery.
retransmitNXT Value
}
func (h *Handler) SetLoggers(handler, scb *slog.Logger) {
h.logger.log = handler
h.scb.logger.log = scb
}
// ConnectionID returns the connection identifier which is incremented every time the connection is closed or open.
func (h *Handler) ConnectionID() *uint64 {
return &h.connid
}
// State returns the state of the TCP state machine as per RFC9293. See [State].
func (h *Handler) State() State { return h.scb.State() }
// SetBuffers sets the internal buffers used to receive and transmit bytes asynchronously via [Handler.Write] and [Handler.Read] calls.
// If the argument buffer is nil then the respective currently set buffer will be reused.
func (h *Handler) SetBuffers(txbuf, rxbuf []byte, packets int) error {
if h.bufRx.Buf == nil && (len(rxbuf) < minBufferSize || len(txbuf) < minBufferSize) {
return lneto.ErrShortBuffer
}
if !h.scb.State().IsClosed() {
return lneto.ErrInvalidConfig
}
if rxbuf != nil {
h.bufRx.Buf = rxbuf
}
h.scb.SetRecvWindow(Size(h.bufRx.Size()))
h.bufRx.Reset()
return h.bufTx.ResetOrReuse(txbuf, packets, 0)
}
// LocalPort returns the local port of the connection. Returns 0 if the connection is closed and uninitialized.
func (h *Handler) LocalPort() uint16 {
return h.localPort
}
// RemotePort returns the remote port of the connection if it is set.
// If the connection is passive and has not yet been established it will return 0.
func (h *Handler) RemotePort() uint16 {
return h.remotePort
}
// OpenActive opens an "active" TCP connection to a known remote port. The caller holds knowledge of the IP address.
// OpenActive is used by TCP Clients to initiate a connection.
func (h *Handler) OpenActive(localPort, remotePort uint16, iss Value) error {
if remotePort == 0 {
return lneto.ErrZeroDestination
} else if h.bufRx.Size() < minBufferSize || h.bufTx.Size() < minBufferSize {
return errBufferTooSmall
} else if h.scb.State() != StateClosed && h.scb.State() != StateTimeWait {
return errNeedClosedTCBToOpen
}
// reset/Abort prepares a SCB for active connection by resetting state to closed.
h.scb.reset()
h.reset(localPort, remotePort, iss)
h.scb.SetRecvWindow(Size(h.bufRx.Size()))
return nil
}
// OpenListen prepares a passive TCP connection where the Handler acts as a server.
// OpenListen is used by TCP Servers to begin listening for remote connections.
func (h *Handler) OpenListen(localPort uint16, iss Value) error {
if localPort == 0 {
return lneto.ErrZeroSource
} else if h.bufRx.Size() < minBufferSize || h.bufTx.Size() < minBufferSize {
return errBufferTooSmall
}
// Open will fail unless SCB in closed state.
err := h.scb.Open(iss, Size(h.bufRx.Size()))
if err != nil {
return err
}
h.reset(localPort, 0, iss)
return nil
}
// Abort forcibly terminates all state associated to current connection.
// After a call to abort no more data can be sent nor received over the connection.
func (h *Handler) Abort() {
h.info("tcp.Handler.Abort")
h.scb.Abort()
h.reset(0, 0, 0)
}
func (h *Handler) reset(localPort, remotePort uint16, iss Value) {
*h = Handler{
connid: h.connid + 1,
scb: h.scb,
bufTx: h.bufTx,
bufRx: h.bufRx,
localPort: localPort,
remotePort: remotePort,
validator: h.validator,
logger: h.logger,
closing: false,
rto: rtoInitial, // RFC 6298 §2.1: initial RTO = 1s.
}
h.bufTx.ResetOrReuse(nil, 0, iss)
h.bufRx.Reset()
}
const (
// rtoInitial is the initial RTO per RFC 6298 §2.1: "the sender SHOULD set RTO <- 1 second".
rtoInitial uint32 = 1000
// rtoMax caps exponential backoff per RFC 6298 §2.5.
rtoMax uint32 = 60_000
)
// Recv receives an incoming TCP packet frame with the first byte being the first octet of the TCP frame.
// The [Handler]'s internal state is updated if the packet is admitted successfully.
func (h *Handler) Recv(incomingPacket []byte) error {
if h.IsTxOver() {
return net.ErrClosed
}
tfrm, err := NewFrame(incomingPacket)
if err != nil {
return err
}
tfrm.ValidateExceptCRC(&h.validator)
err = h.validator.ErrPop()
if err != nil {
return err
}
remotePort := tfrm.SourcePort()
if h.remotePort != 0 && remotePort != h.remotePort {
return lneto.ErrMismatch
}
dstPort := tfrm.DestinationPort()
if h.localPort != dstPort {
return lneto.ErrMismatch
}
payload := tfrm.Payload()
if len(payload) > h.bufRx.Free() {
return lneto.ErrBufferFull
}
segIncoming := tfrm.Segment(len(payload))
if h.scb.IncomingIsKeepalive(segIncoming) {
h.info("tcp.Handler:rx-keepalive", slog.Uint64("port", uint64(h.localPort)))
return nil
}
prevState := h.scb.State()
prevUNA := h.scb.snd.UNA // Capture before Recv updates snd.UNA (RFC 6298 §5.3).
err = h.scb.Recv(segIncoming)
if err != nil {
// Recovery path: after fast retransmit rewinds snd.NXT, a cumulative ACK
// for data sent pre-rewind exceeds the rewound NXT. The ControlBlock rejects
// it, but we know it's valid if ACK <= retransmitNXT (pre-rewind high water mark).
if h.retransmitNXT != 0 && segIncoming.Flags.HasAny(FlagACK) &&
h.scb.snd.NXT.LessThan(segIncoming.ACK) &&
segIncoming.ACK.LessThanEq(h.retransmitNXT) {
// TODO: This is a very hacky workaround. It'd be great
// to detect recover acks in Handler before calling ControlBlock.Recv
// and handle it cleanly instead of with an error.
h.scb.RecoveryACK(segIncoming)
h.bufTx.RecoveryACK(segIncoming.ACK)
h.retransmitNXT = 0
h.rto = rtoInitial
h.nRetx = 0
h.dupACKs = 0
h.lastACK = segIncoming.ACK
err = nil // Accept the segment.
} else {
if h.scb.State() == StateClosed {
// TODO(soypat): Should return EOF/ErrClosed?
err = net.ErrClosed //err // Connection closed by reset.
}
return err
}
}
if h.scb.State() == StateClosed {
// TCB aborted, likely because it received an ACK in LastAck state.
// Clean up connection now unless read pending.
return net.ErrClosed
}
if prevState != h.scb.State() {
h.info("tcp.Handler:rx-statechange", slog.Uint64("port", uint64(h.localPort)), slog.String("old", prevState.String()), slog.String("new", h.scb.State().String()), slog.String("rxflags", segIncoming.Flags.String()))
}
if segIncoming.DATALEN != 0 {
_, err = h.bufRx.Write(payload)
if err != nil {
return err
}
}
if segIncoming.Flags.HasAny(FlagACK) {
// Update TX ring buffer to free up acked data.
h.bufTx.RecvACK(segIncoming.ACK)
// Dup-ACK tracking per RFC 5681 §3.2 and RTO reset per RFC 6298 §5.3.
if segIncoming.ACK != prevUNA && prevUNA.LessThan(segIncoming.ACK) {
// New data acknowledged — reset RTO and dup-ACK counter.
h.rto = rtoInitial // RFC 6298 §5.3.
h.nRetx = 0
h.dupACKs = 0
h.lastACK = segIncoming.ACK
} else if segIncoming.ACK == h.lastACK && segIncoming.DATALEN == 0 &&
!segIncoming.Flags.HasAny(FlagSYN|FlagFIN) && h.bufTx.BufferedSent() > 0 {
// Duplicate ACK per RFC 5681 §2: same ACK, no data, no SYN/FIN,
// and receiver has outstanding data.
h.dupACKs++
if h.dupACKs == 3 {
// RFC 5681 §3.2: "After receiving 3 duplicate ACKs [...]
// TCP performs a retransmission of what appears to be the
// missing segment, without waiting for the retransmission
// timer to expire."
h.triggerRetransmit()
}
}
}
if segIncoming.Flags.HasAny(FlagSYN) {
// Parse remote MSS from TCP options.
h.optcodec.ForEachOption(tfrm.Options(), func(kind OptionKind, data []byte) error {
if kind == OptMaxSegmentSize && len(data) == 2 {
mss := uint16(data[0])<<8 | uint16(data[1])
if mss > 0 {
h.scb.snd.MSS = Size(mss)
}
}
return nil
})
if h.remotePort == 0 {
// Remote reached out and has given us their port, set it on our side.
h.debug("tcp.Handler:rx-remoteport-set", slog.Uint64("port", uint64(h.localPort)), slog.Uint64("remoteport", uint64(remotePort)))
h.remotePort = remotePort
}
}
if h.logenabled(internal.LevelTrace) {
h.trace("tcp.Handler:rx-done",
slog.Uint64("lport", uint64(h.localPort)),
slog.Uint64("rport", uint64(remotePort)),
slog.Uint64("seg.seq", uint64(segIncoming.SEQ)),
slog.Uint64("seg.ack", uint64(segIncoming.ACK)),
slog.Uint64("seg.datalen", uint64(segIncoming.DATALEN)),
)
}
return nil
}
func (h *Handler) Close() error {
h.trace("tcp.Handler.Close")
if h.closing {
return errConnectionClosing
} else if h.State().IsClosed() {
return net.ErrClosed
}
h.closing = true
return nil
}
// Send writes TCP frame to be sent over the network to the remote peer to `b`.
// It does no IP interfacing or CRC calculation of packet, which is left to the caller to perform.
// The returned integer is the length written to the argument buffer.
func (h *Handler) Send(b []byte) (int, error) {
if h.IsTxOver() {
return 0, net.ErrClosed
}
awaitingSyn := h.AwaitingSynSend()
buffered := h.bufTx.BufferedUnsent()
if h.scb.State() == StateCloseWait && !h.closing && buffered == 0 && !h.scb.HasPending() {
// Remote closed with no application data left to send: initiate our own close.
// Checked here (not in Recv) so the application can still write in CLOSE-WAIT
// before Send is called, implementing the half-close per RFC 9293 §3.5.
h.closing = true
}
if !awaitingSyn && buffered == 0 && !h.closing && !h.scb.HasPending() {
// Early nop short circuit.
return 0, nil
}
tfrm, err := NewFrame(b)
if err != nil {
return 0, err
}
if buffered == 0 && h.closing && (h.scb.State() != StateCloseWait || !h.scb.HasPending()) {
// If Close called and no more data to be sent, terminate connection.
// In CLOSE-WAIT: wait until the pending ACK is sent first, since scb.Close()
// overwrites pending with [FIN|ACK] (unlike ESTABLISHED which merges via bitmask).
h.closing = false
err = h.scb.Close()
if err != nil {
h.logerr("tcp.Handler.Close", slog.String("err", errstr(err)), slog.String("state", h.State().String()))
h.Abort()
return 0, io.EOF
}
}
offset := uint8(5)
mss := uint16(len(b) - sizeHeaderTCP)
var segment Segment
if awaitingSyn {
// Handling init syn segment.
segment = ClientSynSegment(h.bufTx.iss, Size(h.bufRx.Size()))
h.optcodec.PutOption16(b[sizeHeaderTCP:], OptMaxSegmentSize, mss)
offset++
} else {
var ok bool
available := min(buffered, len(b)-sizeHeaderTCP)
segment, ok = h.scb.PendingSegment(available)
segment.WND = Size(h.bufRx.Free())
if !ok {
// No pending control segment or data to send. Yield.
return 0, nil
}
if segment.DATALEN > 0 {
n, err := h.bufTx.MakePacket(b[sizeHeaderTCP:sizeHeaderTCP+segment.DATALEN], segment.SEQ, h.now)
if err != nil {
return 0, err
} else if n != int(segment.DATALEN) {
panic("expected n == available")
}
} else if segment.Flags == synack {
h.optcodec.PutOption16(b[sizeHeaderTCP:], OptMaxSegmentSize, mss)
offset++
}
}
prevState := h.scb.State()
err = h.scb.Send(segment)
if err != nil {
return 0, err
} else if prevState != h.scb.State() && h.logenabled(slog.LevelInfo) {
h.info("tcp.Handler:tx-statechange", slog.Uint64("port", uint64(h.localPort)), slog.String("oldState", prevState.String()), slog.String("newState", h.scb.State().String()), slog.String("txflags", segment.Flags.String()))
}
tfrm.SetSourcePort(h.localPort)
tfrm.SetDestinationPort(h.remotePort)
tfrm.SetSegment(segment, offset)
tfrm.SetUrgentPtr(0)
datalen := int(offset)*4 + int(segment.DATALEN)
closedSuccess := prevState == StateTimeWait && segment.Flags.HasAny(FlagACK)
if closedSuccess {
h.reset(0, 0, 0)
}
return datalen, nil
}
// Write implements [io.Writer] by copying b to a internal buffer to be sent over the network on the next
// [Handler.Send] call that can send data to remote peer. Use [Handler.Free] to know the maximum length the argument slice can be before erroring.
func (h *Handler) Write(b []byte) (int, error) {
state := h.State()
if h.closing {
return 0, errConnectionClosing
} else if !state.TxDataOpen() { // Reject write call if data cannot be sent.
return 0, net.ErrClosed
}
return h.bufTx.Write(b)
}
// Read implements [io.Reader] by reading received data from remote peer in internal buffer.
func (h *Handler) Read(b []byte) (n int, err error) {
if h.bufRx.Buffered() > 0 {
n, err = h.bufRx.Read(b)
}
if n > 0 {
h.maybeQueueWindowUpdate()
}
if n == 0 && err == nil {
state := h.State()
if state.IsClosed() {
err = net.ErrClosed
} else if !state.RxDataOpen() {
err = io.EOF
}
}
return n, err
}
// maybeQueueWindowUpdate queues a window update ACK if the receive window has
// opened significantly since it was last advertised. This prevents zero-window
// deadlocks where the remote peer cannot send data because it thinks our window
// is still 0 after we've Read() data from the buffer.
//
// Per RFC 9293 §3.8.6.2.2 (SWS avoidance), the window is updated when freed
// space >= min(bufferSize/2, MSS). This applies uniformly including zero-window
// recovery — the remote uses zero-window probes until enough space opens.
func (h *Handler) maybeQueueWindowUpdate() {
currentFree := Size(h.bufRx.Free())
lastAdvertised := h.scb.RecvWindow()
if currentFree <= lastAdvertised {
return // Window hasn't grown.
}
thresh := Size(h.bufRx.Size()) / 2
if mss := h.scb.snd.MSS; mss > 0 && mss < thresh {
thresh = mss
}
if currentFree-lastAdvertised >= thresh {
h.scb.pending[0] |= FlagACK
}
}
// SizeOutput returns the total size of the transmit ring buffer.
func (h *Handler) SizeOutput() int {
return h.bufTx.Size()
}
// SizeInput returns the total size of the receive ring buffer.
func (h *Handler) SizeInput() int {
return h.bufRx.Size()
}
// BufferedInput returns the number of unread bytes in the receive buffer.
func (h *Handler) BufferedInput() int {
return h.bufRx.Buffered()
}
// BufferedUnsent returns the number of written but unsent bytes in the transmit buffer.
func (h *Handler) BufferedUnsent() int {
return h.bufTx.BufferedUnsent()
}
// FreeOutput returns the number of free bytes in the transmit buffer.
func (h *Handler) FreeOutput() int {
return h.bufTx.Free()
}
// FreeInput returns the number of free bytes in the receive buffer.
func (h *Handler) FreeInput() int {
return h.bufRx.Free()
}
// AwaitingSynResponse returns true if the Handler is an active client opened with [Handler.OpenActive] and has already sent out the first SYN packet to the remote client.
func (h *Handler) AwaitingSynResponse() bool {
return h.remotePort != 0 && h.scb.State() == StateSynSent
}
// AwaitingSynAck returns true if the Handler is a passive server opened with [Handler.OpenListen] and not yet received a valid SYN remote packet.
func (h *Handler) AwaitingSynAck() bool {
return h.remotePort == 0 && h.scb.State() == StateListen
}
// AwaitingSynSend returns true if the Handler is an active client opened with [Handler.OpenActive] and not yet sent out the first SYN packet to the remote client.
func (h *Handler) AwaitingSynSend() bool {
return h.remotePort != 0 && h.scb.State() == StateClosed
}
// IsTxOver returns true if there is no more frames to encapsulate over the network.
// The connection is pretty much over in this case if packets made it succesfully to remote.
func (h *Handler) IsTxOver() bool {
state := h.State()
return state == StateClosed && !h.AwaitingSynSend() ||
state == StateTimeWait && !h.scb.HasPending()
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
// SetNow sets the current time in milliseconds for retransmission timing.
// Must be called by Conn before Send/Recv operations.
func (h *Handler) SetNow(ms uint32) { h.now = ms }
// ShouldRetransmit returns true if the retransmission timeout has expired
// on the oldest unacknowledged segment. Per RFC 6298 §5.1 and §5.4.
func (h *Handler) ShouldRetransmit() bool {
oldest := h.bufTx.slist.Oldest()
if oldest == nil {
return false
}
return h.now-oldest.sentAt >= h.rto
}
// triggerRetransmit rewinds the transmit queue and control block so the next
// Send call retransmits from snd.UNA. Per RFC 9293 §3.10.8, RFC 6298 §5.45.5.
func (h *Handler) triggerRetransmit() {
// Save the high-water mark of NXT before rewinding so that cumulative ACKs
// for data sent pre-rewind can still be accepted (see Recv recovery path).
if h.retransmitNXT == 0 || h.retransmitNXT.LessThan(h.scb.snd.NXT) {
h.retransmitNXT = h.scb.snd.NXT
}
h.scb.Retransmit()
h.bufTx.RetransmitFromUNA()
// RFC 6298 §5.5: "The host MUST set RTO <- RTO * 2 ('back off the timer')."
h.nRetx++
h.rto *= 2
if h.rto > rtoMax {
h.rto = rtoMax
}
h.debug("tcp.Handler:retransmit", slog.Uint64("port", uint64(h.localPort)),
slog.Uint64("rto", uint64(h.rto)), slog.Uint64("nRetx", uint64(h.nRetx)))
}
func errstr(err error) string {
if err == nil {
return "<nil>"
}
return err.Error()
}