feat(tcp): implement window scaling (RFC 7323) (rebased) (#197)

* feat(tcp): implement window scaling (RFC 7323)

The window-scale option was parsed for length validation but never applied:
the effective receive window was capped at the 16-bit field, which caps
throughput at 65535/RTT (about 4MB/s on a 15ms path) no matter how large the
receive buffer is. Worse, a receive buffer over 64KiB silently WRAPPED the
advertised window on the SYN (uint16 truncation): a 128KiB buffer went on
the wire as a near-zero window.

Design: scaling lives purely at the wire seam in Handler. The ControlBlock
always holds real octet counts; conversion happens on frame read (peer
windows shifted up by the peer's offer, never on SYN segments) and frame
write (wireWnd: our shift down, SYN never scaled, saturation instead of
wrap when the value still does not fit). The local shift derives from the
receive buffer size in SetBuffers; every active SYN offers it (a zero
shift still lets the peer scale, RFC 7323 §2.5) and a SYN-ACK echoes it
only when the peer's SYN carried the option. The ControlBlock's three
2**16 window caps move to the scaled maximum (65535<<14).

Tests: on-wire negotiation with asymmetric buffers (shift values, unscaled
saturated SYN windows, first scaled advertisement, peer scaling back up);
a transfer proving more than 64KiB genuinely in flight without a single
ACK, received intact; wire-safety corners (no echo without an offer,
saturation not wrap, so the pre-existing 128KiB SYN wrap bug stays pinned).
Fuzzers clean: 9.1M TCB execs, 4.9M full-stack HTTP execs, 7.2M TCB
actions.

* use min instead of if statement

* omitting use of min saves 8 bytes

* fix option offset bug and add OptionCodec.Next method

---------

Co-authored-by: Derek den Haas <d.haas@directcode.com>
This commit is contained in:
Pat Whittingslow
2026-09-07 17:15:29 -03:00
committed by GitHub
parent 07afcfd924
commit 3eb1f39f1d
7 changed files with 380 additions and 61 deletions
+3 -4
View File
@@ -3,7 +3,6 @@ package tcp
import (
"io"
"log/slog"
"math"
"net"
"github.com/soypat/lneto/internal"
@@ -230,7 +229,7 @@ func (tcb *ControlBlock) Open(iss Value, wnd Size) (err error) {
switch {
case tcb._state != StateClosed && tcb._state != StateTimeWait:
err = errNeedClosedTCBToOpen
case wnd > math.MaxUint16:
case wnd > maxWindow:
err = errWindowTooLarge
}
if err != nil {
@@ -535,7 +534,7 @@ func (tcb *ControlBlock) validateOutgoingSegment(seg Segment) (err error) {
switch {
case tcb._state == StateClosed && !isFirst:
err = io.ErrClosedPipe
case seg.WND > math.MaxUint16:
case seg.WND > maxWindow:
err = errWindowTooLarge
case hasAck && seg.ACK != tcb.rcv.NXT:
err = errAckNotNext
@@ -578,7 +577,7 @@ func (tcb *ControlBlock) validateIncomingSegment(seg Segment) (err error) {
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:
case seg.WND > maxWindow:
err = errWindowOverflow
case tcb._state == StateClosed:
err = io.ErrClosedPipe