Files
lneto/tcp/options.go
T
Pat Whittingslow 3eb1f39f1d 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>
2026-09-07 13:15:29 -07:00

165 lines
5.5 KiB
Go

package tcp
import (
"strings"
"github.com/soypat/lneto"
)
type OptionKind uint8
const (
OptEnd OptionKind = iota // end of option list
OptNop // no-operation
OptMaxSegmentSize // maximum segment size
OptWindowScale // window scale
OptSACKPermitted // SACK permitted
OptSACK // SACK
OptEcho // echo(obsolete)
optEchoReply // echo reply(obsolete)
OptTimestamps // timestamps
optPOCP // partial order connection permitted(obsolete)
optPOSP // partial order service profile(obsolete)
optCC // CC(obsolete)
optCCnew // CC.new(obsolete)
optCCecho // CC.echo(obsolete)
optACR // alternate checksum request(obsolete)
optACD // alternate checksum data(obsolete)
optSkeeter // skeeter
optBubba // bubba
OptTrailerChecksum // trailer checksum
optMD5Signature // MD5 signature(obsolete)
OptSCPSCapabilities // SCPS capabilities
OptSNA // selective negative acks
OptRecordBoundaries // record boundaries
OptCorruptionExperienced // corruption experienced
OptSNAP // SNAP
OptUnassigned // unassigned
OptCompressionFilter // compression filter
OptQuickStartResponse // quick-start response
OptUserTimeout // user timeout or unauthorized use
OptAuthetication // Authentication TCP-AO
OptMultipath // multipath TCP
)
const (
OptFastOpenCookie OptionKind = 34 // fast open cookie
OptEncryptionNegotiation OptionKind = 69 // encryption negotiation
OptAccurateECN0 OptionKind = 172 // accurate ECN order 0
OptAccurateECN1 OptionKind = 174 // accurate ECN order 1
)
// IsObsolete returns true if option considered obsolete by newer TCP specifications.
func (kind OptionKind) IsObsolete() bool {
if kind.IsDefined() {
return strings.HasSuffix(kind.String(), "(obsolete)")
}
return false
}
// IsDefined returns true if the option is a known unreserved option kind.
func (kind OptionKind) IsDefined() bool {
return kind <= 30 || kind == 34 || kind == 69 || kind == 172 || kind == 174
}
type OptionCodec struct {
Flags OptionFlags
}
type OptionFlags uint8
const (
OptFlagSkipSizeValidation OptionFlags = 1 << iota
OptFlagSkipObsolete
)
func (flags OptionFlags) HasAny(ofTheseFlags OptionFlags) bool {
return flags&ofTheseFlags != 0
}
func (op OptionCodec) PutOption16(dst []byte, kind OptionKind, v uint16) (int, error) {
return op.PutOption(dst, kind, byte(v>>8), byte(v))
}
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>>8), byte(v))
}
func (op OptionCodec) PutOption(dst []byte, kind OptionKind, data ...byte) (int, error) {
putSize := 2 + len(data)
if len(dst) < putSize {
return -1, lneto.ErrShortBuffer
} else if putSize > 255 {
return -1, lneto.ErrInvalidLengthField
} else if kind == OptNop || kind == OptEnd {
return -1, lneto.ErrInvalidField
}
dst[0] = byte(kind)
dst[1] = byte(putSize)
copy(dst[2:], data)
return putSize, nil
}
// Next parses the next option in opts and returns it along with the remaining buffer.
// Will skip obsolete options and can validate given flags are set.
// Parser must stop calling Next after [OptEnd] returned.
func (op OptionCodec) Next(opts []byte) (kind OptionKind, optData, remainingOpts []byte, err error) {
REDO:
if len(opts) == 0 || opts[0] == 0 {
return OptEnd, nil, nil, 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
}