diff --git a/CHANGELOG.md b/CHANGELOG.md index f4b623b75..0b77edfec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ version at the top when it merges. ## Unreleased +- `TCPWINDOWMAX=` in an interface file caps the TCP receive window a + socket on that interface settles at, 65,536 to 1,048,576 bytes; unset + leaves the window as it was. The cap never goes below the window the + handshake offered. The key brings the resident `bsdsocket.library` image + to 365,536 bytes; its budget is 366,000 bytes. - `TCPGROWRTT=` in an interface file sets the handshake round trip (1 to 65,535 ms) at or above which a TCP receive window on that interface grows to its maximum. Unset, every device keeps 10 ms. The key adds 100 diff --git a/docs/user/AmiNetXDuo.guide b/docs/user/AmiNetXDuo.guide index 80038275b..a4dc6b683 100644 --- a/docs/user/AmiNetXDuo.guide +++ b/docs/user/AmiNetXDuo.guide @@ -488,6 +488,17 @@ this interface only; editing the file takes effect when the interface is next attached. + @{b}TCPWINDOWMAX@{ub} + The largest TCP receive window, in bytes, 65536 to + 1048576, that a socket on this interface settles at once + its handshake is done. It lowers the window chosen from + the packet pool, TCPGROWRTT and RXBUFFER, and never + raises it. Leave it out for no limit. The value applies + to this interface only; editing the file takes effect + when the interface is next attached. SO_RCVBUF does not + change the window: it bounds the socket's receive queue + in packets. + Interface files written for Roadshow usually carry other keywords: COPYMODE, ALIAS, METRIC, LEASE, DHCPUNICAST, LINKSTATUSCOMMAND, MULTICAST, POINTTOPOINT, DESTINATION and BROADCASTADDRESS among diff --git a/include/aminetxduo/config.h b/include/aminetxduo/config.h index 6b7f3f24d..6bf3ed5cd 100644 --- a/include/aminetxduo/config.h +++ b/include/aminetxduo/config.h @@ -56,6 +56,11 @@ extern "C" { #define AMI_CFG_WRITEREQUESTS_MAX AMINETXDUO_TX_SLOTS #define AMI_CFG_TCP_ACK_MAX 65535UL #define AMI_CFG_TCP_GROW_RTT_MAX 65535UL +/* TCPWINDOWMAX bounds. The SYN advertised min(window, 65535) unscaled; a + cap under it would retract that edge. 65536 >> s << s >= 65535 for every + scale up to 5, and 5 is what 1 MiB (BSD_TCP_WINDOW_MAX) negotiates. */ +#define AMI_CFG_TCP_WINDOW_MAX_MIN 65536UL +#define AMI_CFG_TCP_WINDOW_MAX_MAX 1048576UL #define AMI_CFG_PATH_LEN 128 /* The default domain gets its own cap: SetDefaultDomainName()'s autodoc says @@ -207,6 +212,9 @@ typedef struct AmiIfConfig { /* TCPGROWRTT: handshake round trip in ms at or above which a socket on this interface grows its receive window. 0 = BSD_TCP_WINDOW_GROW_RTT_MS. */ UWORD tcp_grow_rtt; + /* TCPWINDOWMAX: bytes a socket on this interface settles its receive + window at, at most. 0 = no cap. */ + ULONG tcp_window_max; BYTE priority; /* PRIORITY=, -128..127, 0 unset: the highest wins a route two interfaces could carry */ diff --git a/include/aminetxduo/config_advice.h b/include/aminetxduo/config_advice.h index c2e21fb99..795675172 100644 --- a/include/aminetxduo/config_advice.h +++ b/include/aminetxduo/config_advice.h @@ -66,5 +66,6 @@ #define AMI_CFG_ADVICE_FILTER_IS_LOCAL_IPANDARP 51 #define AMI_CFG_ADVICE_TCPACKMAX_IS_ACK_BYTES 52 #define AMI_CFG_ADVICE_TCPGROWRTT_IS_MILLISECONDS 53 +#define AMI_CFG_ADVICE_TCPWINDOWMAX_IS_BYTES 54 #endif /* AMINETXDUO_CONFIG_ADVICE_H */ diff --git a/include/aminetxduo/sana2.h b/include/aminetxduo/sana2.h index ce9660da7..e5b152041 100644 --- a/include/aminetxduo/sana2.h +++ b/include/aminetxduo/sana2.h @@ -105,6 +105,8 @@ ULONG ami_sana2_get_hw_rx_bytes(const AmiSana2If *iface); /* The interface file's TCPGROWRTT in ms, 0 = not set (bsdsocket_window.h, BSD_TCP_WINDOW_GROW_RTT_MS). */ ULONG ami_sana2_get_tcp_grow_rtt(const AmiSana2If *iface); +/* The interface file's TCPWINDOWMAX in bytes, 0 = not set (no cap). */ +ULONG ami_sana2_get_tcp_window_max(const AmiSana2If *iface); BOOL ami_sana2_is_online(const AmiSana2If *iface); /* diff --git a/install/Install-AmiNetXDuo b/install/Install-AmiNetXDuo index b283559f9..d41f6d821 100644 --- a/install/Install-AmiNetXDuo +++ b/install/Install-AmiNetXDuo @@ -2242,6 +2242,8 @@ (append "; Leave it out for this device's default.\n") (append "; TCPGROWRTT= is the handshake round trip in ms at which a\n") (append "; TCP receive window grows. Leave it out for 10.\n") + (append "; TCPWINDOWMAX= caps a TCP receive window in bytes, 65536 to\n") + (append "; 1048576. Leave it out for no cap.\n") (append ";\n") (append "; An IPv6 build also understands CONFIGURE6 (OFF, LINKLOCAL,\n") (append "; AUTO, DHCP or STATIC), ADDRESS6 and GATEWAY6. A build\n") diff --git a/src/bsdsocket/bsdsocket_window.c b/src/bsdsocket/bsdsocket_window.c index 63f24315a..07981a88a 100644 --- a/src/bsdsocket/bsdsocket_window.c +++ b/src/bsdsocket/bsdsocket_window.c @@ -104,6 +104,20 @@ ULONG ami_bsd_tcp_window_settle(ULONG created, ULONG maximum, ULONG bps, return created; } +ULONG ami_bsd_tcp_window_chosen(ULONG created, ULONG maximum, ULONG bps, + ULONG rtt_ms, ULONG grow_rtt_ms, + ULONG hw_bytes, ULONG mss, ULONG window_max) +{ + ULONG want = ami_bsd_tcp_window_settle(created, maximum, bps, rtt_ms, + grow_rtt_ms); + + if (ami_bsd_tcp_window_burst_bound(bps, rtt_ms, grow_rtt_ms)) + want = ami_bsd_tcp_window_fit(want, hw_bytes, mss); + if (window_max != 0UL && want > window_max) + want = window_max; + return want; +} + BOOL ami_bsd_tcp_window_burst_bound(ULONG bps, ULONG rtt_ms, ULONG grow_rtt_ms) { diff --git a/src/bsdsocket/bsdsocket_window.h b/src/bsdsocket/bsdsocket_window.h index 7f38746c4..d72abb305 100644 --- a/src/bsdsocket/bsdsocket_window.h +++ b/src/bsdsocket/bsdsocket_window.h @@ -182,6 +182,18 @@ ULONG ami_bsd_tcp_window_fit(ULONG window, ULONG hw_bytes, ULONG mss); BOOL ami_bsd_tcp_window_burst_bound(ULONG bps, ULONG rtt_ms, ULONG grow_rtt_ms); +/* + * The whole settle decision for one interface: _settle(), then _fit() where + * _burst_bound(), then the interface's TCPWINDOWMAX (`window_max`, 0 = no + * cap, #89). No interface is bps, hw_bytes, grow_rtt_ms and window_max all + * 0. The cap only lowers: the scale negotiated at SYN from `maximum` still + * expresses the result, rounded down to its 1 << scale granularity on the + * wire. Pure arithmetic, host-tested. + */ +ULONG ami_bsd_tcp_window_chosen(ULONG created, ULONG maximum, ULONG bps, + ULONG rtt_ms, ULONG grow_rtt_ms, + ULONG hw_bytes, ULONG mss, ULONG window_max); + #ifndef BSD_TCP_WINDOW_CEILING #ifdef AMINETXDUO_TCP_WINDOW_SCALING #define BSD_TCP_WINDOW_CEILING BSD_TCP_WINDOW_MAX diff --git a/src/bsdsocket/socket.c b/src/bsdsocket/socket.c index bf4e5a106..d79fa2f87 100644 --- a/src/bsdsocket/socket.c +++ b/src/bsdsocket/socket.c @@ -204,21 +204,20 @@ VOID bsd_tcp_window_settle(NX_TCP_SOCKET *tcp, ULONG rtt_ms) AmiSana2If *sana = (nxif != NX_NULL) ? (AmiSana2If *)nxif->nx_interface_additional_link_info : NULL; - ULONG bps = (sana != NULL) ? ami_sana2_get_bps(sana) : 0UL; - ULONG grow = ami_sana2_get_tcp_grow_rtt(sana); /* 0 = built-in line */ ULONG cur = tcp->nx_tcp_socket_rx_window_default; - ULONG want = ami_bsd_tcp_window_settle(cur, bsd_tcp_window_top(tcp), bps, - rtt_ms, grow); - /* ... and never more than the card behind this interface can hold from - the wire at once (bsdsocket_window.h, ami_bsd_tcp_window_fit), where - the whole window can arrive at once -- a LAN, or a card slower than - the path (ami_bsd_tcp_window_burst_bound). The segment size is the + /* Grown for the path, never more than the card behind this interface + can hold from the wire where the whole window can arrive at once, and + never more than its TCPWINDOWMAX (ami_bsd_tcp_window_chosen). A NULL + interface reads as all zeros: no fit, no cap. The segment size is the one the handshake settled: nx_tcp_socket_mss is what the application asked for and stays 0 on an accepted socket. */ - if (sana != NULL && ami_bsd_tcp_window_burst_bound(bps, rtt_ms, grow)) - want = ami_bsd_tcp_window_fit(want, ami_sana2_get_hw_rx_bytes(sana), - tcp->nx_tcp_socket_connect_mss); + ULONG want = ami_bsd_tcp_window_chosen(cur, bsd_tcp_window_top(tcp), + ami_sana2_get_bps(sana), rtt_ms, + ami_sana2_get_tcp_grow_rtt(sana), + ami_sana2_get_hw_rx_bytes(sana), + tcp->nx_tcp_socket_connect_mss, + ami_sana2_get_tcp_window_max(sana)); if (want == cur) return; diff --git a/src/config/config_advice.c b/src/config/config_advice.c index 122a6ecae..295c417ba 100644 --- a/src/config/config_advice.c +++ b/src/config/config_advice.c @@ -130,6 +130,7 @@ static const char *const ami_cfg_advice_text[] = "before requesting an ACK, 1 to 65535. Leave it out for the " "device default; smaller values send more ACKs.", "TCPGROWRTT is milliseconds, 1 to 65535. Leave it out for 10.", + "TCPWINDOWMAX is bytes, 65536 to 1048576. Leave it out for no cap.", }; const char *ami_cfg_advice(UWORD code) diff --git a/src/config/config_parse.c b/src/config/config_parse.c index 2de5ebd47..85a2bfaff 100644 --- a/src/config/config_parse.c +++ b/src/config/config_parse.c @@ -45,6 +45,7 @@ typedef enum IF_KEY_RXBUFFER, IF_KEY_TCPACKMAX, IF_KEY_TCPGROWRTT, + IF_KEY_TCPWINDOWMAX, IF_KEY_PRIORITY } IfKey; @@ -80,6 +81,7 @@ ami_if_keywords[] = { "rxbuffer", IF_KEY_RXBUFFER }, { "tcpackmax", IF_KEY_TCPACKMAX }, { "tcpgrowrtt", IF_KEY_TCPGROWRTT }, + { "tcpwindowmax", IF_KEY_TCPWINDOWMAX }, { "priority", IF_KEY_PRIORITY }, /* Roadshow's, and PRI */ { "pri", IF_KEY_PRIORITY }, @@ -797,6 +799,22 @@ LONG ami_cfg_parse_interface(const char *name, char *buf, AmiIfConfig *out) } break; + case IF_KEY_TCPWINDOWMAX: + if (ami_cfg_parse_ulong(value, &n) && + n >= AMI_CFG_TCP_WINDOW_MAX_MIN && + n <= AMI_CFG_TCP_WINDOW_MAX_MAX) + { + out->tcp_window_max = n; + } + else + { + AMI_WARN("config: %s: bad TCPWINDOWMAX '%s'", out->name, value); + report_bad_value(lineno, AMI_CFG_PROBLEM_WARN, + "TCPWINDOWMAX", value, + AMI_CFG_ADVICE_TCPWINDOWMAX_IS_BYTES); + } + break; + case IF_KEY_IPREQUESTS: case IF_KEY_ARPREQUESTS: case IF_KEY_WRITEREQUESTS: diff --git a/src/config/test/test_config.c b/src/config/test/test_config.c index 664b9f730..a1709e1f2 100644 --- a/src/config/test/test_config.c +++ b/src/config/test/test_config.c @@ -1202,6 +1202,70 @@ static void test_interface_tcp_grow_rtt(void) ami_config_set_reporter(NULL, NULL); } +static void test_interface_tcp_window_max(void) +{ + AmiIfConfig iface; + char *buf; + + printf("interface: TCPWINDOWMAX is optional, 65536..1048576\n"); + + buf = dup_text("device = anxwifipi.device\nconfigure = dhcp\n"); + CHECK(ami_cfg_parse_interface("wifipi", buf, &iface) == AMI_CFG_OK); + CHECK(iface.tcp_window_max == 0); + free(buf); + + buf = dup_text("device = anxwifipi.device\nconfigure = dhcp\n" + "tcpwindowmax = 262144\n"); + CHECK(ami_cfg_parse_interface("wifipi", buf, &iface) == AMI_CFG_OK); + CHECK(iface.tcp_window_max == 262144UL); + free(buf); + + buf = dup_text("device = genet.device\nconfigure = dhcp\n" + "TCPWINDOWMAX=65536\n"); + CHECK(ami_cfg_parse_interface("eth", buf, &iface) == AMI_CFG_OK); + CHECK(iface.tcp_window_max == 65536UL); + free(buf); + + buf = dup_text("device = genet.device\nconfigure = dhcp\n" + "TCPWINDOWMAX=1048576\n"); + CHECK(ami_cfg_parse_interface("eth", buf, &iface) == AMI_CFG_OK); + CHECK(iface.tcp_window_max == 1048576UL); + free(buf); +} + +/* Under the 65536 floor (a retraction of the handshake window), over the + maximum, 0, or not a number: warned with the advice, left unset. */ +static void test_interface_tcp_window_max_rejects_bad_values(void) +{ + static const char *const bad[] = { + "0", "8192", "65535", "1048577", "4294967296", "big", "256k" + }; + AmiIfConfig iface; + char *buf; + char text[96]; + ULONG i; + + printf("interface: a bad TCPWINDOWMAX is warned and ignored\n"); + + ami_config_set_reporter(collect, NULL); + ami_cfg_problem_file("DEVS:NetInterfaces/wifipi"); + for (i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) + { + seen_count = 0; + snprintf(text, sizeof(text), "device = anxwifipi.device\n" + "configure = dhcp\ntcpwindowmax = %s\n", bad[i]); + buf = dup_text(text); + CHECK(ami_cfg_parse_interface("wifipi", buf, &iface) == AMI_CFG_OK); + CHECK(iface.tcp_window_max == 0); + CHECK(seen_count == 1 && seen[0].line == 3); + CHECK(seen_count == 1 && + strstr(seen[0].hint, + "TCPWINDOWMAX is bytes, 65536 to 1048576") != NULL); + free(buf); + } + ami_config_set_reporter(NULL, NULL); +} + static void test_interface_priority(void) { AmiIfConfig iface; @@ -3021,6 +3085,8 @@ int main(int argc, char **argv) test_request_counts_have_ceilings(); test_interface_tcp_ack_max(); test_interface_tcp_grow_rtt(); + test_interface_tcp_window_max(); + test_interface_tcp_window_max_rejects_bad_values(); test_interface_priority(); test_interface_ipv6_only(); #ifdef AMINETXDUO_IPV6 diff --git a/src/sana2/sana2_device.c b/src/sana2/sana2_device.c index ad7d56b36..8b00fcf2b 100644 --- a/src/sana2/sana2_device.c +++ b/src/sana2/sana2_device.c @@ -1321,6 +1321,7 @@ AmiSana2If *ami_sana2_open(const AmiIfConfig *cfg, LONG *err) * by name. 0 is no cap. */ iface->tcp_grow_rtt = cfg->tcp_grow_rtt; + iface->tcp_window_max = cfg->tcp_window_max; if (cfg->rx_buffer != 0) iface->hw_rx_bytes = cfg->rx_buffer; else if ((iface->extension.Accepted & ANXD_S2F_RX_CAPACITY) != 0) @@ -1613,6 +1614,11 @@ ULONG ami_sana2_get_tcp_grow_rtt(const AmiSana2If *iface) return (iface != NULL) ? iface->tcp_grow_rtt : 0; } +ULONG ami_sana2_get_tcp_window_max(const AmiSana2If *iface) +{ + return (iface != NULL) ? iface->tcp_window_max : 0; +} + /* * What a vendor driver's card holds from the wire, by the driver's name, * for the drivers whose hardware is known: they cannot be asked. The diff --git a/src/sana2/sana2_internal.h b/src/sana2/sana2_internal.h index b3568a10c..075444eda 100644 --- a/src/sana2/sana2_internal.h +++ b/src/sana2/sana2_internal.h @@ -862,6 +862,8 @@ struct AmiSana2If ULONG hw_rx_bytes; /* TCPGROWRTT in ms, 0 = BSD_TCP_WINDOW_GROW_RTT_MS (bsdsocket_window.h). */ UWORD tcp_grow_rtt; + /* TCPWINDOWMAX in bytes, 0 = no cap (bsdsocket_window.h). */ + ULONG tcp_window_max; /* The interface file's IPREQUESTS, ARPREQUESTS (0 = the plan decides) and WRITEREQUESTS (1..AMI_SANA2_TX_SLOTS: how many of tx[] are diff --git a/tests/bsdsocket/CMakeLists.txt b/tests/bsdsocket/CMakeLists.txt index 6768eb5bd..b95167491 100644 --- a/tests/bsdsocket/CMakeLists.txt +++ b/tests/bsdsocket/CMakeLists.txt @@ -300,7 +300,8 @@ if(NOT CMAKE_CROSSCOMPILING AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") # mcast.c and cmsg.c -- are stubbed; the test's header says what that does # and does not prove. add_executable(test_sockopt host/test_sockopt_host.c - "${CMAKE_SOURCE_DIR}/src/bsdsocket/options.c") + "${CMAKE_SOURCE_DIR}/src/bsdsocket/options.c" + "${CMAKE_SOURCE_DIR}/src/bsdsocket/bsdsocket_window.c") target_include_directories(test_sockopt PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/host/shim" @@ -323,7 +324,8 @@ if(NOT CMAKE_CROSSCOMPILING AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") # half -- 0 turns the small-write cork on, getsockopt reads it back -- is # compiled and checked whatever the option says, rxdirect's reason below. add_executable(test_sockopt_cork host/test_sockopt_host.c - "${CMAKE_SOURCE_DIR}/src/bsdsocket/options.c") + "${CMAKE_SOURCE_DIR}/src/bsdsocket/options.c" + "${CMAKE_SOURCE_DIR}/src/bsdsocket/bsdsocket_window.c") target_include_directories(test_sockopt_cork PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/host/shim" diff --git a/tests/bsdsocket/host/test_sockopt_host.c b/tests/bsdsocket/host/test_sockopt_host.c index 10b077a78..6e0a25c45 100644 --- a/tests/bsdsocket/host/test_sockopt_host.c +++ b/tests/bsdsocket/host/test_sockopt_host.c @@ -6,7 +6,9 @@ */ #include "bsdsocket_vectors.h" +#include "bsdsocket_window.h" #include "interfaces.h" +#include "nx_tcp.h" /* NX_TCP_MAXIMUM_RX_QUEUE */ /* TCP_USER_TIMEOUT. host_prelude.h undefines glibc's, which is 18 where the Amiga's is 0x1001; this is the header options.c takes it from too. */ @@ -54,6 +56,7 @@ static struct ULONG raw_available; ULONG udp_available; ULONG packet_length; + ULONG rx_queue_max; /* last receive_queue_max_set value, +1 */ } h; static void h_reset(void) @@ -305,7 +308,8 @@ UINT _nxe_tcp_socket_mss_set(NX_TCP_SOCKET *socket_ptr, ULONG mss) UINT _nxe_tcp_socket_receive_queue_max_set(NX_TCP_SOCKET *socket_ptr, UINT receive_queue_maximum) { - (VOID)socket_ptr; (VOID)receive_queue_maximum; + (VOID)socket_ptr; + h.rx_queue_max = (ULONG)receive_queue_maximum + 1UL; return NX_SUCCESS; } @@ -827,6 +831,70 @@ static void t_nodelay(void) #endif } +/* + * SO_RCVBUF against TCPWINDOWMAX (#89). The cap sets the advertised window + * at settle; SO_RCVBUF bounds the receive queue in packets and never touches + * the window, before or after settle. getsockopt reads SO_RCVBUF back when + * set, else the settled window. + */ +static void t_rcvbuf_window(void) +{ + const ULONG capped = ami_bsd_tcp_window_chosen( + (ULONG)BSD_TCP_WINDOW_LAN, 802816UL, 100000000UL, 2UL, 2UL, 0UL, + 1460UL, 262144UL); + AmiSocket *s; + LONG value; + socklen_t len; + LONG rc; + + printf("options.c: SO_RCVBUF beside TCPWINDOWMAX\n"); + + h_reset(); + s = h_tcp(0); + CHECK(capped == 262144UL, "TCPWINDOWMAX=262144 settles at 262144"); + s->as_Nx.tcp.nx_tcp_socket_rx_window_default = capped; + s->as_Nx.tcp.nx_tcp_socket_rx_window_current = capped; +#ifdef NX_ENABLE_TCP_WINDOW_SCALING + s->as_Nx.tcp.nx_tcp_socket_rx_window_maximum = 802816UL; +#endif + + value = 0; len = (socklen_t)sizeof(value); + rc = bsd_getsockopt(0, SOL_SOCKET, SO_RCVBUF, &value, &len, &h_base); + CHECK(rc == 0 && value == 262144L, "unset SO_RCVBUF reads the capped window"); + + value = 65536; + rc = bsd_setsockopt(0, SOL_SOCKET, SO_RCVBUF, &value, sizeof(value), + &h_base); + CHECK(rc == 0, "SO_RCVBUF 65536 is accepted"); +#ifdef NX_ENABLE_LOW_WATERMARK + CHECK(h.rx_queue_max == (ULONG)((65536UL + 1459UL) / 1460UL < + (ULONG)NX_TCP_MAXIMUM_RX_QUEUE + ? (65536UL + 1459UL) / 1460UL + : (ULONG)NX_TCP_MAXIMUM_RX_QUEUE) + 1UL, + "SO_RCVBUF bounds the receive queue in packets"); +#endif + CHECK(s->as_Nx.tcp.nx_tcp_socket_rx_window_default == capped && + s->as_Nx.tcp.nx_tcp_socket_rx_window_current == capped, + "SO_RCVBUF moved the window"); +#ifdef NX_ENABLE_TCP_WINDOW_SCALING + CHECK(s->as_Nx.tcp.nx_tcp_socket_rx_window_maximum == 802816UL, + "SO_RCVBUF moved the window maximum"); +#endif + + value = 0; len = (socklen_t)sizeof(value); + rc = bsd_getsockopt(0, SOL_SOCKET, SO_RCVBUF, &value, &len, &h_base); + CHECK(rc == 0 && value == 65536L, "a set SO_RCVBUF reads back as set"); + + /* Larger than the cap: still only the queue; the window stays capped. */ + value = 1048576; + rc = bsd_setsockopt(0, SOL_SOCKET, SO_RCVBUF, &value, sizeof(value), + &h_base); + CHECK(rc == 0 && + s->as_Nx.tcp.nx_tcp_socket_rx_window_default == capped && + s->as_Nx.tcp.nx_tcp_socket_rx_window_current == capped, + "a large SO_RCVBUF lifted the capped window"); +} + static void t_user_timeout(void) { AmiSocket *s; @@ -924,6 +992,7 @@ int main(void) t_refusals(); t_user_timeout(); t_nodelay(); + t_rcvbuf_window(); t_ioctls(); printf("%lu checks, %lu failures\n", h_checks, h_failures); diff --git a/tests/netstack/CMakeLists.txt b/tests/netstack/CMakeLists.txt index 8611efec7..38ae32a84 100644 --- a/tests/netstack/CMakeLists.txt +++ b/tests/netstack/CMakeLists.txt @@ -678,6 +678,21 @@ if(NOT CMAKE_CROSSCOMPILING) add_test(NAME pool_window COMMAND test_pool_window) + # The window a socket settles at for an NI's TCPGROWRTT/TCPWINDOWMAX and + # a pool, offline (#89): build/.../predict_window [key=value ...]. + add_executable(predict_window + host/predict_window_host.c + "${CMAKE_SOURCE_DIR}/src/bsdsocket/bsdsocket_window.c") + + target_include_directories(predict_window PRIVATE + "${CMAKE_SOURCE_DIR}/src/config/test/shim" # exec/types.h + "${CMAKE_SOURCE_DIR}/src/bsdsocket" + "${CMAKE_SOURCE_DIR}/include") + + target_compile_options(predict_window PRIVATE -Wall -Wextra -Werror) + + add_test(NAME predict_window COMMAND predict_window) + # The pool NetX actually carves is the pool that was planned. Links the # real nx_packet_pool_create(), because the count that failed run-bigmem.sh # (4106 on a 4096 clamp) is NetX's own, not the planning arithmetic's. diff --git a/tests/netstack/host/predict_window_host.c b/tests/netstack/host/predict_window_host.c new file mode 100644 index 000000000..29461fc9b --- /dev/null +++ b/tests/netstack/host/predict_window_host.c @@ -0,0 +1,119 @@ +/* + * The receive window a TCP socket settles at, offline (#89): the pool + * share it opens with and may grow to, then bsd_tcp_window_settle()'s + * decision for one interface's TCPGROWRTT, RXBUFFER and TCPWINDOWMAX. + * Uses the stack's own arithmetic (bsdsocket_window.c); the scale and + * the growth guard mirror socket.c and nx_tcp_packet_send_syn.c. + * + * predict_window [pool=N] [payload=B] [consumers=N] [bps=N] [rtt=MS] + * [growrtt=MS] [hw=B] [mss=B] [windowmax=B] [peerscale=0|1] + * + * With no arguments, the #89 WiFiPi A1200 arms. Output is key=value. + * + * SPDX-License-Identifier: MIT + */ + +#include "bsdsocket_window.h" + +#include +#include +#include + +typedef struct +{ + ULONG pool, payload, consumers, bps, rtt, growrtt, hw, mss, windowmax; + ULONG peerscale; +} Arm; + +static ULONG scale_for(ULONG maximum) +{ + ULONG s = 0; + + while (s < 14UL && (maximum >> s) > 65535UL) + s++; + return s; +} + +static void predict(const char *name, const Arm *a) +{ + ULONG created = ami_bsd_tcp_window_for(a->pool, a->payload, a->consumers); + ULONG maximum = ami_bsd_tcp_window_max_for(a->pool, a->payload, + a->consumers); + ULONG scale, want, settled; + + if (!a->peerscale) /* the fork pins both at 65535 */ + { + created = maximum = 65535UL; + scale = 0; + } + else + scale = scale_for(maximum); + + want = ami_bsd_tcp_window_chosen(created, maximum, a->bps, a->rtt, + a->growrtt, a->hw, a->mss, a->windowmax); + settled = want; + if (want > created && (want >> scale) > 65535UL) + settled = created; /* socket.c: growth not expressible */ + + printf("arm=%s pool=%lu consumers=%lu rtt=%lu growrtt=%lu hw=%lu " + "windowmax=%lu created=%lu maximum=%lu scale=%lu settled=%lu " + "advertised=%lu\n", + name, (unsigned long)a->pool, (unsigned long)a->consumers, + (unsigned long)a->rtt, (unsigned long)a->growrtt, + (unsigned long)a->hw, (unsigned long)a->windowmax, + (unsigned long)created, (unsigned long)maximum, + (unsigned long)scale, (unsigned long)settled, + (unsigned long)((settled >> scale) << scale)); +} + +int main(int argc, char **argv) +{ + static const struct { const char *name; ULONG growrtt, windowmax; } arms[] = { + { "default", 0UL, 0UL }, + { "growrtt2", 2UL, 0UL }, + { "growrtt2_cap512k", 2UL, 524288UL }, + { "growrtt2_cap256k", 2UL, 262144UL }, + { "growrtt2_cap128k", 2UL, 131072UL }, + { "growrtt2_cap64k", 2UL, 65536UL }, + { "cap256k", 0UL, 262144UL }, + { "cap64k", 0UL, 65536UL }, + }; + Arm a = { 4096UL, (ULONG)AMI_POOL_PAYLOAD, 3UL, 0UL, 2UL, 0UL, 0UL, + 1460UL, 0UL, 1UL }; + int i; + + if (argc < 2) + { + for (i = 0; i < (int)(sizeof(arms) / sizeof(arms[0])); i++) + { + a.growrtt = arms[i].growrtt; + a.windowmax = arms[i].windowmax; + predict(arms[i].name, &a); + } + return 0; + } + + for (i = 1; i < argc; i++) + { + static const char *const keys[] = { "pool", "payload", "consumers", + "bps", "rtt", "growrtt", "hw", "mss", "windowmax", "peerscale" }; + ULONG *slots[] = { &a.pool, &a.payload, &a.consumers, &a.bps, &a.rtt, + &a.growrtt, &a.hw, &a.mss, &a.windowmax, + &a.peerscale }; + const char *eq = strchr(argv[i], '='); + size_t k; + + for (k = 0; eq != NULL && k < sizeof(keys) / sizeof(keys[0]); k++) + if (strlen(keys[k]) == (size_t)(eq - argv[i]) && + strncmp(argv[i], keys[k], (size_t)(eq - argv[i])) == 0) + break; + if (eq == NULL || k == sizeof(keys) / sizeof(keys[0])) + { + fprintf(stderr, "error=unknown_argument arg=%s\n", argv[i]); + return 2; + } + *slots[k] = strtoul(eq + 1, NULL, 0); + } + predict("custom", &a); + return 0; +} diff --git a/tests/netstack/host/test_pool_window_host.c b/tests/netstack/host/test_pool_window_host.c index 0c56cfec6..28f73fe24 100644 --- a/tests/netstack/host/test_pool_window_host.c +++ b/tests/netstack/host/test_pool_window_host.c @@ -557,6 +557,176 @@ static void i_the_window_settles_for_the_path_and_the_link(void) } +/* + * TCPWINDOWMAX (#89): the interface's cap on the settled window, applied + * after the grow decision and the card's fit. 0 is no cap, which is also + * what a socket with no interface gets. 802,816 is the WiFiPi A1200's + * grown window (a 4,096-packet pool, three sockets live). The arithmetic + * takes any cap literally; the parser's 65,536 floor is what keeps a cap + * from retracting the handshake window (config.h). + */ +#define K_A1200 ami_bsd_tcp_window_max_for(4096UL, (ULONG)AMI_POOL_PAYLOAD, 3UL) + +/* What the peer sees: the window in scaled units, 16 bits of them. */ +static ULONG k_advertised(ULONG window, ULONG scale) +{ + ULONG units = window >> scale; + + return ((units > 65535UL) ? 65535UL : units) << scale; +} + +static void k_tcpwindowmax_unset_and_no_interface_are_baseline(void) +{ + const ULONG lan = (ULONG)BSD_TCP_WINDOW_LAN; + const ULONG a1200 = K_A1200; + ULONG k; + + h_check(a1200 == 802816UL, "the A1200 pool share is not 802,816"); + + /* 0/unset: exactly the uncapped decision, every path, link and card. */ + for (k = 0; k < 40UL; k++) + { + ULONG bps = (k % 4UL == 0UL) ? 0UL : (k % 4UL == 1UL) ? 10000000UL + : (k % 4UL == 2UL) ? 100000000UL + : (ULONG)BSD_TCP_WINDOW_FAST_BPS; + ULONG rtt = k / 4UL * 3UL; + ULONG hw = (k & 1UL) ? 52UL * 256UL : 0UL; + ULONG want = ami_bsd_tcp_window_settle(lan, a1200, bps, rtt, 2UL); + + if (ami_bsd_tcp_window_burst_bound(bps, rtt, 2UL)) + want = ami_bsd_tcp_window_fit(want, hw, 1460UL); + h_check(ami_bsd_tcp_window_chosen(lan, a1200, bps, rtt, 2UL, hw, + 1460UL, 0UL) == want, + "TCPWINDOWMAX unset changed the settled window"); + } + /* No interface: every getter 0, the built-in 10 ms line, no fit. */ + h_check(ami_bsd_tcp_window_chosen(lan, a1200, 0UL, 9UL, 0UL, 0UL, + 1460UL, 0UL) == + ami_bsd_tcp_window_settle(lan, a1200, 0UL, 9UL, 0UL) && + ami_bsd_tcp_window_chosen(lan, a1200, 0UL, 10UL, 0UL, 0UL, + 1460UL, 0UL) == + ami_bsd_tcp_window_settle(lan, a1200, 0UL, 10UL, 0UL), + "no interface is not the baseline settle"); +} + +static void l_tcpwindowmax_clamps_below_and_ignores_above(void) +{ + const ULONG lan = (ULONG)BSD_TCP_WINDOW_LAN; + const ULONG max = (ULONG)BSD_TCP_WINDOW_MAX; + const ULONG lmax = (ULONG)BSD_TCP_WINDOW_MAX_LAN; + const ULONG gbit = (ULONG)BSD_TCP_WINDOW_FAST_BPS; + const ULONG hundm = 100000000UL; + const ULONG a1200 = K_A1200; + ULONG k; + + h_check(ami_bsd_tcp_window_chosen(lan, a1200, hundm, 2UL, 2UL, 0UL, + 1460UL, 0UL) == a1200, + "TCPGROWRTT=2 no longer grows the A1200 to 802,816"); + h_check(ami_bsd_tcp_window_chosen(lan, a1200, hundm, 2UL, 2UL, 0UL, + 1460UL, 262144UL) == 262144UL, + "TCPWINDOWMAX=262144 did not cap a grown window"); + h_check(ami_bsd_tcp_window_chosen(lan, a1200, hundm, 2UL, 2UL, 0UL, + 1460UL, a1200 - 1UL) == a1200 - 1UL, + "a cap one below the chosen window did not clamp"); + h_check(ami_bsd_tcp_window_chosen(lan, a1200, hundm, 2UL, 2UL, 0UL, + 1460UL, a1200) == a1200 && + ami_bsd_tcp_window_chosen(lan, a1200, hundm, 2UL, 2UL, 0UL, + 1460UL, max) == a1200, + "a cap at or above the chosen window moved it"); + /* A window that did not grow can be lowered, never raised. */ + h_check(ami_bsd_tcp_window_chosen(lan, a1200, hundm, 1UL, 2UL, 0UL, + 1460UL, 262144UL) == lan, + "a cap raised an ungrown window"); + h_check(ami_bsd_tcp_window_chosen(lan, a1200, hundm, 1UL, 2UL, 0UL, + 1460UL, 65536UL) == 65536UL, + "a cap did not lower an ungrown window"); + /* The gigabit LAN maximum and the card's fit are both mins with it. */ + h_check(ami_bsd_tcp_window_chosen(lan, max, gbit, 1UL, 0UL, 0UL, + 1460UL, 524288UL) == lmax && + ami_bsd_tcp_window_chosen(lan, max, gbit, 1UL, 0UL, 0UL, + 1460UL, 131072UL) == 131072UL, + "the cap and the gigabit LAN maximum are not a min"); + h_check(ami_bsd_tcp_window_chosen(lan, a1200, hundm, 2UL, 2UL, + 52UL * 256UL, 1460UL, 262144UL) == + 8UL * 1460UL, + "the cap and the card's fit are not a min"); + for (k = 65536UL; k <= max; k += 4093UL) + h_check(ami_bsd_tcp_window_chosen(lan, a1200, hundm, 2UL, 2UL, 0UL, + 1460UL, k) <= a1200, + "a cap raised the window past the negotiated maximum"); + /* Unfloored arithmetic: a sub-two-segment cap is taken as given. */ + h_check(ami_bsd_tcp_window_chosen(lan, a1200, hundm, 2UL, 2UL, 0UL, + 1460UL, 2000UL) == 2000UL, + "a sub-two-segment cap was not taken literally"); +} + +static void m_tcpwindowmax_scaled_rounding_at_the_boundaries(void) +{ + const ULONG lan = (ULONG)BSD_TCP_WINDOW_LAN; + const ULONG hundm = 100000000UL; + const ULONG a1200 = K_A1200; + + /* 802,816 negotiates scale 4 (50,176 units). */ + h_check((a1200 >> 3) > 65535UL && (a1200 >> 4) <= 65535UL, + "the A1200 maximum is not scale 4"); + /* 65,535 (under the parser floor) is 65,520 on the wire at scale 4: a + 15-byte retraction of the handshake's 65,535. 65,536 is not. */ + h_check(k_advertised(ami_bsd_tcp_window_chosen(lan, a1200, hundm, 2UL, + 2UL, 0UL, 1460UL, 65535UL), 4UL) == 65520UL, + "65,535 at scale 4 is not 65,520 on the wire"); + h_check(k_advertised(ami_bsd_tcp_window_chosen(lan, a1200, hundm, 2UL, + 2UL, 0UL, 1460UL, 65536UL), 4UL) == 65536UL, + "65,536 at scale 4 is not 65,536 on the wire"); + h_check(k_advertised(262145UL, 4UL) == 262144UL && + k_advertised(262159UL, 4UL) == 262144UL && + k_advertised(262160UL, 4UL) == 262160UL, + "scale 4 does not round down to 16 bytes"); + /* 65,535 << 4 is above the maximum: inert. */ + h_check(ami_bsd_tcp_window_chosen(lan, a1200, hundm, 2UL, 2UL, 0UL, + 1460UL, 65535UL << 4) == a1200, + "65,535 << scale is above the maximum and clamped"); + /* An unscaled peer: both fields pinned at 65,535, a floor cap inert. */ + h_check(ami_bsd_tcp_window_chosen(65535UL, 65535UL, hundm, 2UL, 2UL, 0UL, + 1460UL, 65536UL) == 65535UL, + "a floor cap moved an unscaled peer's pinned 65,535"); + /* 1 MiB needs scale 5, the largest the parser's range negotiates. */ + h_check((1048576UL >> 4) > 65535UL && (1048576UL >> 5) <= 65535UL, + "1 MiB is not scale 5"); +} + +/* The SYN / SYN-ACK advertised min(created, 65535) unscaled; no cap the + parser accepts may move that right edge left, at any scale 0..5. */ +static void n_tcpwindowmax_never_retracts_the_handshake_window(void) +{ + static const ULONG created[] = { 8192UL, 50176UL, 65535UL, 100352UL }; + const ULONG hundm = 100000000UL; + ULONG s, c, cap, bad = 0, n = 0; + + for (s = 0; s <= 5UL; s++) + for (c = 0; c < sizeof(created) / sizeof(created[0]); c++) + { + /* The largest maximum that negotiates scale s. */ + ULONG maximum = (s == 0UL) ? 65535UL : 65535UL << s; + ULONG syn = (created[c] < 65535UL) ? created[c] : 65535UL; + + if (maximum > (ULONG)BSD_TCP_WINDOW_MAX) + maximum = (ULONG)BSD_TCP_WINDOW_MAX; + for (cap = 65536UL; cap <= 1048576UL; + cap += (cap < 66000UL || cap > 1048000UL) ? 1UL : 97UL) + { + ULONG want = ami_bsd_tcp_window_chosen(created[c], maximum, + hundm, 2UL, 2UL, 0UL, 1460UL, cap); + + n++; + if (k_advertised(want, s) < syn) + bad++; + } + } + h_check(n > 100000UL && bad == 0UL, + "a cap in range retracted the handshake window"); +} + + /* * The bytes the pool is sized from are the fastest memory class's, not * everything Exec has: the A3000 with 12 MB of motherboard RAM ahead of a @@ -613,6 +783,10 @@ int main(void) h_a_big_machine_is_bounded_by_the_link(); i_the_window_settles_for_the_path_and_the_link(); j_the_pool_is_sized_from_the_fastest_class(); + 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(); printf("%lu checks, %lu failures, %s\n", h_checks, h_failures, (h_failures == 0UL) ? "PASS" : "FAIL"); diff --git a/tests/sana2/host/test_sana2_device_host.c b/tests/sana2/host/test_sana2_device_host.c index b00f3e1a1..750552813 100644 --- a/tests/sana2/host/test_sana2_device_host.c +++ b/tests/sana2/host/test_sana2_device_host.c @@ -1505,6 +1505,60 @@ static void case_tcp_grow_rtt(void) "a NULL interface keeps the 10 ms line"); } +/* TCPWINDOWMAX reaches the interface at open; unsaid, or no interface at + all, is 0, which bsdsocket reads as no cap. */ +static void case_tcp_window_max(void) +{ + const ULONG lan = (ULONG)BSD_TCP_WINDOW_LAN; + const ULONG max = 802816UL; + AmiSana2If *iface; + LONG err = 0; + + printf(" the interface file's TCPWINDOWMAX reaches the interface\n"); + + h_config(); + h_cfg.tcp_grow_rtt = 2; + h_cfg.tcp_window_max = 262144UL; + iface = ami_sana2_open(&h_cfg, &err); + h_check(iface != NULL && ami_sana2_get_tcp_window_max(iface) == 262144UL, + "TCPWINDOWMAX=262144 is carried to the interface"); + /* What socket.c:bsd_tcp_window_settle does with it. */ + h_check(iface != NULL && + ami_bsd_tcp_window_chosen(lan, max, ami_sana2_get_bps(iface), 2UL, + ami_sana2_get_tcp_grow_rtt(iface), + ami_sana2_get_hw_rx_bytes(iface), 1460UL, + ami_sana2_get_tcp_window_max(iface)) == 262144UL, + "TCPWINDOWMAX=262144 caps a grown 802,816"); + if (iface != NULL) + (VOID)ami_sana2_close(iface); + + h_config(); + h_cfg.tcp_grow_rtt = 2; + iface = ami_sana2_open(&h_cfg, &err); + h_check(iface != NULL && ami_sana2_get_tcp_window_max(iface) == 0, + "an unsaid TCPWINDOWMAX is 0"); + h_check(iface != NULL && + ami_bsd_tcp_window_chosen(lan, max, ami_sana2_get_bps(iface), 2UL, + ami_sana2_get_tcp_grow_rtt(iface), + ami_sana2_get_hw_rx_bytes(iface), 1460UL, + ami_sana2_get_tcp_window_max(iface)) == max, + "an unsaid TCPWINDOWMAX capped the grown window"); + if (iface != NULL) + (VOID)ami_sana2_close(iface); + + h_check(ami_sana2_get_tcp_window_max(NULL) == 0, "no interface is 0"); + /* A NULL interface: every getter 0, the built-in line, no fit, no cap. */ + h_check(ami_bsd_tcp_window_chosen(lan, max, ami_sana2_get_bps(NULL), 10UL, + ami_sana2_get_tcp_grow_rtt(NULL), + ami_sana2_get_hw_rx_bytes(NULL), 1460UL, + ami_sana2_get_tcp_window_max(NULL)) == max && + ami_bsd_tcp_window_chosen(lan, max, ami_sana2_get_bps(NULL), 9UL, + ami_sana2_get_tcp_grow_rtt(NULL), + ami_sana2_get_hw_rx_bytes(NULL), 1460UL, + ami_sana2_get_tcp_window_max(NULL)) == lan, + "a NULL interface is capped or fitted"); +} + /* FILTER=EVERYTHING is SANA2OPF_PROM at OpenDevice(), and nothing else in the file touches the flags. */ static void case_filter_everything(void) @@ -1917,6 +1971,7 @@ int main(void) case_keeps_online(); case_request_counts(); case_tcp_grow_rtt(); + case_tcp_window_max(); case_filter_everything(); case_offline_bounded_sync(); case_offline_bounded_late_reply(); diff --git a/tools/ci.sh b/tools/ci.sh index f7f52fc39..a2b56b5fd 100755 --- a/tools/ci.sh +++ b/tools/ci.sh @@ -368,7 +368,9 @@ host_test_targets() { # builddir # forgotten, not swept into the next one (#53) # 473 with sana2_copy_netx: a padded short TCP frame copied in more than # one S2_CopyFromBuff call keeps a correct checksum (#89) -HOST_TESTS_EXPECTED=473 +# 474 with predict_window: the settled window per TCPGROWRTT and +# TCPWINDOWMAX arm, offline (#89) +HOST_TESTS_EXPECTED=474 case "$(uname -m)" in x86_64|amd64) ;; # test_inet, test_route, test_expunge, test_expunge_cork, test_select,