The GSO engine can now be asked to group several MSS segments into one output skb, which is what re-segmentation needs. Add KUnit coverage for the max_segs parameter.
The parameterized GSO test gains a max_segs input and three cases: two limits which cut the input into two and three output skbs, and a max_segs of one MSS, which must leave the output unchanged. It drives skb_segment() directly, because the synthetic protocol it uses has no gso_segment callback, and stores max_segs in the GSO control block itself. The TCP test drives __skb_gso_segment() with a max_segs of two MSS and checks that every output skb stays GSO, keeps its gso_size, and stays within the limit. The length test runs a 200 KiB TCP skb through validate_xmit_skb_list(), the caller which sets max_segs, and checks that the length declared by every output matches the L3 length of that output. The limit test checks that the GSO size limit which netif_skb_features() applies follows the packet's L3 protocol, for an IPv4 and an IPv6 skb, also with the tag inside the frame. Assisted-by: LLM Signed-off-by: Wang Zhan <[email protected]> --- v4: - name the cases and the descriptions after re-segmentation - reserve the headroom the VLAN step needs in both builders - check the exact gso_segs of each output - free the segments when a bounded case does not match v3: https://lore.kernel.org/[email protected]/ v2: https://lore.kernel.org/[email protected]/ v1: https://lore.kernel.org/[email protected]/ --- net/core/net_test.c | 351 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) diff --git a/net/core/net_test.c b/net/core/net_test.c index 9c3a590865d269..6a91db83e53017 100644 --- a/net/core/net_test.c +++ b/net/core/net_test.c @@ -4,7 +4,14 @@ /* GSO */ +#include <linux/if_ether.h> +#include <linux/if_vlan.h> +#include <linux/ip.h> +#include <linux/ipv6.h> +#include <linux/netdevice.h> #include <linux/skbuff.h> +#include <linux/tcp.h> +#include <net/gso.h> static const char hdr[] = "abcdefgh"; #define GSO_TEST_SIZE 1000 @@ -34,6 +41,9 @@ enum gso_test_nr { GSO_TEST_FRAG_LIST_PURE, GSO_TEST_FRAG_LIST_NON_UNIFORM, GSO_TEST_GSO_BY_FRAGS, + GSO_TEST_RESEGMENT, + GSO_TEST_RESEGMENT_MULTI, + GSO_TEST_RESEGMENT_ONE_MSS, }; struct gso_test_case { @@ -46,10 +56,12 @@ struct gso_test_case { const unsigned int *frags; unsigned int nr_frag_skbs; const unsigned int *frag_skbs; + unsigned int max_segs; /* output as expected */ unsigned int nr_segs; const unsigned int *segs; + bool segs_are_gso; }; static struct gso_test_case cases[] = { @@ -135,6 +147,51 @@ static struct gso_test_case cases[] = { .nr_segs = 4, .segs = (const unsigned int[]) { 100, 200, 300, 400 }, }, + { + .id = GSO_TEST_RESEGMENT, + .name = "resegment", + .linear_len = GSO_TEST_SIZE, + .nr_frags = 3, + .frags = (const unsigned int[]) { + GSO_TEST_SIZE, GSO_TEST_SIZE, 3, + }, + .max_segs = 2, + .nr_segs = 2, + .segs = (const unsigned int[]) { + 2 * GSO_TEST_SIZE, GSO_TEST_SIZE + 3, + }, + .segs_are_gso = true, + }, + { + .id = GSO_TEST_RESEGMENT_MULTI, + .name = "resegment_multi", + .linear_len = 2 * GSO_TEST_SIZE, + .nr_frags = 4, + .frags = (const unsigned int[]) { + GSO_TEST_SIZE, GSO_TEST_SIZE, GSO_TEST_SIZE, 3, + }, + .max_segs = 2, + .nr_segs = 3, + .segs = (const unsigned int[]) { + 2 * GSO_TEST_SIZE, 2 * GSO_TEST_SIZE, GSO_TEST_SIZE + 3, + }, + .segs_are_gso = true, + }, + { + /* A max_segs of one leaves one MSS per skb, like no limit. */ + .id = GSO_TEST_RESEGMENT_ONE_MSS, + .name = "resegment_one_mss", + .linear_len = GSO_TEST_SIZE, + .nr_frags = 3, + .frags = (const unsigned int[]) { + GSO_TEST_SIZE, GSO_TEST_SIZE, 3, + }, + .max_segs = 1, + .nr_segs = 4, + .segs = (const unsigned int[]) { + GSO_TEST_SIZE, GSO_TEST_SIZE, GSO_TEST_SIZE, 3, + }, + }, }; static void gso_test_case_to_desc(struct gso_test_case *t, char *desc) @@ -226,6 +283,7 @@ static void gso_test_func(struct kunit *test) if (tcase->id == GSO_TEST_FRAG_LIST_NON_UNIFORM) features &= ~NETIF_F_SG; + SKB_GSO_CB(skb)->max_segs = tcase->max_segs; segs = skb_segment(skb, features); if (IS_ERR(segs)) { KUNIT_FAIL(test, "segs error %pe", segs); @@ -239,6 +297,7 @@ static void gso_test_func(struct kunit *test) for (cur = segs, i = 0; cur; cur = next, i++) { next = cur->next; + KUNIT_ASSERT_LT(test, i, tcase->nr_segs); KUNIT_ASSERT_EQ(test, cur->len, sizeof(hdr) + tcase->segs[i]); /* segs have skb->data pointing to the mac header */ @@ -247,6 +306,18 @@ static void gso_test_func(struct kunit *test) /* header was copied to all segs */ KUNIT_ASSERT_EQ(test, memcmp(skb_mac_header(cur), hdr, sizeof(hdr)), 0); + if (tcase->segs_are_gso) { + KUNIT_EXPECT_TRUE(test, skb_is_gso(cur)); + KUNIT_EXPECT_EQ(test, skb_shinfo(cur)->gso_size, + GSO_TEST_SIZE); + KUNIT_EXPECT_EQ(test, skb_shinfo(cur)->gso_segs, + DIV_ROUND_UP(tcase->segs[i], + GSO_TEST_SIZE)); + KUNIT_EXPECT_FALSE(test, skb_shinfo(cur)->gso_type & + SKB_GSO_PARTIAL); + } else if (tcase->max_segs) { + KUNIT_EXPECT_FALSE(test, skb_is_gso(cur)); + } /* last seg can be found through segs->prev pointer */ if (!next) @@ -261,6 +332,283 @@ static void gso_test_func(struct kunit *test) consume_skb(skb); } +#define GSO_TCP_HDR_LEN \ + (ETH_HLEN + sizeof(struct iphdr) + sizeof(struct tcphdr)) + +static struct sk_buff *gso_tcp_skb_new(unsigned int payload_len) +{ + struct sk_buff *skb; + struct ethhdr *eth; + struct tcphdr *th; + struct iphdr *iph; + + skb = alloc_skb(NET_SKB_PAD + GSO_TCP_HDR_LEN + payload_len, + GFP_KERNEL); + if (!skb) + return NULL; + skb_reserve(skb, NET_SKB_PAD); + skb_put_zero(skb, GSO_TCP_HDR_LEN + payload_len); + + skb_reset_mac_header(skb); + eth = eth_hdr(skb); + eth->h_proto = htons(ETH_P_IP); + skb->protocol = eth->h_proto; + + skb_set_network_header(skb, ETH_HLEN); + iph = ip_hdr(skb); + iph->version = 4; + iph->ihl = sizeof(*iph) / 4; + iph->protocol = IPPROTO_TCP; + iph->tot_len = htons(sizeof(*iph) + sizeof(*th) + payload_len); + + skb_set_transport_header(skb, ETH_HLEN + sizeof(*iph)); + th = tcp_hdr(skb); + th->doff = sizeof(*th) / 4; + + skb->ip_summed = CHECKSUM_PARTIAL; + skb->csum_start = skb_transport_header(skb) - skb->head; + skb->csum_offset = offsetof(struct tcphdr, check); + skb_shinfo(skb)->gso_type = SKB_GSO_TCPV4; + skb_shinfo(skb)->gso_size = GSO_TEST_SIZE; + skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(payload_len, GSO_TEST_SIZE); + + return skb; +} + +/* + * The transmit path passes the features of the device which rejects this + * skb, with the GSO bits cleared, so the engine segments it. + */ +static void gso_test_tcp_resegment(struct kunit *test) +{ + netdev_features_t features = NETIF_F_SG | NETIF_F_HW_CSUM; + const unsigned int payload_len = 3 * GSO_TEST_SIZE + 3; + struct sk_buff *skb, *segs, *cur, *next; + const unsigned int expected[] = { + 2 * GSO_TEST_SIZE, GSO_TEST_SIZE + 3, + }; + const unsigned int max_segs = 2; + int i = 0; + + if (!IS_ENABLED(CONFIG_INET)) + kunit_skip(test, "requires CONFIG_INET"); + + skb = gso_tcp_skb_new(payload_len); + if (!skb) { + KUNIT_FAIL(test, "no skb"); + return; + } + + segs = __skb_gso_segment(skb, features, true, max_segs); + if (IS_ERR_OR_NULL(segs)) { + KUNIT_FAIL(test, "segs error %pe", segs); + consume_skb(skb); + return; + } + + for (cur = segs; cur; cur = next, i++) { + next = cur->next; + + if (i < ARRAY_SIZE(expected)) { + KUNIT_EXPECT_EQ(test, cur->len, + GSO_TCP_HDR_LEN + expected[i]); + KUNIT_EXPECT_EQ(test, skb_shinfo(cur)->gso_segs, + DIV_ROUND_UP(expected[i], + GSO_TEST_SIZE)); + } + KUNIT_EXPECT_TRUE(test, skb_is_gso(cur)); + KUNIT_EXPECT_EQ(test, skb_shinfo(cur)->gso_size, + GSO_TEST_SIZE); + + consume_skb(cur); + } + + KUNIT_EXPECT_EQ(test, i, ARRAY_SIZE(expected)); + consume_skb(skb); +} + +#define GSO_TCP6_HDR_LEN \ + (ETH_HLEN + sizeof(struct ipv6hdr) + sizeof(struct tcphdr)) + +static struct sk_buff *gso_tcp6_skb_new(unsigned int payload_len) +{ + struct ipv6hdr *ip6h; + struct sk_buff *skb; + struct ethhdr *eth; + struct tcphdr *th; + + skb = alloc_skb(NET_SKB_PAD + GSO_TCP6_HDR_LEN + payload_len, + GFP_KERNEL); + if (!skb) + return NULL; + skb_reserve(skb, NET_SKB_PAD); + skb_put_zero(skb, GSO_TCP6_HDR_LEN + payload_len); + + skb_reset_mac_header(skb); + eth = eth_hdr(skb); + eth->h_proto = htons(ETH_P_IPV6); + skb->protocol = eth->h_proto; + + skb_set_network_header(skb, ETH_HLEN); + ip6h = ipv6_hdr(skb); + ip6h->version = 6; + ip6h->nexthdr = IPPROTO_TCP; + ip6h->payload_len = htons(sizeof(*th) + payload_len); + + skb_set_transport_header(skb, ETH_HLEN + sizeof(*ip6h)); + th = tcp_hdr(skb); + th->doff = sizeof(*th) / 4; + + skb->ip_summed = CHECKSUM_PARTIAL; + skb->csum_start = skb_transport_header(skb) - skb->head; + skb->csum_offset = offsetof(struct tcphdr, check); + skb_shinfo(skb)->gso_type = SKB_GSO_TCPV6; + skb_shinfo(skb)->gso_size = GSO_TEST_SIZE; + skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(payload_len, GSO_TEST_SIZE); + + return skb; +} + +/* + * The device GSO size limit is per L3 protocol, so only a lowered limit of + * the packet's own protocol may cost it the TSO bit, and a tag inside the + * frame, which replaces skb->protocol with the ethertype, must not change + * which limit applies. Takes ownership of @skb. + */ +static void gso_test_tcp_l3_limit(struct kunit *test, struct net_device *dev, + struct sk_buff *skb, netdev_features_t tso, + bool ipv6) +{ + unsigned int other = GSO_LEGACY_MAX_SIZE; + unsigned int own = GSO_MAX_SIZE; + + /* The skb fits the limit of its own L3 protocol, not the other's. */ + dev->gso_max_size = ipv6 ? own : other; + dev->gso_ipv4_max_size = ipv6 ? other : own; + KUNIT_EXPECT_TRUE(test, netif_skb_features(skb) & tso); + + /* ...and the other way around. */ + swap(own, other); + dev->gso_max_size = ipv6 ? own : other; + dev->gso_ipv4_max_size = ipv6 ? other : own; + KUNIT_EXPECT_FALSE(test, netif_skb_features(skb) & tso); + + skb = vlan_insert_tag_set_proto(skb, htons(ETH_P_8021Q), 0); + if (!skb) { + KUNIT_FAIL(test, "no tagged skb"); + return; + } + KUNIT_EXPECT_TRUE(test, skb->protocol == htons(ETH_P_8021Q)); + + swap(own, other); + dev->gso_max_size = ipv6 ? own : other; + dev->gso_ipv4_max_size = ipv6 ? other : own; + KUNIT_EXPECT_TRUE(test, netif_skb_features(skb) & tso); + + swap(own, other); + dev->gso_max_size = ipv6 ? own : other; + dev->gso_ipv4_max_size = ipv6 ? other : own; + KUNIT_EXPECT_FALSE(test, netif_skb_features(skb) & tso); + + consume_skb(skb); +} + +static void gso_test_tcp_limit_l3_proto(struct kunit *test) +{ + static const struct net_device_ops dummy_netdev_ops = { }; + const unsigned int payload_len = 100 * 1024; + struct net_device *dev; + struct sk_buff *skb; + + dev = alloc_etherdev(0); + if (!dev) { + KUNIT_FAIL(test, "no net_device"); + return; + } + dev->netdev_ops = &dummy_netdev_ops; + dev->hw_features = NETIF_F_SG | NETIF_F_HW_CSUM | NETIF_F_TSO | + NETIF_F_TSO6; + dev->features = dev->hw_features; + dev->vlan_features = dev->hw_features; + + skb = gso_tcp6_skb_new(payload_len); + if (!skb) { + KUNIT_FAIL(test, "no IPv6 skb"); + goto free_dev; + } + skb->dev = dev; + gso_test_tcp_l3_limit(test, dev, skb, NETIF_F_TSO6, true); + + skb = gso_tcp_skb_new(payload_len); + if (!skb) { + KUNIT_FAIL(test, "no IPv4 skb"); + goto free_dev; + } + skb->dev = dev; + gso_test_tcp_l3_limit(test, dev, skb, NETIF_F_TSO, false); + +free_dev: + free_netdev(dev); +} + +/* + * A re-segmented output is a plain GSO skb, so the whole length lands in the + * 16-bit L3 length field. A device which accepts GSO skbs above 64 KiB would + * otherwise be handed outputs whose length field truncates. + */ +static void gso_test_tcp_resegment_l3_len(struct kunit *test) +{ + static const struct net_device_ops dummy_netdev_ops = { }; + const unsigned int payload_len = 200 * 1024; + struct sk_buff *skb, *segs, *cur, *next; + unsigned int nr_segs = 0; + struct net_device *dev; + bool again = false; + + if (!IS_ENABLED(CONFIG_INET)) + kunit_skip(test, "requires CONFIG_INET"); + + dev = alloc_etherdev(0); + if (!dev) { + KUNIT_FAIL(test, "no net_device"); + return; + } + dev->netdev_ops = &dummy_netdev_ops; + dev->hw_features = NETIF_F_SG | NETIF_F_HW_CSUM | NETIF_F_TSO; + dev->features = dev->hw_features; + dev->vlan_features = dev->hw_features; + dev->gso_ipv4_max_size = 120 * 1024; + + skb = gso_tcp_skb_new(payload_len); + if (!skb) { + KUNIT_FAIL(test, "no skb"); + goto free_dev; + } + skb->dev = dev; + + segs = validate_xmit_skb_list(skb, dev, &again); + if (IS_ERR_OR_NULL(segs)) { + KUNIT_FAIL(test, "segs error %pe", segs); + goto free_dev; + } + + for (cur = segs; cur; cur = next, nr_segs++) { + next = cur->next; + cur->next = NULL; + + KUNIT_EXPECT_TRUE(test, skb_is_gso(cur)); + KUNIT_EXPECT_EQ(test, ntohs(ip_hdr(cur)->tot_len), + cur->len - (skb_network_header(cur) - + skb_mac_header(cur))); + consume_skb(cur); + } + + KUNIT_EXPECT_EQ(test, nr_segs, 4); + +free_dev: + free_netdev(dev); +} + /* IP tunnel flags */ #include <net/ip_tunnels.h> @@ -372,6 +720,9 @@ static void ip_tunnel_flags_test_run(struct kunit *test) static struct kunit_case net_test_cases[] = { KUNIT_CASE_PARAM(gso_test_func, gso_test_gen_params), + KUNIT_CASE(gso_test_tcp_resegment), + KUNIT_CASE(gso_test_tcp_limit_l3_proto), + KUNIT_CASE(gso_test_tcp_resegment_l3_len), KUNIT_CASE_PARAM(ip_tunnel_flags_test_run, ip_tunnel_flags_test_gen_params), { }, -- 2.47.3 _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
