From 30f831b44a98a660ccaf608f88d8bb945318dc59 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 2 Apr 2026 13:59:53 -0700 Subject: [PATCH 1/8] selftests: drv-net: gro: add data burst test case Add a test trying to induce a GRO context timeout followed by another sequence of packets for the same flow. The second burst arrives 100ms after the first one so any implementation (SW or HW) must time out waiting at that point. We expect both bursts to be aggregated successfully but separately. Reviewed-by: Willem de Bruijn Link: https://patch.msgid.link/20260402210000.1512696-2-kuba@kernel.org Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/gro.py | 2 +- tools/testing/selftests/net/lib/gro.c | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tools/testing/selftests/drivers/net/gro.py b/tools/testing/selftests/drivers/net/gro.py index 70709bf670c7..10da5d4bee9b 100755 --- a/tools/testing/selftests/drivers/net/gro.py +++ b/tools/testing/selftests/drivers/net/gro.py @@ -289,7 +289,7 @@ def _gro_variants(): # Tests that work for all protocols common_tests = [ - "data_same", "data_lrg_sml", "data_sml_lrg", + "data_same", "data_lrg_sml", "data_sml_lrg", "data_burst", "ack", "flags_psh", "flags_syn", "flags_rst", "flags_urg", "flags_cwr", "tcp_csum", "tcp_seq", "tcp_ts", "tcp_opt", diff --git a/tools/testing/selftests/net/lib/gro.c b/tools/testing/selftests/net/lib/gro.c index 3e611ae25f61..4d002af4a7aa 100644 --- a/tools/testing/selftests/net/lib/gro.c +++ b/tools/testing/selftests/net/lib/gro.c @@ -12,6 +12,7 @@ * - data_same: same size packets coalesce * - data_lrg_sml: large then small coalesces * - data_sml_lrg: small then large doesn't coalesce + * - data_burst: two bursts of two, separated by 100ms * * ack: * Pure ACK does not coalesce. @@ -1298,6 +1299,21 @@ static void gro_sender(void) } else if (strcmp(testname, "data_sml_lrg") == 0) { send_data_pkts(txfd, &daddr, PAYLOAD_LEN / 2, PAYLOAD_LEN); write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "data_burst") == 0) { + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + + create_packet(buf, 0, 0, PAYLOAD_LEN, 0); + write_packet(txfd, buf, total_hdr_len + PAYLOAD_LEN, &daddr); + create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); + write_packet(txfd, buf, total_hdr_len + PAYLOAD_LEN, &daddr); + + usleep(100 * 1000); /* 100ms */ + create_packet(buf, PAYLOAD_LEN * 2, 0, PAYLOAD_LEN, 0); + write_packet(txfd, buf, total_hdr_len + PAYLOAD_LEN, &daddr); + create_packet(buf, PAYLOAD_LEN * 3, 0, PAYLOAD_LEN, 0); + write_packet(txfd, buf, total_hdr_len + PAYLOAD_LEN, &daddr); + + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); /* ack test */ } else if (strcmp(testname, "ack") == 0) { @@ -1463,6 +1479,11 @@ static void gro_receiver(void) correct_payload[0] = PAYLOAD_LEN / 2; correct_payload[1] = PAYLOAD_LEN; check_recv_pkts(rxfd, correct_payload, 2); + } else if (strcmp(testname, "data_burst") == 0) { + printf("two bursts of two data packets: "); + correct_payload[0] = PAYLOAD_LEN * 2; + correct_payload[1] = PAYLOAD_LEN * 2; + check_recv_pkts(rxfd, correct_payload, 2); /* ack test */ } else if (strcmp(testname, "ack") == 0) { From 436ea8a1b7ed1328cf24cebb3ebaa21aa0c6a81a Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 2 Apr 2026 13:59:54 -0700 Subject: [PATCH 2/8] selftests: drv-net: gro: add 1 byte payload test Small IPv4 packets get padded to 60B, this may break / confuse some buggy implementations. Add a test to coalesce a 1B payload. Keep this separate from the lrg_sml test because I suspect some implementations may not handle this case (treat padded frames as ineligible for coalescing). Reviewed-by: Willem de Bruijn Link: https://patch.msgid.link/20260402210000.1512696-3-kuba@kernel.org Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/gro.py | 4 +++- tools/testing/selftests/net/lib/gro.c | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/tools/testing/selftests/drivers/net/gro.py b/tools/testing/selftests/drivers/net/gro.py index 10da5d4bee9b..73436d16b40d 100755 --- a/tools/testing/selftests/drivers/net/gro.py +++ b/tools/testing/selftests/drivers/net/gro.py @@ -11,6 +11,7 @@ coalescing behavior. Test cases: - data_same: Same size data packets coalesce - data_lrg_sml: Large packet followed by smaller one coalesces + - data_lrg_1byte: Large packet followed by 1B one coalesces (Ethernet padding) - data_sml_lrg: Small packet followed by larger one doesn't coalesce - ack: Pure ACK packets do not coalesce - flags_psh: Packets with PSH flag don't coalesce @@ -289,7 +290,8 @@ def _gro_variants(): # Tests that work for all protocols common_tests = [ - "data_same", "data_lrg_sml", "data_sml_lrg", "data_burst", + "data_same", "data_lrg_sml", "data_sml_lrg", "data_lrg_1byte", + "data_burst", "ack", "flags_psh", "flags_syn", "flags_rst", "flags_urg", "flags_cwr", "tcp_csum", "tcp_seq", "tcp_ts", "tcp_opt", diff --git a/tools/testing/selftests/net/lib/gro.c b/tools/testing/selftests/net/lib/gro.c index 4d002af4a7aa..27ccd742615a 100644 --- a/tools/testing/selftests/net/lib/gro.c +++ b/tools/testing/selftests/net/lib/gro.c @@ -10,8 +10,9 @@ * packet coalesced: it can be smaller than the rest and coalesced * as long as it is in the same flow. * - data_same: same size packets coalesce - * - data_lrg_sml: large then small coalesces - * - data_sml_lrg: small then large doesn't coalesce + * - data_lrg_sml: large then small coalesces + * - data_lrg_1byte: large then 1 byte coalesces (Ethernet padding) + * - data_sml_lrg: small then large doesn't coalesce * - data_burst: two bursts of two, separated by 100ms * * ack: @@ -1296,6 +1297,9 @@ static void gro_sender(void) } else if (strcmp(testname, "data_lrg_sml") == 0) { send_data_pkts(txfd, &daddr, PAYLOAD_LEN, PAYLOAD_LEN / 2); write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "data_lrg_1byte") == 0) { + send_data_pkts(txfd, &daddr, PAYLOAD_LEN, 1); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); } else if (strcmp(testname, "data_sml_lrg") == 0) { send_data_pkts(txfd, &daddr, PAYLOAD_LEN / 2, PAYLOAD_LEN); write_packet(txfd, fin_pkt, total_hdr_len, &daddr); @@ -1474,6 +1478,10 @@ static void gro_receiver(void) printf("large data packets followed by a smaller one: "); correct_payload[0] = PAYLOAD_LEN * 1.5; check_recv_pkts(rxfd, correct_payload, 1); + } else if (strcmp(testname, "data_lrg_1byte") == 0) { + printf("large data packet followed by a 1 byte one: "); + correct_payload[0] = PAYLOAD_LEN + 1; + check_recv_pkts(rxfd, correct_payload, 1); } else if (strcmp(testname, "data_sml_lrg") == 0) { printf("small data packets followed by a larger one: "); correct_payload[0] = PAYLOAD_LEN / 2; From d97348474708d5f67bec6da28c2e14e12598b965 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 2 Apr 2026 13:59:55 -0700 Subject: [PATCH 3/8] selftests: drv-net: gro: always wait for FIN in the capacity test The new capacity/order test exits as soon as it sees the expected packet sequence. This may allow the "flushing" FIN packet to spill over to the next test. Let's always wait for the FIN before exiting. Reviewed-by: Willem de Bruijn Link: https://patch.msgid.link/20260402210000.1512696-4-kuba@kernel.org Signed-off-by: Jakub Kicinski --- tools/testing/selftests/net/lib/gro.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tools/testing/selftests/net/lib/gro.c b/tools/testing/selftests/net/lib/gro.c index 27ccd742615a..f05398c18e0c 100644 --- a/tools/testing/selftests/net/lib/gro.c +++ b/tools/testing/selftests/net/lib/gro.c @@ -1154,7 +1154,7 @@ static void check_capacity_pkts(int fd) memset(coalesced, 0, sizeof(coalesced)); memset(flow_order, -1, sizeof(flow_order)); - while (total_data < num_flows * CAPACITY_PAYLOAD_LEN * 2) { + while (1) { ip_ext_len = 0; pkt_size = recv(fd, buffer, IP_MAXPACKET + ETH_HLEN + 1, 0); if (pkt_size < 0) @@ -1167,7 +1167,6 @@ static void check_capacity_pkts(int fd) tcph = (struct tcphdr *)(buffer + tcp_offset + ip_ext_len); - /* FIN packet terminates reception */ if (tcph->fin) break; @@ -1189,7 +1188,13 @@ static void check_capacity_pkts(int fd) data_len = pkt_size - total_hdr_len - ip_ext_len; } - flow_order[num_pkt] = flow_id; + if (num_pkt < num_flows * 2) { + flow_order[num_pkt] = flow_id; + } else if (num_pkt == num_flows * 2) { + vlog("More packets than expected (%d)\n", + num_flows * 2); + fail_reason = fail_reason ?: "too many packets"; + } coalesced[flow_id] = data_len; if (data_len == CAPACITY_PAYLOAD_LEN * 2) { From 5469b695f23642611c47e31329025a21de86c629 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 2 Apr 2026 13:59:56 -0700 Subject: [PATCH 4/8] selftests: drv-net: gro: prepare for ip6ip6 support Try to use already calculated offsets and not depend on the ipip flag as much. This patch should not change any functionality, it's just a cleanup to make ip6ip6 support easier. Reviewed-by: Willem de Bruijn Link: https://patch.msgid.link/20260402210000.1512696-5-kuba@kernel.org Signed-off-by: Jakub Kicinski --- tools/testing/selftests/net/lib/gro.c | 73 +++++++++++++++------------ 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/tools/testing/selftests/net/lib/gro.c b/tools/testing/selftests/net/lib/gro.c index f05398c18e0c..57080ecc3df8 100644 --- a/tools/testing/selftests/net/lib/gro.c +++ b/tools/testing/selftests/net/lib/gro.c @@ -139,6 +139,14 @@ static bool order_check; #define TXTIME_DELAY_MS 5 +/* Max TCP payload that GRO will coalesce. The outer header overhead + * varies by encapsulation, reducing the effective max payload. + */ +static int max_payload(void) +{ + return IP_MAXPACKET - (total_hdr_len - ETH_HLEN); +} + static void vlog(const char *fmt, ...) { va_list args; @@ -156,15 +164,13 @@ static void setup_sock_filter(int fd) const int ethproto_off = offsetof(struct ethhdr, h_proto); int optlen = 0; int ipproto_off, opt_ipproto_off; - int next_off; - if (ipip) - next_off = sizeof(struct iphdr) + offsetof(struct iphdr, protocol); - else if (proto == PF_INET) - next_off = offsetof(struct iphdr, protocol); + if (proto == PF_INET) + ipproto_off = tcp_offset - sizeof(struct iphdr) + + offsetof(struct iphdr, protocol); else - next_off = offsetof(struct ipv6hdr, nexthdr); - ipproto_off = ETH_HLEN + next_off; + ipproto_off = tcp_offset - sizeof(struct ipv6hdr) + + offsetof(struct ipv6hdr, nexthdr); /* Overridden later if exthdrs are used: */ opt_ipproto_off = ipproto_off; @@ -381,19 +387,23 @@ static void write_packet(int fd, char *buf, int len, struct sockaddr_ll *daddr) static void create_packet(void *buf, int seq_offset, int ack_offset, int payload_len, int fin) { + int ip_hdr_len = (proto == PF_INET) ? + sizeof(struct iphdr) : sizeof(struct ipv6hdr); + int inner_ip_off = tcp_offset - ip_hdr_len; + memset(buf, 0, total_hdr_len); memset(buf + total_hdr_len, 'a', payload_len); fill_transportlayer(buf + tcp_offset, seq_offset, ack_offset, payload_len, fin); - if (ipip) { - fill_networklayer(buf + ETH_HLEN, payload_len + sizeof(struct iphdr), - IPPROTO_IPIP); - fill_networklayer(buf + ETH_HLEN + sizeof(struct iphdr), - payload_len, IPPROTO_TCP); - } else { - fill_networklayer(buf + ETH_HLEN, payload_len, IPPROTO_TCP); + fill_networklayer(buf + inner_ip_off, payload_len, IPPROTO_TCP); + if (inner_ip_off > ETH_HLEN) { + int encap_proto = (proto == PF_INET) ? + IPPROTO_IPIP : IPPROTO_IPV6; + + fill_networklayer(buf + ETH_HLEN, + payload_len + ip_hdr_len, encap_proto); } fill_datalinklayer(buf); @@ -547,8 +557,7 @@ static void send_ack(int fd, struct sockaddr_ll *daddr) static void recompute_packet(char *buf, char *no_ext, int extlen) { struct tcphdr *tcphdr = (struct tcphdr *)(buf + tcp_offset); - struct ipv6hdr *ip6h = (struct ipv6hdr *)(buf + ETH_HLEN); - struct iphdr *iph = (struct iphdr *)(buf + ETH_HLEN); + int off; memmove(buf, no_ext, total_hdr_len); memmove(buf + total_hdr_len + extlen, @@ -558,18 +567,22 @@ static void recompute_packet(char *buf, char *no_ext, int extlen) tcphdr->check = 0; tcphdr->check = tcp_checksum(tcphdr, PAYLOAD_LEN + extlen); if (proto == PF_INET) { - iph->tot_len = htons(ntohs(iph->tot_len) + extlen); - iph->check = 0; - iph->check = checksum_fold(iph, sizeof(struct iphdr), 0); + for (off = ETH_HLEN; off < tcp_offset; + off += sizeof(struct iphdr)) { + struct iphdr *iph = (struct iphdr *)(buf + off); - if (ipip) { - iph += 1; iph->tot_len = htons(ntohs(iph->tot_len) + extlen); iph->check = 0; iph->check = checksum_fold(iph, sizeof(struct iphdr), 0); } } else { - ip6h->payload_len = htons(ntohs(ip6h->payload_len) + extlen); + for (off = ETH_HLEN; off < tcp_offset; + off += sizeof(struct ipv6hdr)) { + struct ipv6hdr *ip6h = (struct ipv6hdr *)(buf + off); + + ip6h->payload_len = + htons(ntohs(ip6h->payload_len) + extlen); + } } } @@ -1425,14 +1438,12 @@ static void gro_sender(void) /* large sub-tests */ } else if (strcmp(testname, "large_max") == 0) { - int offset = (proto == PF_INET && !ipip) ? 20 : 0; - int remainder = (MAX_PAYLOAD + offset) % MSS; + int remainder = max_payload() % MSS; send_large(txfd, &daddr, remainder); write_packet(txfd, fin_pkt, total_hdr_len, &daddr); } else if (strcmp(testname, "large_rem") == 0) { - int offset = (proto == PF_INET && !ipip) ? 20 : 0; - int remainder = (MAX_PAYLOAD + offset) % MSS; + int remainder = max_payload() % MSS; send_large(txfd, &daddr, remainder + 1); write_packet(txfd, fin_pkt, total_hdr_len, &daddr); @@ -1636,19 +1647,17 @@ static void gro_receiver(void) /* large sub-tests */ } else if (strcmp(testname, "large_max") == 0) { - int offset = (proto == PF_INET && !ipip) ? 20 : 0; - int remainder = (MAX_PAYLOAD + offset) % MSS; + int remainder = max_payload() % MSS; - correct_payload[0] = (MAX_PAYLOAD + offset); + correct_payload[0] = max_payload(); correct_payload[1] = remainder; printf("Shouldn't coalesce if exceed IP max pkt size: "); check_recv_pkts(rxfd, correct_payload, 2); } else if (strcmp(testname, "large_rem") == 0) { - int offset = (proto == PF_INET && !ipip) ? 20 : 0; - int remainder = (MAX_PAYLOAD + offset) % MSS; + int remainder = max_payload() % MSS; /* last segment sent individually, doesn't start new segment */ - correct_payload[0] = (MAX_PAYLOAD + offset) - remainder; + correct_payload[0] = max_payload() - remainder; correct_payload[1] = remainder + 1; correct_payload[2] = remainder + 1; printf("last segment sent individually: "); From 166b0cc6df8c8bd961058aa3ed0c2d1a2bdaffca Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 2 Apr 2026 13:59:57 -0700 Subject: [PATCH 5/8] selftests: drv-net: gro: remove TOTAL_HDR_LEN Willem points out TOTAL_HDR_LEN is identical to MAX_HDR_LEN. This seems to have been the case ever since the test was added. Replace the uses of TOTAL_HDR_LEN with MAX_HDR_LEN, MAX seems more common for what this value is. Reviewed-by: Willem de Bruijn Link: https://patch.msgid.link/20260402210000.1512696-6-kuba@kernel.org Signed-off-by: Jakub Kicinski --- tools/testing/selftests/net/lib/gro.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tools/testing/selftests/net/lib/gro.c b/tools/testing/selftests/net/lib/gro.c index 57080ecc3df8..3616ecc42a01 100644 --- a/tools/testing/selftests/net/lib/gro.c +++ b/tools/testing/selftests/net/lib/gro.c @@ -94,7 +94,6 @@ #define START_SEQ 100 #define START_ACK 100 #define ETH_P_NONE 0 -#define TOTAL_HDR_LEN (ETH_HLEN + sizeof(struct ipv6hdr) + sizeof(struct tcphdr)) #define MSS (4096 - sizeof(struct tcphdr) - sizeof(struct ipv6hdr)) #define MAX_PAYLOAD (IP_MAXPACKET - sizeof(struct tcphdr) - sizeof(struct ipv6hdr)) #define NUM_LARGE_PKT (MAX_PAYLOAD / MSS) @@ -526,9 +525,9 @@ static void send_data_pkts(int fd, struct sockaddr_ll *daddr, */ static void send_large(int fd, struct sockaddr_ll *daddr, int remainder) { - static char pkts[NUM_LARGE_PKT][TOTAL_HDR_LEN + MSS]; - static char last[TOTAL_HDR_LEN + MSS]; - static char new_seg[TOTAL_HDR_LEN + MSS]; + static char pkts[NUM_LARGE_PKT][MAX_HDR_LEN + MSS]; + static char last[MAX_HDR_LEN + MSS]; + static char new_seg[MAX_HDR_LEN + MSS]; int i; for (i = 0; i < NUM_LARGE_PKT; i++) From 024597cc2077e36dc69327e46fa6dfb1cd0885cd Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 2 Apr 2026 13:59:58 -0700 Subject: [PATCH 6/8] selftests: drv-net: gro: make large packet math more precise When constructing the packets for large_* test cases we use a static value for packet count and MSS. It works okay for ipv4 vs ipv6 but the gap between ipv4 and ip6ip6 is going to be quite significant. Make the defines calculate the worst case values, those are only used for sizing stack arrays. Create helpers for calculating precise values based on the exact test case. Reviewed-by: Willem de Bruijn Link: https://patch.msgid.link/20260402210000.1512696-7-kuba@kernel.org Signed-off-by: Jakub Kicinski --- tools/testing/selftests/net/lib/gro.c | 45 +++++++++++++++++---------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/tools/testing/selftests/net/lib/gro.c b/tools/testing/selftests/net/lib/gro.c index 3616ecc42a01..dc8638d5d74d 100644 --- a/tools/testing/selftests/net/lib/gro.c +++ b/tools/testing/selftests/net/lib/gro.c @@ -94,10 +94,11 @@ #define START_SEQ 100 #define START_ACK 100 #define ETH_P_NONE 0 -#define MSS (4096 - sizeof(struct tcphdr) - sizeof(struct ipv6hdr)) -#define MAX_PAYLOAD (IP_MAXPACKET - sizeof(struct tcphdr) - sizeof(struct ipv6hdr)) -#define NUM_LARGE_PKT (MAX_PAYLOAD / MSS) +#define ASSUMED_MTU 4096 +#define MAX_MSS (ASSUMED_MTU - sizeof(struct iphdr) - sizeof(struct tcphdr)) #define MAX_HDR_LEN (ETH_HLEN + sizeof(struct ipv6hdr) + sizeof(struct tcphdr)) +#define MAX_LARGE_PKT_CNT ((IP_MAXPACKET - (MAX_HDR_LEN - ETH_HLEN)) / \ + (ASSUMED_MTU - (MAX_HDR_LEN - ETH_HLEN))) #define MIN_EXTHDR_SIZE 8 #define EXT_PAYLOAD_1 "\x00\x00\x00\x00\x00\x00" #define EXT_PAYLOAD_2 "\x11\x11\x11\x11\x11\x11" @@ -146,6 +147,16 @@ static int max_payload(void) return IP_MAXPACKET - (total_hdr_len - ETH_HLEN); } +static int calc_mss(void) +{ + return ASSUMED_MTU - (total_hdr_len - ETH_HLEN); +} + +static int num_large_pkt(void) +{ + return max_payload() / calc_mss(); +} + static void vlog(const char *fmt, ...) { va_list args; @@ -525,18 +536,20 @@ static void send_data_pkts(int fd, struct sockaddr_ll *daddr, */ static void send_large(int fd, struct sockaddr_ll *daddr, int remainder) { - static char pkts[NUM_LARGE_PKT][MAX_HDR_LEN + MSS]; - static char last[MAX_HDR_LEN + MSS]; - static char new_seg[MAX_HDR_LEN + MSS]; + static char pkts[MAX_LARGE_PKT_CNT][MAX_HDR_LEN + MAX_MSS]; + static char new_seg[MAX_HDR_LEN + MAX_MSS]; + static char last[MAX_HDR_LEN + MAX_MSS]; + const int num_pkt = num_large_pkt(); + const int mss = calc_mss(); int i; - for (i = 0; i < NUM_LARGE_PKT; i++) - create_packet(pkts[i], i * MSS, 0, MSS, 0); - create_packet(last, NUM_LARGE_PKT * MSS, 0, remainder, 0); - create_packet(new_seg, (NUM_LARGE_PKT + 1) * MSS, 0, remainder, 0); + for (i = 0; i < num_pkt; i++) + create_packet(pkts[i], i * mss, 0, mss, 0); + create_packet(last, num_pkt * mss, 0, remainder, 0); + create_packet(new_seg, (num_pkt + 1) * mss, 0, remainder, 0); - for (i = 0; i < NUM_LARGE_PKT; i++) - write_packet(fd, pkts[i], total_hdr_len + MSS, daddr); + for (i = 0; i < num_pkt; i++) + write_packet(fd, pkts[i], total_hdr_len + mss, daddr); write_packet(fd, last, total_hdr_len + remainder, daddr); write_packet(fd, new_seg, total_hdr_len + remainder, daddr); } @@ -1437,12 +1450,12 @@ static void gro_sender(void) /* large sub-tests */ } else if (strcmp(testname, "large_max") == 0) { - int remainder = max_payload() % MSS; + int remainder = max_payload() % calc_mss(); send_large(txfd, &daddr, remainder); write_packet(txfd, fin_pkt, total_hdr_len, &daddr); } else if (strcmp(testname, "large_rem") == 0) { - int remainder = max_payload() % MSS; + int remainder = max_payload() % calc_mss(); send_large(txfd, &daddr, remainder + 1); write_packet(txfd, fin_pkt, total_hdr_len, &daddr); @@ -1646,14 +1659,14 @@ static void gro_receiver(void) /* large sub-tests */ } else if (strcmp(testname, "large_max") == 0) { - int remainder = max_payload() % MSS; + int remainder = max_payload() % calc_mss(); correct_payload[0] = max_payload(); correct_payload[1] = remainder; printf("Shouldn't coalesce if exceed IP max pkt size: "); check_recv_pkts(rxfd, correct_payload, 2); } else if (strcmp(testname, "large_rem") == 0) { - int remainder = max_payload() % MSS; + int remainder = max_payload() % calc_mss(); /* last segment sent individually, doesn't start new segment */ correct_payload[0] = max_payload() - remainder; From 9a84a4047df79f7f58a4915abaf09d1089103233 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 2 Apr 2026 13:59:59 -0700 Subject: [PATCH 7/8] selftests: drv-net: gro: test ip6ip6 We explicitly test ipip encap. Let's add ip6ip6, too. Having just ipip seems like favoring IPv4 which we should not do :) Testing all combinations is left for future work, not sure it's actually worth it. Reviewed-by: Willem de Bruijn Link: https://patch.msgid.link/20260402210000.1512696-8-kuba@kernel.org Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/gro.py | 2 +- tools/testing/selftests/net/lib/gro.c | 23 +++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/tools/testing/selftests/drivers/net/gro.py b/tools/testing/selftests/drivers/net/gro.py index 73436d16b40d..ee95144b73ac 100755 --- a/tools/testing/selftests/drivers/net/gro.py +++ b/tools/testing/selftests/drivers/net/gro.py @@ -313,7 +313,7 @@ def _gro_variants(): ] for mode in ["sw", "hw", "lro"]: - for protocol in ["ipv4", "ipv6", "ipip"]: + for protocol in ["ipv4", "ipv6", "ipip", "ip6ip6"]: for test_name in common_tests: yield mode, protocol, test_name diff --git a/tools/testing/selftests/net/lib/gro.c b/tools/testing/selftests/net/lib/gro.c index dc8638d5d74d..b99c0f00b8fe 100644 --- a/tools/testing/selftests/net/lib/gro.c +++ b/tools/testing/selftests/net/lib/gro.c @@ -96,7 +96,8 @@ #define ETH_P_NONE 0 #define ASSUMED_MTU 4096 #define MAX_MSS (ASSUMED_MTU - sizeof(struct iphdr) - sizeof(struct tcphdr)) -#define MAX_HDR_LEN (ETH_HLEN + sizeof(struct ipv6hdr) + sizeof(struct tcphdr)) +#define MAX_HDR_LEN \ + (ETH_HLEN + sizeof(struct ipv6hdr) * 2 + sizeof(struct tcphdr)) #define MAX_LARGE_PKT_CNT ((IP_MAXPACKET - (MAX_HDR_LEN - ETH_HLEN)) / \ (ASSUMED_MTU - (MAX_HDR_LEN - ETH_HLEN))) #define MIN_EXTHDR_SIZE 8 @@ -131,6 +132,7 @@ static int tcp_offset = -1; static int total_hdr_len = -1; static int ethhdr_proto = -1; static bool ipip; +static bool ip6ip6; static uint64_t txtime_ns; static int num_flows = 4; static bool order_check; @@ -1125,7 +1127,8 @@ static void check_recv_pkts(int fd, int *correct_payload, if (iph->version == 4) ip_ext_len = (iph->ihl - 5) * 4; - else if (ip6h->version == 6 && ip6h->nexthdr != IPPROTO_TCP) + else if (ip6h->version == 6 && !ip6ip6 && + ip6h->nexthdr != IPPROTO_TCP) ip_ext_len = MIN_EXTHDR_SIZE; tcph = (struct tcphdr *)(buffer + tcp_offset + ip_ext_len); @@ -1187,7 +1190,8 @@ static void check_capacity_pkts(int fd) if (iph->version == 4) ip_ext_len = (iph->ihl - 5) * 4; - else if (ip6h->version == 6 && ip6h->nexthdr != IPPROTO_TCP) + else if (ip6h->version == 6 && !ip6ip6 && + ip6h->nexthdr != IPPROTO_TCP) ip_ext_len = MIN_EXTHDR_SIZE; tcph = (struct tcphdr *)(buffer + tcp_offset + ip_ext_len); @@ -1700,6 +1704,7 @@ static void parse_args(int argc, char **argv) { "ipv4", no_argument, NULL, '4' }, { "ipv6", no_argument, NULL, '6' }, { "ipip", no_argument, NULL, 'e' }, + { "ip6ip6", no_argument, NULL, 'E' }, { "num-flows", required_argument, NULL, 'n' }, { "rx", no_argument, NULL, 'r' }, { "saddr", required_argument, NULL, 's' }, @@ -1711,7 +1716,7 @@ static void parse_args(int argc, char **argv) }; int c; - while ((c = getopt_long(argc, argv, "46d:D:ei:n:rs:S:t:ov", opts, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "46d:D:eEi:n:rs:S:t:ov", opts, NULL)) != -1) { switch (c) { case '4': proto = PF_INET; @@ -1726,6 +1731,11 @@ static void parse_args(int argc, char **argv) proto = PF_INET; ethhdr_proto = htons(ETH_P_IP); break; + case 'E': + ip6ip6 = true; + proto = PF_INET6; + ethhdr_proto = htons(ETH_P_IPV6); + break; case 'd': addr4_dst = addr6_dst = optarg; break; @@ -1770,12 +1780,15 @@ int main(int argc, char **argv) if (ipip) { tcp_offset = ETH_HLEN + sizeof(struct iphdr) * 2; total_hdr_len = tcp_offset + sizeof(struct tcphdr); + } else if (ip6ip6) { + tcp_offset = ETH_HLEN + sizeof(struct ipv6hdr) * 2; + total_hdr_len = tcp_offset + sizeof(struct tcphdr); } else if (proto == PF_INET) { tcp_offset = ETH_HLEN + sizeof(struct iphdr); total_hdr_len = tcp_offset + sizeof(struct tcphdr); } else if (proto == PF_INET6) { tcp_offset = ETH_HLEN + sizeof(struct ipv6hdr); - total_hdr_len = MAX_HDR_LEN; + total_hdr_len = tcp_offset + sizeof(struct tcphdr); } else { error(1, 0, "Protocol family is not ipv4 or ipv6"); } From 764d0833e795916ffe33906ace17bab027c093f8 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 2 Apr 2026 14:00:00 -0700 Subject: [PATCH 8/8] selftests: drv-net: gro: add a test for bad IPv4 csum We have a test for coalescing with bad TCP checksum, let's also test bad IPv4 header checksum. Reviewed-by: Willem de Bruijn Link: https://patch.msgid.link/20260402210000.1512696-9-kuba@kernel.org Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/gro.py | 1 + tools/testing/selftests/net/lib/gro.c | 29 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/tools/testing/selftests/drivers/net/gro.py b/tools/testing/selftests/drivers/net/gro.py index ee95144b73ac..221f27e57147 100755 --- a/tools/testing/selftests/drivers/net/gro.py +++ b/tools/testing/selftests/drivers/net/gro.py @@ -301,6 +301,7 @@ def _gro_variants(): # Tests specific to IPv4 ipv4_tests = [ + "ip_csum", "ip_ttl", "ip_opt", "ip_frag4", "ip_id_df1_inc", "ip_id_df1_fixed", "ip_id_df0_inc", "ip_id_df0_fixed", diff --git a/tools/testing/selftests/net/lib/gro.c b/tools/testing/selftests/net/lib/gro.c index b99c0f00b8fe..11b16ae5f0e8 100644 --- a/tools/testing/selftests/net/lib/gro.c +++ b/tools/testing/selftests/net/lib/gro.c @@ -36,6 +36,7 @@ * Packets with different (ECN, TTL, TOS) header, IP options or * IP fragments shouldn't coalesce. * - ip_ecn, ip_tos: shared between IPv4/IPv6 + * - ip_csum: IPv4 only, bad IP header checksum * - ip_ttl, ip_opt, ip_frag4: IPv4 only * - ip_id_df*: IPv4 IP ID field coalescing tests * - ip_frag6, ip_v6ext_*: IPv6 only @@ -685,6 +686,24 @@ static void send_changed_checksum(int fd, struct sockaddr_ll *daddr) write_packet(fd, buf, pkt_size, daddr); } +/* Packets with incorrect IPv4 header checksum don't coalesce. */ +static void send_changed_ip_checksum(int fd, struct sockaddr_ll *daddr) +{ + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + struct iphdr *iph = (struct iphdr *)(buf + ETH_HLEN); + int pkt_size = total_hdr_len + PAYLOAD_LEN; + + create_packet(buf, 0, 0, PAYLOAD_LEN, 0); + write_packet(fd, buf, pkt_size, daddr); + + create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); + iph->check = iph->check - 1; + write_packet(fd, buf, pkt_size, daddr); + + create_packet(buf, PAYLOAD_LEN * 2, 0, PAYLOAD_LEN, 0); + write_packet(fd, buf, pkt_size, daddr); +} + /* Packets with non-consecutive sequence number don't coalesce.*/ static void send_changed_seq(int fd, struct sockaddr_ll *daddr) { @@ -1402,6 +1421,10 @@ static void gro_sender(void) write_packet(txfd, fin_pkt, total_hdr_len, &daddr); /* ip sub-tests - IPv4 only */ + } else if (strcmp(testname, "ip_csum") == 0) { + send_changed_ip_checksum(txfd, &daddr); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); } else if (strcmp(testname, "ip_ttl") == 0) { send_changed_ttl(txfd, &daddr); write_packet(txfd, fin_pkt, total_hdr_len, &daddr); @@ -1598,6 +1621,12 @@ static void gro_receiver(void) check_recv_pkts(rxfd, correct_payload, 2); /* ip sub-tests - IPv4 only */ + } else if (strcmp(testname, "ip_csum") == 0) { + correct_payload[0] = PAYLOAD_LEN; + correct_payload[1] = PAYLOAD_LEN; + correct_payload[2] = PAYLOAD_LEN; + printf("bad ip checksum doesn't coalesce: "); + check_recv_pkts(rxfd, correct_payload, 3); } else if (strcmp(testname, "ip_ttl") == 0) { correct_payload[0] = PAYLOAD_LEN; correct_payload[1] = PAYLOAD_LEN;