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>,
"Eugenio Pérez" <eperezma@redhat.com>,
"Hassan Sultan" <hassansultan@google.com>,
virtualization@lists.linux.dev
Subject: [PATCH net v2 3/3] selftests: net: tun: test outer protocol of UDP tunnel GSO packets
Date: Wed, 4 Nov 2026 06:01:31 +0100 [thread overview]
Message-ID: <20261104050131.629967-4-edumazet@kernel.org> (raw)
In-Reply-To: <20261104050131.629967-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.
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-04 5:01 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-04 5:01 [PATCH net v2 0/3] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet
2026-11-04 5:01 ` [PATCH net v2 1/3] " Eric Dumazet
2026-11-04 15:32 ` Willem de Bruijn
2026-11-04 15:36 ` Michael S. Tsirkin
2026-11-04 5:01 ` [PATCH net v2 2/3] selftests: net: tuntap: fix UDP checksum length in build_udp_packet_csum() Eric Dumazet
2026-11-04 5:01 ` Eric Dumazet [this message]
2026-11-04 15:36 ` [PATCH net v2 3/3] selftests: net: tun: test outer protocol of UDP tunnel GSO packets Willem de Bruijn
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=20261104050131.629967-4-edumazet@kernel.org \
--to=edumazet@kernel.org \
--cc=davem@davemloft.net \
--cc=eperezma@redhat.com \
--cc=hassansultan@google.com \
--cc=horms@kernel.org \
--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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox