Skip to content

Cap the settled TCP receive window per interface: TCPWINDOWMAX (#89) - #116

Closed
tinic wants to merge 3 commits into
mainfrom
feat/tcp-window-max
Closed

tinic wants to merge 3 commits into
mainfrom
feat/tcp-window-max

Conversation

@tinic

@tinic tinic commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

#89. TCPWINDOWMAX=<bytes> in an interface file caps the receive window a TCP socket on that interface settles at. It exists for a one-variable arm: TCPGROWRTT=2 grows a WiFiPi socket to the pool share (802816 on the test A1200), and this cap holds everything else fixed while lowering that figure. RXBUFFER is untouched: it declares card capacity and only feeds the fit when the path is burst-bound.

Change

  • include/aminetxduo/config.h:61-62,216: AMI_CFG_TCP_WINDOW_MAX_MIN 65536, _MAX 1048576; AmiIfConfig.tcp_window_max (ULONG, 0 = unset).
  • src/config/config_parse.c:802-816: key TCPWINDOWMAX. 65536..1048576 is accepted; 0, out-of-range and non-numeric values are warned with advice 54 and ignored, following TCPGROWRTT's pattern. src/config/config_advice.c:133: "TCPWINDOWMAX is bytes, 65536 to 1048576. Leave it out for no cap."
  • Range (review follow-up, c004ad8): the SYN / SYN-ACK advertised min(window, 65535) unscaled before settle, so a cap below that would retract the right edge the peer already has; at scale 4 even 65535 is 65520 on the wire. 65536 >> s << s >= 65535 for every scale 0..5, and 1 MiB negotiates scale 5, so the floor is 65536. The arithmetic stays unfloored. 1048576 is BSD_TCP_WINDOW_MAX: no socket grows past it, so a larger cap would have no effect.
  • src/sana2/sana2_internal.h:866, sana2_device.c:1324,1617, include/aminetxduo/sana2.h:109: copied at ami_sana2_open beside TCPGROWRTT; ami_sana2_get_tcp_window_max() returns 0 for a NULL interface.
  • src/bsdsocket/bsdsocket_window.c:107, .h:193: ami_bsd_tcp_window_chosen() is the whole settle decision as pure arithmetic: _settle(), then _fit() where _burst_bound(), then min(want, cap) when cap != 0.
  • src/bsdsocket/socket.c:205-220: bsd_tcp_window_settle calls it with the interface's getters. A NULL interface reads as all zeros. That is the old sana == NULL path (bps 0, the built-in 10 ms line, and _fit(hw=0) is the identity), so there is no fit and no cap. The shrink/grow application and the scale guard that follow are unchanged.
  • Unchanged: RXBUFFER, the creation window, rx_window_maximum and scale negotiation, and SO_RCVBUF.

Scale

The scale is negotiated at SYN from rx_window_maximum (802816, which gives scale 4). The cap only lowers want, so want >> scale is still at most 65535 and the growth guard never fires because of it. At settle nothing has been received, so the existing shrink path removes the whole difference from rx_window_current. On the wire the window is rounded down to 1 << scale: 65535 advertises as 65520 at scale 4. For a peer that offered no scaling, both fields are pinned at 65535. A cap above that has no effect, and a cap below it shrinks, which needs no scale.

SO_RCVBUF

SO_RCVBUF (options.c:340-369) sets as_RcvBuf and, in a low-watermark build, the receive queue maximum in 1460-byte packets. It never writes the window fields, before or after settle. getsockopt(SO_RCVBUF) returns the set value, or the settled (capped) rx_window_default when unset. Precedence: TCPWINDOWMAX bounds what is advertised; SO_RCVBUF separately bounds what the queue keeps, and a segment past it is dropped even inside the window. A large SO_RCVBUF never lifts the cap. t_rcvbuf_window in test_sockopt_host.c tests this.

Tests (host, playhouse2)

Named cases: k_tcpwindowmax_unset_and_no_interface_are_baseline, l_tcpwindowmax_clamps_below_and_ignores_above, m_tcpwindowmax_scaled_rounding_at_the_boundaries, n_tcpwindowmax_never_retracts_the_handshake_window (test_pool_window, 1510 checks, 0 failures); test_interface_tcp_window_max, test_interface_tcp_window_max_rejects_bad_values (test_config); case_tcp_window_max (test_sana2_device); t_rcvbuf_window (test_sockopt, test_sockopt_cork).

  • test_pool_window (k-n): unset equals the uncapped decision over 40 path/link/card combinations, and no interface keeps the default line. A cap one below the chosen window clamps it; caps at or above it, including 1048576, change nothing. A cap never raises an ungrown window but can lower one. It is a min with the gigabit LAN maximum and with the X-Surf fit. Scale boundaries: 65535, 65536 and 65535<<4 are tested, and the unscaled peer's pinned 65535 with caps of 65534/65536. A sweep checks that no cap ever exceeds the negotiated maximum, and n_ checks that for every cap in 65536..1048576 and every scale 0..5 the advertised window is >= min(created, 65535). A cap of 2000 (below 2*MSS) is applied literally: the arithmetic has no floor, and the parser's 65536 is the floor.
  • test_sana2_device case_tcp_window_max: the value reaches the interface, and the socket.c composition caps 802816 to 262144; unset leaves the grown window and NULL is no cap. 427/0.
  • test_config test_interface_tcp_window_max: absent, 262144, 65536 and 1048576 are accepted; 0, 8192, 65535, 1048577, 4294967296, big and 256k each warn on line 3 with the advice. 881/0.
  • test_sockopt/_cork t_rcvbuf_window: 76/0.
  • config_routes and config_runtime pass. HOST_TESTS_EXPECTED is 474 (+predict_window).

Size (default, pinned /opt/amiga)

main 7fd931d this
bsdsocket.library 365408 365536 (+128, spare 464 of 366000)
AddNetInterface 33404 33580 (+176)
sizeof(AmiNetStack) 68172 68172 (+0)

RAM: +4 B per AmiSana2If (attached interface) and +4 B per AmiIfConfig. check-stack-frames.sh: clean, 48 roots.

Predicted window

build/host/tests/netstack/predict_window [pool= payload= consumers= bps= rtt= growrtt= hw= mss= windowmax= peerscale=] uses the same arithmetic and prints key=value. With no arguments it prints the #89 A1200 arms: 4096-packet pool, 3 live sockets, 2 ms handshake.

arm TCPGROWRTT TCPWINDOWMAX settled advertised (scale 4)
default - - 100352 100352
growrtt2 2 - 802816 802816
growrtt2_cap512k 2 524288 524288 524288
growrtt2_cap256k 2 262144 262144 262144
growrtt2_cap128k 2 131072 131072 131072
growrtt2_cap64k 2 65536 65536 65536
cap256k - 262144 100352 100352
cap64k - 65536 65536 65536

No hardware or emulator run. Draft: do not merge.

🤖 Generated with Claude Code

tinic and others added 3 commits September 28, 2026 01:12
TCPWINDOWMAX=<bytes> in an interface file is the largest receive window
a socket on that interface settles at, 8192..1048576 (BSD_TCP_WINDOW, the
smallest window a socket opens with, to BSD_TCP_WINDOW_MAX, the largest it
grows to).  0, out-of-range and non-numeric values are warned and ignored
like TCPGROWRTT.  Unset is 0: no cap, the settle decision is unchanged.

The value rides on AmiSana2If beside TCPGROWRTT.  The settle decision in
socket.c moves into ami_bsd_tcp_window_chosen() (bsdsocket_window.c):
_settle(), _fit() where _burst_bound(), then min(want, cap).  A NULL
interface reads as all zeros, which is the old sana == NULL path: no fit,
no cap.  The cap only lowers the window, so the scale negotiated at SYN
from rx_window_maximum still expresses it.  RXBUFFER, the creation window,
the maximum and scale negotiation are untouched; SO_RCVBUF still bounds
only the receive queue.

predict_window (tests/netstack) prints the window a socket settles at for
a pool and an interface's keys, from the same arithmetic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SYN / SYN-ACK advertised min(window, 65535) unscaled before settle.
A cap under that moves the peer's right edge left, and at scale 4 even
65535 is 65520 on the wire.  65536 >> s << s >= 65535 for every scale up
to 5, and 1 MiB negotiates 5, so 65536..1048576 is the range; 65535 is
now warned and ignored.  The arithmetic stays unfloored.

The window tests are split into named cases, one of which checks every
in-range cap at scales 0..5 against the handshake window; the config
test gets its own case for rejected values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI run 36396529239 at c004ad8 reads bsdsocket.library at 365,536 bytes
(image_size spare 464 of 366,000).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tinic tinic closed this Sep 28, 2026
@tinic
tinic deleted the feat/tcp-window-max branch September 30, 2026 00:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants