mirror of
https://github.com/soypat/lneto.git
synced 2026-09-08 15:59:10 +00:00
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:
+61
-45
@@ -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) {
|
||||
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) {
|
||||
@@ -100,49 +100,65 @@ func (op OptionCodec) PutOption(dst []byte, kind OptionKind, data ...byte) (int,
|
||||
return putSize, nil
|
||||
}
|
||||
|
||||
func (op OptionCodec) ForEachOption(opts []byte, fn func(OptionKind, []byte) error) error {
|
||||
off := 0
|
||||
skipSizeValidation := op.Flags.HasAny(OptFlagSkipSizeValidation)
|
||||
skipObsolete := op.Flags.HasAny(OptFlagSkipObsolete)
|
||||
for off < len(opts) && opts[off] != 0 {
|
||||
kind := OptionKind(opts[off])
|
||||
off++
|
||||
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
|
||||
// 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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user