From: Eric Dumazet <edumazet@kernel.org>
To: netdev@vger.kernel.org, "David S. Miller" <davem@davemloft.net>,
Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>
Cc: "Eric Dumazet" <edumazet@kernel.org>,
"Simon Horman" <horms@kernel.org>,
"Willem de Bruijn" <willemb@google.com>,
"Michael S. Tsirkin" <mst@redhat.com>,
"Jason Wang" <jasowangio@gmail.com>,
"Eugenio Pérez" <eperezma@redhat.com>,
"Hassan Sultan" <hassansultan@google.com>,
virtualization@lists.linux.dev
Subject: [PATCH net v3 5/8] selftests: net: tun: test outer protocol of UDP tunnel GSO packets
Date: Thu, 5 Nov 2026 12:57:12 +0100 [thread overview]
Message-ID: <20261105115715.1466924-6-edumazet@kernel.org> (raw)
In-Reply-To: <20261105115715.1466924-1-edumazet@kernel.org>
Add tests writing single 4in4 Geneve GSO frames to tun devices with
UDP tunnel offloads enabled.
tun_vnet_udptnl_proto uses a tap device:
- outer_proto_ok: an outer IPv4 header, with or without an 802.1Q tag,
is accepted.
- outer_proto_bad: an outer ethertype that does not match the declared
tunnel type (IPv6 on an IPv4 tunnel, or NSH with or without a VLAN
tag) is rejected with EINVAL.
tun_vnet_udptnl_proto_l3 uses an IFF_TUN device with a tun_pi header,
so skb->protocol comes from userspace and there is no Ethernet header.
The outer IPv4 header carries options, as virtio_net_hdr_tnl_to_skb()
expects room for an Ethernet header before the outer transport header.
- outer_proto_ok: tun_pi and the frame agree on IPv4, accepted.
- outer_proto_bad: tun_pi claims IPv6, or the IP version of the frame
is not 4 while tun_pi claims IPv4: rejected with EINVAL.
Without the first patch of this series, both outer_proto_bad tests fail
because the kernel accepts the frames.
Reviewed-by: Willem de Bruijn <willemb@google.com>
Signed-off-by: Eric Dumazet <edumazet@kernel.org>
---
tools/testing/selftests/net/tun.c | 269 ++++++++++++++++++++++++++++++
1 file changed, 269 insertions(+)
diff --git a/tools/testing/selftests/net/tun.c b/tools/testing/selftests/net/tun.c
index 34de2580e32e..81410afbbcc8 100644
--- a/tools/testing/selftests/net/tun.c
+++ b/tools/testing/selftests/net/tun.c
@@ -683,6 +683,275 @@ TEST_F(tun_vnet_gso, vlan_tcpv4_gso_no_csum_64k)
ASSERT_EQ(ret, len);
}
+#ifndef ETH_P_NSH
+#define ETH_P_NSH 0x894F
+#endif
+
+FIXTURE(tun_vnet_udptnl_proto)
+{
+ char ifname[IFNAMSIZ];
+ uint8_t *pkt;
+ int fd;
+};
+
+FIXTURE_SETUP(tun_vnet_udptnl_proto)
+{
+ int flags = IFF_TAP | IFF_NO_PI | IFF_VNET_HDR;
+ int features = TUN_F_CSUM | TUN_F_UDP_TUNNEL_GSO |
+ TUN_F_UDP_TUNNEL_GSO_CSUM | TUN_F_USO4 | TUN_F_USO6;
+
+ self->fd = -1;
+ if (unshare(CLONE_NEWNET))
+ SKIP(return, "Cannot create network namespace: %s",
+ strerror(errno));
+
+ self->pkt = calloc(1, MAX_VNET_TUNNEL_PACKET_SZ);
+ ASSERT_NE(self->pkt, NULL);
+
+ memset(self->ifname, 0, sizeof(self->ifname));
+ self->fd = tun_open(self->ifname, flags, TUN_VNET_TNL_SIZE, features,
+ param_hwaddr_outer_src);
+ ASSERT_GE(self->fd, 0);
+}
+
+FIXTURE_TEARDOWN(tun_vnet_udptnl_proto)
+{
+ if (self->fd >= 0)
+ close(self->fd);
+ free(self->pkt);
+}
+
+/* Build a 4in4 Geneve GSO frame whose outer ethertype is @l3proto,
+ * optionally behind an 802.1Q tag.
+ */
+static int build_udptnl_frame(uint8_t *buf, bool vlan, uint16_t l3proto,
+ int payload_len)
+{
+ struct virtio_net_hdr_v1_hash_tunnel *vh = (void *)buf;
+ int vlan_len = vlan ? 4 : 0;
+ int hlen = ETH_HLEN + vlan_len + UDP_TUNNEL_GENEVE_4IN4_HDRLEN;
+ uint8_t *outer_udph, *cur = buf;
+ int pktlen;
+
+ /* The buffer is reused: do not leave bytes from a previous frame. */
+ memset(buf, 0, TUN_VNET_TNL_SIZE + hlen + payload_len);
+
+ cur += build_virtio_net_hdr_v1_hash_tunnel(cur, true, hlen, 1400,
+ AF_INET, AF_INET);
+ vh->outer_th_offset += vlan_len;
+ vh->inner_nh_offset += vlan_len;
+ vh->hash_hdr.hdr.csum_start += vlan_len;
+
+ if (vlan) {
+ uint16_t vlan_tag[2] = { htons(100), htons(l3proto) };
+
+ cur += build_eth(cur, ETH_P_8021Q, param_hwaddr_outer_dst,
+ param_hwaddr_outer_src);
+ memcpy(cur, vlan_tag, sizeof(vlan_tag));
+ cur += sizeof(vlan_tag);
+ } else {
+ cur += build_eth(cur, l3proto, param_hwaddr_outer_dst,
+ param_hwaddr_outer_src);
+ }
+
+ pktlen = UDP_TUNNEL_GENEVE_4IN4_HDRLEN - sizeof(struct iphdr) +
+ payload_len;
+ cur += build_ipv4_header(cur, IPPROTO_UDP, pktlen,
+ ¶m_ipaddr4_outer_dst,
+ ¶m_ipaddr4_outer_src);
+
+ outer_udph = cur;
+ cur += build_udp_header(cur, UDP_SRC_PORT, VN_PORT,
+ pktlen - sizeof(struct udphdr));
+ cur += build_geneve_header(cur, VN_ID);
+ cur += build_eth(cur, ETH_P_IP, param_hwaddr_inner_dst,
+ param_hwaddr_inner_src);
+ cur += build_ipv4_header(cur, IPPROTO_UDP,
+ sizeof(struct udphdr) + payload_len,
+ ¶m_ipaddr4_inner_dst,
+ ¶m_ipaddr4_inner_src);
+ cur += build_udp_packet(cur, UDP_DST_PORT, UDP_SRC_PORT, payload_len,
+ AF_INET, false);
+ build_udp_packet_csum(outer_udph, AF_INET, false);
+
+ return cur - buf;
+}
+
+TEST_F(tun_vnet_udptnl_proto, outer_proto_ok)
+{
+ uint8_t *pkt = self->pkt;
+ int len, ret;
+
+ len = build_udptnl_frame(pkt, false, ETH_P_IP, 2800);
+ ret = write(self->fd, pkt, len);
+ ASSERT_EQ(ret, len);
+
+ /* The outer IPv4 header can follow a VLAN tag. */
+ len = build_udptnl_frame(pkt, true, ETH_P_IP, 2800);
+ ret = write(self->fd, pkt, len);
+ ASSERT_EQ(ret, len);
+}
+
+TEST_F(tun_vnet_udptnl_proto, outer_proto_bad)
+{
+ uint8_t *pkt = self->pkt;
+ int len, ret;
+
+ /* Outer ethertype does not match the declared IPv4 tunnel. */
+ len = build_udptnl_frame(pkt, false, ETH_P_IPV6, 2800);
+ ret = write(self->fd, pkt, len);
+ ASSERT_EQ(ret, -1);
+ ASSERT_EQ(errno, EINVAL);
+
+ /* Outer header is not IP at all. */
+ len = build_udptnl_frame(pkt, false, ETH_P_NSH, 2800);
+ ret = write(self->fd, pkt, len);
+ ASSERT_EQ(ret, -1);
+ ASSERT_EQ(errno, EINVAL);
+
+ /* Same, behind a VLAN tag. */
+ len = build_udptnl_frame(pkt, true, ETH_P_NSH, 2800);
+ ret = write(self->fd, pkt, len);
+ ASSERT_EQ(ret, -1);
+ ASSERT_EQ(errno, EINVAL);
+}
+
+/* IFF_TUN device with a tun_pi header: skb->protocol comes from
+ * userspace, and there is no Ethernet header.
+ */
+FIXTURE(tun_vnet_udptnl_proto_l3)
+{
+ char ifname[IFNAMSIZ];
+ uint8_t *pkt;
+ int fd;
+};
+
+FIXTURE_SETUP(tun_vnet_udptnl_proto_l3)
+{
+ int flags = IFF_TUN | IFF_VNET_HDR;
+ int features = TUN_F_CSUM | TUN_F_UDP_TUNNEL_GSO |
+ TUN_F_UDP_TUNNEL_GSO_CSUM | TUN_F_USO4 | TUN_F_USO6;
+
+ self->fd = -1;
+ if (unshare(CLONE_NEWNET))
+ SKIP(return, "Cannot create network namespace: %s",
+ strerror(errno));
+
+ self->pkt = calloc(1, MAX_VNET_TUNNEL_PACKET_SZ);
+ ASSERT_NE(self->pkt, NULL);
+
+ memset(self->ifname, 0, sizeof(self->ifname));
+ self->fd = tun_open(self->ifname, flags, TUN_VNET_TNL_SIZE, features,
+ NULL);
+ ASSERT_GE(self->fd, 0);
+}
+
+FIXTURE_TEARDOWN(tun_vnet_udptnl_proto_l3)
+{
+ if (self->fd >= 0)
+ close(self->fd);
+ free(self->pkt);
+}
+
+#define IPV4_OPT_NOP 1
+#define IPV4_OPT_LEN 16
+
+/* Build a tun_pi header carrying @pi_proto, followed by a 4in4 Geneve
+ * GSO frame whose outer IP header claims version @ip_version.
+ *
+ * virtio_net_hdr_tnl_to_skb() wants room for an Ethernet header before
+ * the outer transport header, even without one: pad the outer IPv4
+ * header with NOP options.
+ */
+static int build_udptnl_l3_frame(uint8_t *buf, uint16_t pi_proto,
+ int ip_version, int payload_len)
+{
+ struct tun_pi *pi = (struct tun_pi *)buf;
+ uint8_t *vnet = buf + sizeof(*pi);
+ struct virtio_net_hdr_v1_hash_tunnel *vh = (void *)vnet;
+ int hlen = UDP_TUNNEL_GENEVE_4IN4_HDRLEN + IPV4_OPT_LEN;
+ uint8_t *cur, *outer_iph, *outer_udph;
+ struct iphdr *iph;
+ int pktlen;
+
+ /* The buffer is reused: do not leave bytes from a previous frame. */
+ memset(buf, 0, sizeof(*pi) + TUN_VNET_TNL_SIZE + hlen + payload_len);
+ pi->proto = htons(pi_proto);
+
+ cur = vnet + build_virtio_net_hdr_v1_hash_tunnel(vnet, false, hlen,
+ 1400, AF_INET,
+ AF_INET);
+ vh->outer_th_offset += IPV4_OPT_LEN;
+ vh->inner_nh_offset += IPV4_OPT_LEN;
+ vh->hash_hdr.hdr.csum_start += IPV4_OPT_LEN;
+
+ /* Build the frame without options first: build_udp_packet_csum()
+ * expects the IP addresses right before the UDP header.
+ */
+ outer_iph = cur;
+ pktlen = UDP_TUNNEL_GENEVE_4IN4_HDRLEN - sizeof(struct iphdr) +
+ payload_len;
+ cur += build_ipv4_header(cur, IPPROTO_UDP, pktlen,
+ ¶m_ipaddr4_outer_dst,
+ ¶m_ipaddr4_outer_src);
+
+ outer_udph = cur;
+ cur += build_udp_header(cur, UDP_SRC_PORT, VN_PORT,
+ pktlen - sizeof(struct udphdr));
+ cur += build_geneve_header(cur, VN_ID);
+ cur += build_eth(cur, ETH_P_IP, param_hwaddr_inner_dst,
+ param_hwaddr_inner_src);
+ cur += build_ipv4_header(cur, IPPROTO_UDP,
+ sizeof(struct udphdr) + payload_len,
+ ¶m_ipaddr4_inner_dst,
+ ¶m_ipaddr4_inner_src);
+ cur += build_udp_packet(cur, UDP_DST_PORT, UDP_SRC_PORT, payload_len,
+ AF_INET, false);
+ build_udp_packet_csum(outer_udph, AF_INET, false);
+
+ /* Then insert the options after the fixed outer IPv4 header. */
+ memmove(outer_udph + IPV4_OPT_LEN, outer_udph, cur - outer_udph);
+ memset(outer_udph, IPV4_OPT_NOP, IPV4_OPT_LEN);
+ cur += IPV4_OPT_LEN;
+
+ iph = (struct iphdr *)outer_iph;
+ iph->version = ip_version;
+ iph->ihl += IPV4_OPT_LEN / 4;
+ iph->tot_len = htons(ntohs(iph->tot_len) + IPV4_OPT_LEN);
+ iph->check = 0;
+ iph->check = build_ip_csum(outer_iph, iph->ihl << 2, 0);
+
+ return cur - buf;
+}
+
+TEST_F(tun_vnet_udptnl_proto_l3, outer_proto_ok)
+{
+ uint8_t *pkt = self->pkt;
+ int len, ret;
+
+ len = build_udptnl_l3_frame(pkt, ETH_P_IP, 4, 2800);
+ ret = write(self->fd, pkt, len);
+ ASSERT_EQ(ret, len);
+}
+
+TEST_F(tun_vnet_udptnl_proto_l3, outer_proto_bad)
+{
+ uint8_t *pkt = self->pkt;
+ int len, ret;
+
+ /* tun_pi protocol does not match the declared IPv4 tunnel. */
+ len = build_udptnl_l3_frame(pkt, ETH_P_IPV6, 4, 2800);
+ ret = write(self->fd, pkt, len);
+ ASSERT_EQ(ret, -1);
+ ASSERT_EQ(errno, EINVAL);
+
+ /* tun_pi claims IPv4, but the frame says otherwise. */
+ len = build_udptnl_l3_frame(pkt, ETH_P_IP, 6, 2800);
+ ret = write(self->fd, pkt, len);
+ ASSERT_EQ(ret, -1);
+ ASSERT_EQ(errno, EINVAL);
+}
+
FIXTURE(tun_vnet_udptnl)
{
char ifname[IFNAMSIZ];
--
2.53.0
next prev parent reply other threads:[~2026-11-05 11:57 UTC|newest]
Thread overview: 33+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-05 11:57 [PATCH net v3 0/8] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet
2026-11-05 11:57 ` [PATCH net v3 1/8] " Eric Dumazet
2026-11-06 8:16 ` sashiko-bot
2026-11-08 23:01 ` netdev-bot+sashiko
2026-11-05 11:57 ` [PATCH net v3 2/8] net: virtio: do not use dev->type to find the outer L3 header Eric Dumazet
2026-11-06 4:18 ` Willem de Bruijn
2026-11-06 8:48 ` Eric Dumazet
2026-11-06 8:16 ` sashiko-bot
2026-11-06 8:50 ` Eric Dumazet
2026-11-06 9:27 ` Michael S. Tsirkin
2026-11-06 9:55 ` Eric Dumazet
2026-11-08 23:01 ` netdev-bot+sashiko
2026-11-05 11:57 ` [PATCH net v3 3/8] tun: clear stale skb->protocol of NAPI frags skbs Eric Dumazet
2026-11-06 4:21 ` Willem de Bruijn
2026-11-06 8:49 ` Eric Dumazet
2026-11-06 8:16 ` sashiko-bot
2026-11-08 23:01 ` netdev-bot+sashiko
2026-11-05 11:57 ` [PATCH net v3 4/8] selftests: net: tuntap: fix UDP checksum length in build_udp_packet_csum() Eric Dumazet
2026-11-06 8:16 ` sashiko-bot
2026-11-05 11:57 ` Eric Dumazet [this message]
2026-11-06 8:16 ` [PATCH net v3 5/8] selftests: net: tun: test outer protocol of UDP tunnel GSO packets sashiko-bot
2026-11-05 11:57 ` [PATCH net v3 6/8] selftests: net: tun: test UDP tunnel GSO on retyped devices Eric Dumazet
2026-11-06 4:25 ` Willem de Bruijn
2026-11-06 8:16 ` sashiko-bot
2026-11-08 23:02 ` netdev-bot+sashiko
2026-11-05 11:57 ` [PATCH net v3 7/8] selftests: net: tun: test more outer header mismatches of UDP tunnel GSO Eric Dumazet
2026-11-06 8:16 ` sashiko-bot
2026-11-06 8:51 ` Eric Dumazet
2026-11-08 23:02 ` netdev-bot+sashiko
2026-11-05 11:57 ` [PATCH net v3 8/8] selftests: net: tuntap: write UDP tunnel offsets in little endian Eric Dumazet
2026-11-06 8:16 ` sashiko-bot
2026-11-06 8:52 ` Eric Dumazet
2026-11-08 23:02 ` netdev-bot+sashiko
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261105115715.1466924-6-edumazet@kernel.org \
--to=edumazet@kernel.org \
--cc=davem@davemloft.net \
--cc=eperezma@redhat.com \
--cc=hassansultan@google.com \
--cc=horms@kernel.org \
--cc=jasowangio@gmail.com \
--cc=kuba@kernel.org \
--cc=mst@redhat.com \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=virtualization@lists.linux.dev \
--cc=willemb@google.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.