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 ( import (
"io" "io"
"log/slog" "log/slog"
"math"
"net" "net"
"github.com/soypat/lneto/internal" "github.com/soypat/lneto/internal"
@@ -230,7 +229,7 @@ func (tcb *ControlBlock) Open(iss Value, wnd Size) (err error) {
switch { switch {
case tcb._state != StateClosed && tcb._state != StateTimeWait: case tcb._state != StateClosed && tcb._state != StateTimeWait:
err = errNeedClosedTCBToOpen err = errNeedClosedTCBToOpen
case wnd > math.MaxUint16: case wnd > maxWindow:
err = errWindowTooLarge err = errWindowTooLarge
} }
if err != nil { if err != nil {
@@ -535,7 +534,7 @@ func (tcb *ControlBlock) validateOutgoingSegment(seg Segment) (err error) {
switch { switch {
case tcb._state == StateClosed && !isFirst: case tcb._state == StateClosed && !isFirst:
err = io.ErrClosedPipe err = io.ErrClosedPipe
case seg.WND > math.MaxUint16: case seg.WND > maxWindow:
err = errWindowTooLarge err = errWindowTooLarge
case hasAck && seg.ACK != tcb.rcv.NXT: case hasAck && seg.ACK != tcb.rcv.NXT:
err = errAckNotNext 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 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. // See section 3.4 of RFC 9293 for more on these checks.
switch { switch {
case seg.WND > math.MaxUint16: case seg.WND > maxWindow:
err = errWindowOverflow err = errWindowOverflow
case tcb._state == StateClosed: case tcb._state == StateClosed:
err = io.ErrClosedPipe err = io.ErrClosedPipe
+2 -2
View File
@@ -14,7 +14,7 @@ import (
var ( var (
errDropSegment error = lneto.ErrPacketDrop errDropSegment error = lneto.ErrPacketDrop
errWindowTooLarge = errors.New("invalid window size > 2**16") errWindowTooLarge = errors.New("invalid window size > max scaled window")
errBufferTooSmall error = lneto.ErrShortBuffer errBufferTooSmall error = lneto.ErrShortBuffer
errNeedClosedTCBToOpen = errors.New("need closed TCB to call open") errNeedClosedTCBToOpen = errors.New("need closed TCB to call open")
@@ -25,7 +25,7 @@ var (
errBadSegack = errors.New("seqs:bad segack") errBadSegack = errors.New("seqs:bad segack")
errFinwaitExpectedACK = errors.New("seqs:finwait1 expected ACK") errFinwaitExpectedACK = errors.New("seqs:finwait1 expected ACK")
errWindowOverflow = newRejectErr("wnd > 2**16") errWindowOverflow = newRejectErr("wnd > max scaled window")
errSeqNotInWindow = newRejectErr("seq not in snd/rcv.wnd") errSeqNotInWindow = newRejectErr("seq not in snd/rcv.wnd")
errZeroWindow = newRejectErr("zero window") errZeroWindow = newRejectErr("zero window")
errLastNotInWindow = newRejectErr("last not in snd/rcv.wnd") errLastNotInWindow = newRejectErr("last not in snd/rcv.wnd")
+73 -9
View File
@@ -29,6 +29,14 @@ type Handler struct {
// Read and Write calls belong to the current connection. // Read and Write calls belong to the current connection.
optcodec OptionCodec optcodec OptionCodec
// Window scaling (RFC 7323 §2). wndShiftLocal is derived from the receive
// buffer in [Handler.SetBuffers], wndShiftPeer learned from the peer's offer.
// Scaling lives at the wire seam only: the ControlBlock always holds real
// octet counts, converted on frame read ([Handler.Recv]) and write
// ([Handler.wireWnd]).
wndShiftLocal uint8
wndShiftPeer uint8
peerOfferedWS bool
// reasm tracks out-of-order segments staged in bufRx's free region. Always // reasm tracks out-of-order segments staged in bufRx's free region. Always
// enabled once buffers are set (see [Handler.SetBuffers]). // enabled once buffers are set (see [Handler.SetBuffers]).
reasm reassembly reasm reassembly
@@ -68,6 +76,7 @@ func (h *Handler) SetBuffers(txbuf, rxbuf []byte, packets int) error {
h.bufRx.Buf = rxbuf h.bufRx.Buf = rxbuf
} }
h.scb.SetRecvWindow(Size(h.bufRx.Size())) h.scb.SetRecvWindow(Size(h.bufRx.Size()))
h.wndShiftLocal = wndShiftFor(h.bufRx.Size())
h.bufRx.Reset() h.bufRx.Reset()
h.reasm.reset(maxReasmSegments) h.reasm.reset(maxReasmSegments)
return h.bufTx.ResetOrReuse(txbuf, packets, 0) return h.bufTx.ResetOrReuse(txbuf, packets, 0)
@@ -147,9 +156,10 @@ func (h *Handler) reset(localPort, remotePort uint16, iss Value) {
closing: false, closing: false,
shutdownRx: false, shutdownRx: false,
// Persist configuration across reopen: // Persist configuration across reopen:
validator: h.validator, validator: h.validator,
policy: h.policy, policy: h.policy,
logger: h.logger, logger: h.logger,
wndShiftLocal: h.wndShiftLocal, // derived from buffers, which persist too
// persist memory across repoen: // persist memory across repoen:
bufTx: h.bufTx, bufTx: h.bufTx,
bufRx: h.bufRx, bufRx: h.bufRx,
@@ -189,6 +199,12 @@ func (h *Handler) Recv(incomingPacket []byte) error {
} }
payload := tfrm.Payload() payload := tfrm.Payload()
segIncoming := tfrm.Segment(len(payload)) segIncoming := tfrm.Segment(len(payload))
if h.peerOfferedWS && !segIncoming.Flags.HasAny(FlagSYN) {
// Peer windows arrive scaled once both sides offered scaling, but never on
// SYN segments (RFC 7323 §2.2). Restore real octets before the
// ControlBlock sees them.
segIncoming.WND <<= h.wndShiftPeer
}
if h.scb.IncomingIsKeepalive(segIncoming) { if h.scb.IncomingIsKeepalive(segIncoming) {
h.info("tcp.Handler:rx-keepalive", slog.Uint64("port", uint64(h.localPort))) h.info("tcp.Handler:rx-keepalive", slog.Uint64("port", uint64(h.localPort)))
return nil return nil
@@ -266,6 +282,11 @@ func (h *Handler) Recv(incomingPacket []byte) error {
h.scb.snd.MSS = Size(mss) h.scb.snd.MSS = Size(mss)
} }
} }
if kind == OptWindowScale && len(data) == 1 {
// RFC 7323 §2.3: a shift above 14 is clamped, not rejected.
h.peerOfferedWS = true
h.wndShiftPeer = min(data[0], maxWndShift)
}
return nil return nil
}) })
if h.remotePort == 0 { if h.remotePort == 0 {
@@ -423,8 +444,7 @@ func (h *Handler) Send(b []byte) (int, error) {
if awaitingSyn || requeueControl && h.scb.State() == StateSynSent { if awaitingSyn || requeueControl && h.scb.State() == StateSynSent {
// Handling init syn segment. // Handling init syn segment.
segment = ClientSynSegment(h.bufTx.iss, Size(h.bufRx.Size())) segment = ClientSynSegment(h.bufTx.iss, Size(h.bufRx.Size()))
h.optcodec.PutOption16(b[optHead:], OptMaxSegmentSize, mss) offset += h.putSynOptions(b[optHead:], mss, false)
offset++
if requeueControl { if requeueControl {
h.info("tcp.Handler:requeue-syn", slog.Uint64("port", uint64(h.localPort)), slog.Uint64("rport", uint64(h.remotePort))) h.info("tcp.Handler:requeue-syn", slog.Uint64("port", uint64(h.localPort)), slog.Uint64("rport", uint64(h.remotePort)))
} }
@@ -435,8 +455,7 @@ func (h *Handler) Send(b []byte) (int, error) {
WND: Size(h.bufRx.Free()), WND: Size(h.bufRx.Free()),
Flags: synack, Flags: synack,
} }
h.optcodec.PutOption16(b[optHead:], OptMaxSegmentSize, mss) offset += h.putSynOptions(b[optHead:], mss, true)
offset++
h.info("tcp.Handler:requeue-synack", slog.Uint64("port", uint64(h.localPort)), slog.Uint64("rport", uint64(h.remotePort))) h.info("tcp.Handler:requeue-synack", slog.Uint64("port", uint64(h.localPort)), slog.Uint64("rport", uint64(h.remotePort)))
} else if requeueControl { } else if requeueControl {
h.requeueControl = false h.requeueControl = false
@@ -454,8 +473,7 @@ func (h *Handler) Send(b []byte) (int, error) {
// No pending control segment or data to send. Yield. // No pending control segment or data to send. Yield.
return 0, nil return 0, nil
} else if segment.Flags == synack { } else if segment.Flags == synack {
h.optcodec.PutOption16(b[optHead:], OptMaxSegmentSize, mss) offset += h.putSynOptions(b[optHead:], mss, true)
offset++
} else if segment.DATALEN > 0 { } else if segment.DATALEN > 0 {
n, err := h.bufTx.MakePacket(b[optHead:optHead+int(segment.DATALEN)], segment.SEQ) n, err := h.bufTx.MakePacket(b[optHead:optHead+int(segment.DATALEN)], segment.SEQ)
if err != nil { if err != nil {
@@ -477,6 +495,7 @@ func (h *Handler) Send(b []byte) (int, error) {
h.requeueControl = false h.requeueControl = false
tfrm.SetSourcePort(h.localPort) tfrm.SetSourcePort(h.localPort)
tfrm.SetDestinationPort(h.remotePort) tfrm.SetDestinationPort(h.remotePort)
segment.WND = h.wireWnd(segment) // wire representation only; scb keeps real octets
tfrm.SetSegment(segment, offset) tfrm.SetSegment(segment, offset)
tfrm.SetUrgentPtr(0) tfrm.SetUrgentPtr(0)
datalen := int(offset)*4 + int(segment.DATALEN) datalen := int(offset)*4 + int(segment.DATALEN)
@@ -623,6 +642,51 @@ func (h *Handler) recvWindow() Size {
return 0 return 0
} }
// maxWndShift is the RFC 7323 §2.2/§2.3 cap on the window-scale shift count and
// maxWindow the largest window it permits: the 16-bit wire field at that shift.
const (
maxWndShift = 14
maxWindow = 0xFFFF << maxWndShift
)
// wndShiftFor returns the smallest window-scale shift with which a receive
// buffer of bufSize octets can be advertised in the 16-bit window field.
func wndShiftFor(bufSize int) (shift uint8) {
for shift < maxWndShift && bufSize>>shift > 0xFFFF {
shift++
}
return shift
}
// putSynOptions writes the option block shared by SYN and SYN-ACK segments.
// MSS always, then the NOP-padded window-scale offer. An active SYN always
// offers scaling, since a zero shift still lets the peer scale its own window
// (RFC 7323 §2.5). A SYN-ACK echoes the offer only when the peer's SYN carried
// it (§2.2). Returns the number of 32-bit header words written.
func (h *Handler) putSynOptions(b []byte, mss uint16, isSynack bool) uint8 {
h.optcodec.PutOption16(b, OptMaxSegmentSize, mss)
words := uint8(1)
if (!isSynack || h.peerOfferedWS) && len(b) >= 8 {
b[4] = byte(OptNop)
h.optcodec.PutOption(b[5:], OptWindowScale, h.wndShiftLocal)
words++
}
return words
}
// wireWnd converts a segment's real window to its on-wire representation.
// SYN segments are never scaled (RFC7323 §2.2), we cap SYN windows at maxuint16.
func (h *Handler) wireWnd(seg Segment) Size {
wnd := seg.WND
if h.peerOfferedWS && !seg.Flags.HasAny(FlagSYN) {
wnd >>= h.wndShiftLocal
}
if wnd > 0xFFFF {
wnd = 0xFFFF
}
return wnd
}
// 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. // 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 { func (h *Handler) AwaitingSynResponse() bool {
return h.remotePort != 0 && h.scb.State() == StateSynSent return h.remotePort != 0 && h.scb.State() == StateSynSent
+61 -45
View File
@@ -82,7 +82,7 @@ func (op OptionCodec) PutOption16(dst []byte, kind OptionKind, v uint16) (int, e
} }
func (op OptionCodec) PutOption32(dst []byte, kind OptionKind, v uint32) (int, error) { func (op OptionCodec) PutOption32(dst []byte, kind OptionKind, v uint32) (int, error) {
return op.PutOption(dst, kind, byte(v>>24), byte(v>>16), byte(v>>7), byte(v)) return op.PutOption(dst, kind, byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
} }
func (op OptionCodec) PutOption(dst []byte, kind OptionKind, data ...byte) (int, error) { func (op OptionCodec) PutOption(dst []byte, kind OptionKind, data ...byte) (int, error) {
@@ -100,49 +100,65 @@ func (op OptionCodec) PutOption(dst []byte, kind OptionKind, data ...byte) (int,
return putSize, nil return putSize, nil
} }
func (op OptionCodec) ForEachOption(opts []byte, fn func(OptionKind, []byte) error) error { // Next parses the next option in opts and returns it along with the remaining buffer.
off := 0 // Will skip obsolete options and can validate given flags are set.
skipSizeValidation := op.Flags.HasAny(OptFlagSkipSizeValidation) // Parser must stop calling Next after [OptEnd] returned.
skipObsolete := op.Flags.HasAny(OptFlagSkipObsolete) func (op OptionCodec) Next(opts []byte) (kind OptionKind, optData, remainingOpts []byte, err error) {
for off < len(opts) && opts[off] != 0 { REDO:
kind := OptionKind(opts[off]) if len(opts) == 0 || opts[0] == 0 {
off++ return OptEnd, nil, nil, nil
if kind == OptNop {
continue
}
if len(opts[off:]) < 1 {
return lneto.ErrTruncatedFrame
}
size := int(opts[off]) // Total option length including kind and length bytes.
off++
dataLen := size - 2 // Data bytes after kind and length.
if dataLen < 0 || len(opts[off:]) < dataLen {
return lneto.ErrTruncatedFrame
}
if !skipSizeValidation {
expectSize := -1
switch kind {
case OptTimestamps:
expectSize = 10
case OptMaxSegmentSize, OptUserTimeout:
expectSize = 4
case OptWindowScale:
expectSize = 3
case OptSACKPermitted:
expectSize = 2
}
if expectSize != -1 && size != expectSize {
return lneto.ErrInvalidLengthField
}
}
if !(skipObsolete && kind.IsObsolete()) {
err := fn(kind, opts[off:off+dataLen])
if err != nil {
return err
}
}
off += dataLen
} }
return nil var size int
kind = OptionKind(opts[0])
if kind == OptNop {
return kind, nil, opts[1:], nil
} else if len(opts) == 1 {
return kind, nil, nil, lneto.ErrTruncatedFrame
}
size = int(opts[1])
if size > len(opts) {
return kind, nil, nil, lneto.ErrTruncatedFrame
} else if size < 2 {
return kind, nil, nil, lneto.ErrInvalidLengthField
}
optData = opts[2:size]
remainingOpts = opts[size:]
if op.Flags.HasAny(OptFlagSkipObsolete) && kind.IsObsolete() {
opts = remainingOpts
goto REDO
}
if !op.Flags.HasAny(OptFlagSkipSizeValidation) {
var expectSize int
switch kind {
case OptTimestamps:
expectSize = 10
case OptMaxSegmentSize, OptUserTimeout:
expectSize = 4
case OptWindowScale:
expectSize = 3
case OptSACKPermitted:
expectSize = 2
}
if expectSize != 0 && size != expectSize {
err = lneto.ErrInvalidLengthField
}
}
return kind, optData, remainingOpts, err
}
// ForEachOption calls fn on all non-End/Nop options in opts. Will skip obsolete options if flag set.
func (op OptionCodec) ForEachOption(opts []byte, fn func(OptionKind, []byte) error) (err error) {
var kind OptionKind = 1
var data []byte
for kind != 0 {
kind, data, opts, err = op.Next(opts)
if err != nil {
break
} else if kind <= OptNop {
continue
} else if err = fn(kind, data); err != nil {
break
}
}
return err
} }
+3
View File
@@ -14,11 +14,14 @@ type Policy interface {
// PreTx is called before writing to a frame. // PreTx is called before writing to a frame.
// The outgoing frame options can be set by the Policy and will be respected if Frame offset >5. // The outgoing frame options can be set by the Policy and will be respected if Frame offset >5.
// Keep in mind Handler will add options PreTx already added, these options are best overwritten in PostTx.
// retransmitFrom is ignored unless within [snd.UNA, snd.NXT] and returned retransmit==true. // retransmitFrom is ignored unless within [snd.UNA, snd.NXT] and returned retransmit==true.
// newTransmitLimit sets the maximum number of new bytes to send over the wire (congestion control). // newTransmitLimit sets the maximum number of new bytes to send over the wire (congestion control).
// If not implementing congestion control then newTransmitLimit=[TransmitUnlimited]. // If not implementing congestion control then newTransmitLimit=[TransmitUnlimited].
PreTx(h *Handler, outgoingOpts Frame) (newTransmitLimit Size, retransmitFrom Value, retransmit bool) PreTx(h *Handler, outgoingOpts Frame) (newTransmitLimit Size, retransmitFrom Value, retransmit bool)
// PostTx called on leaving the transmit path with the fully written frame. // PostTx called on leaving the transmit path with the fully written frame.
// PostTx can strategically overwrite options normally set by Handler like MSS, Window scaling which
// ends up being more ergonomic than adding them in PreTx and then de-duplicating them in PostTx.
PostTx(h *Handler, outgoing Frame) PostTx(h *Handler, outgoing Frame)
// PreRx is called by [Handler] on every incoming segment. // PreRx is called by [Handler] on every incoming segment.
+1 -1
View File
@@ -329,7 +329,7 @@ func TestWindowReject_ChallengeACK(t *testing.T) {
SEQ: issB + 512, SEQ: issB + 512,
ACK: issA, ACK: issA,
Flags: FlagACK, Flags: FlagACK,
WND: Size(1 << 17), // > MaxUint16. WND: Size(maxWindow + 1), // beyond even the largest scaled window (RFC 7323).
} }
err := tcb.Recv(seg) err := tcb.Recv(seg)
if err == nil { if err == nil {
+237
View File
@@ -0,0 +1,237 @@
package tcp
import (
"bytes"
"math/rand"
"testing"
)
// synOptions parses the option block of a raw TCP frame and returns the MSS
// and window-scale values found, with ok flags for presence.
func synOptions(t *testing.T, frame []byte) (mss uint16, mssOK bool, shift uint8, shiftOK bool) {
t.Helper()
tfrm, err := NewFrame(frame)
if err != nil {
t.Fatal(err)
}
var oc OptionCodec
err = oc.ForEachOption(tfrm.Options(), func(kind OptionKind, data []byte) error {
switch {
case kind == OptMaxSegmentSize && len(data) == 2:
mss = uint16(data[0])<<8 | uint16(data[1])
mssOK = true
case kind == OptWindowScale && len(data) == 1:
shift = data[0]
shiftOK = true
}
return nil
})
if err != nil {
t.Fatal(err)
}
return mss, mssOK, shift, shiftOK
}
// TestWindowScaleNegotiation walks the three-way handshake with asymmetric
// buffer sizes and verifies RFC 7323 §2 end to end on the wire: the SYN and
// SYN-ACK carry the shift derived from each side's receive buffer, SYN
// windows are never scaled (and saturate rather than wrap the 16-bit field),
// and the first post-handshake segment advertises the scaled window.
func TestWindowScaleNegotiation(t *testing.T) {
const clientBuf = 256 << 10 // shift 3: 256KiB>>3 = 32Ki fits, >>2 does not.
const serverBuf = 1 << 20 // shift 5: 1MiB>>5 = 32Ki fits, >>4 does not.
rng := rand.New(rand.NewSource(1))
client, server := new(Handler), new(Handler)
err := client.SetBuffers(make([]byte, 2048), make([]byte, clientBuf), 32)
if err != nil {
t.Fatal(err)
}
err = server.SetBuffers(make([]byte, 2048), make([]byte, serverBuf), 32)
if err != nil {
t.Fatal(err)
}
setupClientServer(t, rng, client, server)
packetBuf := make([]byte, 2048)
// Client SYN: window-scale offer present, window field unscaled+saturated.
n, err := client.Send(packetBuf)
if err != nil {
t.Fatal(err)
}
_, mssOK, shift, shiftOK := synOptions(t, packetBuf[:n])
if !mssOK {
t.Fatal("client SYN lacks MSS option")
}
if !shiftOK {
t.Fatal("client SYN lacks window-scale option")
}
if shift != 3 {
t.Errorf("client SYN shift = %d, want 3 (buffer %d)", shift, clientBuf)
}
tfrm, _ := NewFrame(packetBuf[:n])
if got := tfrm.WindowSize(); got != 0xFFFF {
t.Errorf("client SYN wire window = %d, want 65535 (saturated, never scaled)", got)
}
if err = server.Recv(packetBuf[:n]); err != nil {
t.Fatal(err)
}
// Server SYN-ACK: echoes its own shift because the SYN offered scaling.
clear(packetBuf)
n, err = server.Send(packetBuf)
if err != nil {
t.Fatal(err)
}
_, _, shift, shiftOK = synOptions(t, packetBuf[:n])
if !shiftOK {
t.Fatal("server SYN-ACK lacks window-scale option")
}
if shift != 5 {
t.Errorf("server SYN-ACK shift = %d, want 5 (buffer %d)", shift, serverBuf)
}
tfrm, _ = NewFrame(packetBuf[:n])
if got := tfrm.WindowSize(); got != 0xFFFF {
t.Errorf("server SYN-ACK wire window = %d, want 65535 (saturated, never scaled)", got)
}
if err = client.Recv(packetBuf[:n]); err != nil {
t.Fatal(err)
}
// Client handshake ACK: the first scaled window on the wire. The client
// advertises its whole free buffer, which only fits the field when
// right-shifted by its offered shift.
clear(packetBuf)
n, err = client.Send(packetBuf)
if err != nil {
t.Fatal(err)
}
tfrm, _ = NewFrame(packetBuf[:n])
wantWire := uint16(clientBuf >> 3)
if got := tfrm.WindowSize(); got != wantWire {
t.Errorf("client ACK wire window = %d, want %d (%d >> 3)", got, wantWire, clientBuf)
}
if err = server.Recv(packetBuf[:n]); err != nil {
t.Fatal(err)
}
// The server must have scaled the advertisement back up to real octets.
if got := server.scb.snd.WND; got != Size(clientBuf) {
t.Errorf("server snd.WND = %d, want %d (scaled back up)", got, clientBuf)
}
}
// TestWindowScaleBigTransfer proves the negotiated scale carries real data
// past the unscaled 64KiB ceiling: with 256KiB buffers on both sides the
// server streams segments without receiving a single ACK, and must be able
// to put more than 64KiB in flight before stalling on the send window. The
// client then receives everything intact.
func TestWindowScaleBigTransfer(t *testing.T) {
const bufSize = 256 << 10
const payload = 200 << 10
const mtu = 2048
rng := rand.New(rand.NewSource(2))
client, server := new(Handler), new(Handler)
err := client.SetBuffers(make([]byte, mtu), make([]byte, bufSize), 32)
if err != nil {
t.Fatal(err)
}
err = server.SetBuffers(make([]byte, bufSize), make([]byte, bufSize), 256)
if err != nil {
t.Fatal(err)
}
setupClientServer(t, rng, client, server)
packetBuf := make([]byte, mtu)
establish(t, client, server, packetBuf)
data := make([]byte, payload)
rng.Read(data)
nw, err := server.Write(data)
if err != nil {
t.Fatal(err)
} else if nw != payload {
t.Fatalf("server buffered %d of %d", nw, payload)
}
// Stream server→client WITHOUT delivering anything back: no ACKs, so
// everything sent stays in flight. Past 64KiB in flight is the proof
// that the scaled window governs the sender.
inFlight := 0
frames := make([][]byte, 0, payload/1024)
for {
clear(packetBuf)
n, err := server.Send(packetBuf)
if err != nil {
t.Fatal(err)
}
if n <= sizeHeaderTCP {
break // window exhausted (or nothing left to send).
}
tfrm, _ := NewFrame(packetBuf[:n])
inFlight += len(tfrm.Payload())
frames = append(frames, append([]byte(nil), packetBuf[:n]...))
if inFlight >= payload {
break
}
}
if inFlight <= 0xFFFF {
t.Fatalf("server stalled at %d bytes in flight; scaled window should allow more than 65535", inFlight)
}
// Deliver the flight; the client must reassemble the stream intact.
for _, frm := range frames {
if err := client.Recv(frm); err != nil {
t.Fatal(err)
}
}
got := make([]byte, inFlight)
nr, err := client.Read(got)
if err != nil {
t.Fatal(err)
}
if nr != inFlight {
t.Fatalf("client read %d of %d in-flight bytes", nr, inFlight)
}
if !bytes.Equal(got[:nr], data[:nr]) {
t.Fatal("received data differs from sent data")
}
}
// TestWindowScaleWireSafety covers the wire-conversion corners without a
// peer: no echo of the offer when the peer never gave one, and saturation
// (not wrap-around) of oversized windows for both SYN and non-SYN segments
// when scaling is off. Before window scaling existed a 128KiB receive buffer
// wrapped to a near-zero wire window on the SYN; that regression stays pinned
// here.
func TestWindowScaleWireSafety(t *testing.T) {
h := new(Handler)
err := h.SetBuffers(make([]byte, 2048), make([]byte, 128<<10), 32)
if err != nil {
t.Fatal(err)
}
if h.wndShiftLocal != 2 {
// 128KiB>>1 = 65536 still overflows the field; >>2 = 32768 fits.
t.Errorf("wndShiftLocal = %d, want 2 for 128KiB buffer", h.wndShiftLocal)
}
var b [16]byte
if words := h.putSynOptions(b[:], 1460, true); words != 1 {
t.Errorf("SYN-ACK echoed window scale without a peer offer (words=%d)", words)
}
if words := h.putSynOptions(b[:], 1460, false); words != 2 {
t.Errorf("active SYN did not offer window scale (words=%d)", words)
}
// Scaling off: oversized windows saturate the 16-bit field.
if got := h.wireWnd(Segment{WND: 128 << 10, Flags: FlagACK}); got != 0xFFFF {
t.Errorf("unscaled oversized window = %d, want 65535", got)
}
// SYN never scales, even with a negotiated peer shift.
h.peerOfferedWS = true
if got := h.wireWnd(Segment{WND: 128 << 10, Flags: FlagSYN}); got != 0xFFFF {
t.Errorf("SYN window = %d, want 65535 (saturated, unscaled)", got)
}
// Established segment with negotiated scaling: shifted representation.
if got := h.wireWnd(Segment{WND: 128 << 10, Flags: FlagACK}); got != (128<<10)>>2 {
t.Errorf("scaled window = %d, want %d", got, (128<<10)>>2)
}
}