diff --git a/include/linux/virtio_net.h b/include/linux/virtio_net.h
index 013c6c7e2dea..54048759cc6e 100644
--- a/include/linux/virtio_net.h
+++ b/include/linux/virtio_net.h
@@ -5,7 +5,11 @@
#include <linux/if_vlan.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
+#include <linux/netdevice.h>
#include <linux/udp.h>
+#include <net/gso.h>
+#include <net/ip.h>
+#include <net/ipv6.h>
#include <net/tcp.h>
#include <uapi/linux/tcp.h>
#include <uapi/linux/virtio_net.h>
@@ -48,6 +52,216 @@ static inline int virtio_net_hdr_set_proto(struct sk_buff *skb,
return 0;
}
+/* TODO: nothing virtio specific here, move to ip.h */
+
+/*
+ * Pull and minimally validate the IPv4 header at @nhoff.
+ * Returns the header and stores the transport header offset in
+ * @thoff, or NULL if truncated or malformed. The whole header,
+ * options included, is guaranteed linear on return.
+ */
+static inline struct iphdr *virtio_net_ip4_parse(struct sk_buff *skb,
+ unsigned int nhoff,
+ unsigned int *thoff)
+{
+ unsigned int ihl;
+ struct iphdr *iph;
+
+ if (!pskb_may_pull(skb, nhoff + sizeof(struct iphdr)))
+ return NULL;
+
+ iph = (struct iphdr *)(skb->data + nhoff);
+ if (iph->version != 4 ||
+ iph->ihl < sizeof(struct iphdr) / sizeof(u32))
+ return NULL;
+
+ ihl = iph->ihl * sizeof(u32);
+ *thoff = nhoff + ihl;
+ /* Pull the options as well: the transport header offset
+ * returned above must be within the linear data. iph goes
+ * stale across the pull.
+ */
+ if (!pskb_may_pull(skb, nhoff + ihl))
+ return NULL;
+
+ return (struct iphdr *)(skb->data + nhoff);
+}
+
+/* Where does the L3 header start, and what protocol is it?
+ * Handles Ethernet and VLAN headers; other devices use the
+ * network header offset the caller set up.
+ */
+static inline bool
+virtio_net_l3_start(struct sk_buff *skb, int *nhoff, __be16 *proto)
+{
+ /* skb->dev should always be set here already,
+ * but let's not crash when it is not.
+ */
+ if (WARN_ON_ONCE(!skb->dev))
+ return false;
+
+ if (skb->dev->type == ARPHRD_ETHER ||
+ skb->dev->type == ARPHRD_LOOPBACK) {
+ __be16 h_proto;
+
+ if (!pskb_may_pull(skb, ETH_HLEN))
+ return false;
+ h_proto = eth_hdr(skb)->h_proto;
+ /* skb->protocol is a sender claim, and parts of the
+ * stack parse the packet per it: when it disagrees with
+ * the frame we do not know which view the packet will
+ * be parsed as, so do not validate.
+ */
+ if (skb->protocol && skb->protocol != h_proto)
+ return false;
+ *nhoff = ETH_HLEN;
+ *proto = h_proto;
+ if (eth_type_vlan(*proto)) {
+ *proto = __vlan_get_protocol(skb, *proto, nhoff);
+ if (!*proto)
+ return false;
+ }
+ } else {
+ const struct iphdr *iph;
+
+ *nhoff = skb_network_offset(skb);
+ /* An unset network header shows up as an offset
+ * outside of the packet.
+ */
+ if (*nhoff < 0 || (unsigned int)*nhoff >= skb->len)
+ return false;
+ /* No L2 header to cross-check with: take the protocol
+ * from the IP version at the network header.
+ */
+ if (!pskb_may_pull(skb, *nhoff + 1))
+ return false;
+ iph = (const struct iphdr *)(skb->data + *nhoff);
+ if (iph->version == 4)
+ *proto = htons(ETH_P_IP);
+ else if (iph->version == 6)
+ *proto = htons(ETH_P_IPV6);
+ else
+ return false;
+ if (skb->protocol && skb->protocol != *proto)
+ return false;
+ }
+ return true;
+}
+
+/* Walk the L3 header at @nhoff: where does its transport header
+ * start, and which protocol is it? Returns false if the header
+ * is truncated, malformed or fragmented.
+ */
+static inline bool
+virtio_net_l4_start(struct sk_buff *skb, unsigned int nhoff, __be16 proto,
+ unsigned int *thoff, int *ip_proto)
+{
+ if (proto == htons(ETH_P_IP)) {
+ const struct iphdr *iph = virtio_net_ip4_parse(skb, nhoff, thoff);
+
+ if (!iph || ip_is_fragment(iph))
+ return false;
+ *ip_proto = iph->protocol;
+ return true;
+ }
+
+ if (proto == htons(ETH_P_IPV6)) {
+ const struct ipv6hdr *iph;
+ int flags = 0;
+
+ if (!pskb_may_pull(skb, nhoff + sizeof(*iph)))
+ return false;
+ iph = (const void *)(skb->data + nhoff);
+ if (iph->version != 6)
+ return false;
+ *thoff = nhoff + sizeof(*iph);
+ *ip_proto = iph->nexthdr;
+ if (*ip_proto != IPPROTO_TCP && *ip_proto != IPPROTO_UDP) {
+ *thoff = nhoff;
+ /* Find the transport header behind any
+ * extension headers, AH included.
+ */
+ *ip_proto = ipv6_find_hdr(skb, thoff, -1, NULL,
+ &flags);
+ if (*ip_proto < 0 || flags & IP6_FH_F_FRAG)
+ return false;
+ }
+ return true;
+ }
+
+ return false;
+}
+
+/* Does checksum metadata match a known TCP or UDP header? */
+static inline bool
+virtio_net_csum_partial_ok_at_offset(struct sk_buff *skb, u8 hdr_gso_type,
+ unsigned int nhoff, __be16 proto)
+{
+ unsigned int thoff;
+ int ip_proto;
+
+ if (!virtio_net_l4_start(skb, nhoff, proto, &thoff, &ip_proto))
+ return false;
+
+ if (skb_checksum_start_offset(skb) != thoff)
+ return false;
+
+ switch (ip_proto) {
+ case IPPROTO_TCP:
+ if (skb->csum_offset != offsetof(struct tcphdr, check) ||
+ !pskb_may_pull(skb, thoff + sizeof(struct tcphdr)))
+ return false;
+ break;
+ case IPPROTO_UDP:
+ if (skb->csum_offset != offsetof(struct udphdr, check) ||
+ !pskb_may_pull(skb, thoff + sizeof(struct udphdr)))
+ return false;
+ break;
+ default:
+ return false;
+ }
+
+ switch (hdr_gso_type & ~VIRTIO_NET_HDR_GSO_ECN) {
+ case VIRTIO_NET_HDR_GSO_NONE:
+ return true;
+ case VIRTIO_NET_HDR_GSO_TCPV4:
+ return proto == htons(ETH_P_IP) && ip_proto == IPPROTO_TCP;
+ case VIRTIO_NET_HDR_GSO_TCPV6:
+ return proto == htons(ETH_P_IPV6) && ip_proto == IPPROTO_TCP;
+ case VIRTIO_NET_HDR_GSO_UDP:
+ case VIRTIO_NET_HDR_GSO_UDP_L4:
+ return ip_proto == IPPROTO_UDP;
+ default:
+ return false;
+ }
+}
+
+static inline bool
+virtio_net_csum_partial_ok(struct sk_buff *skb, u8 hdr_gso_type)
+{
+ int nhoff;
+ __be16 proto;
+
+ if (!virtio_net_l3_start(skb, &nhoff, &proto))
+ return false;
+
+ return virtio_net_csum_partial_ok_at_offset(skb, hdr_gso_type, nhoff,
+ proto);
+}
+
+/*
+ * Handle a packet whose checksum metadata does not match the packet:
+ * GSO packets are dropped, as segmentation needs one checksum per
+ * segment; others are completed in software.
+ */
+static inline int __virtio_net_resolve_csum_partial_mismatch(struct sk_buff *skb)
+{
+ if (skb_is_gso(skb))
+ return -EINVAL;
+
+ return skb_checksum_help(skb) ?: 1;
+}
+
static inline int __virtio_net_hdr_to_skb(struct sk_buff *skb,
const struct virtio_net_hdr *hdr,
bool little_endian, u8 hdr_gso_type)@@ -194,11 +408,35 @@ static inline int __virtio_net_hdr_to_skb(struct sk_buff *skb,
return 0;
}
+/*
+ * Return value:
+ * 0 - success
+ * >0 - checksum was completed in software (slow path)
+ * <0 - failure
+ */
static inline int virtio_net_hdr_to_skb(struct sk_buff *skb,
const struct virtio_net_hdr *hdr,
bool little_endian)
{
- return __virtio_net_hdr_to_skb(skb, hdr, little_endian, hdr->gso_type);
+ typeof(skb->transport_header) old_transport = skb->transport_header;
+ int ret = __virtio_net_hdr_to_skb(skb, hdr, little_endian,
+ hdr->gso_type);
+
+ if (ret < 0)
+ return ret;
+
+ if (likely(skb->ip_summed != CHECKSUM_PARTIAL ||
+ virtio_net_csum_partial_ok(skb, hdr->gso_type)))
+ return 0;
+
+ /*
+ * Here we do not trust the checksum metadata.
+ * __virtio_net_hdr_to_skb set the transport header to csum_start
+ * from virtio hdr, restore it to a known good value.
+ */
+ skb->transport_header = old_transport;
+
+ return __virtio_net_resolve_csum_partial_mismatch(skb);
}
/* This function must be called after virtio_net_hdr_from_skb(). */@@ -282,6 +520,35 @@ static inline unsigned int virtio_l3min(bool is_ipv6)
return is_ipv6 ? sizeof(struct ipv6hdr) : sizeof(struct iphdr);
}
+/* Do the tunnel offsets match the outer headers in the frame?
+ * The L3 header the tunnel claims must be the one the frame
+ * carries, with UDP following it at @outer_th.
+ */
+static inline bool
+virtio_net_tnl_offsets_ok(struct sk_buff *skb, bool outer_isv6,
+ unsigned int outer_th)
+{
+ int nhoff;
+ __be16 proto;
+ unsigned int thoff;
+ int ip_proto;
+
+ if (!virtio_net_l3_start(skb, &nhoff, &proto))
+ return false;
+ if (outer_isv6 != (proto == htons(ETH_P_IPV6)))
+ return false;
+ if (!virtio_net_l4_start(skb, nhoff, proto, &thoff, &ip_proto))
+ return false;
+
+ return ip_proto == IPPROTO_UDP && outer_th == thoff;
+}
+
+/*
+ * Return value:
+ * 0 - success
+ * >0 - checksum was completed in software (slow path)
+ * <0 - failure
+ */
static inline int
virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
const struct virtio_net_hdr_v1_hash_tunnel *vhdr,@@ -290,9 +557,11 @@ virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
bool little_endian)
{
const struct virtio_net_hdr *hdr = (const struct virtio_net_hdr *)vhdr;
+ typeof(skb->transport_header) old_transport = skb->transport_header;
unsigned int inner_nh, outer_th, inner_th;
unsigned int inner_l3min, outer_l3min;
u8 gso_inner_type, gso_tunnel_type;
+ __be16 inner_proto;
bool outer_isv6, inner_isv6;
int ret;
@@ -335,9 +604,12 @@ virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
/* Let the basic parsing deal with plain GSO features. */
ret = __virtio_net_hdr_to_skb(skb, hdr, true,
hdr->gso_type & ~gso_tunnel_type);
- if (ret)
+ if (ret < 0)
return ret;
+ if (!virtio_net_tnl_offsets_ok(skb, outer_isv6, outer_th))
+ return -EINVAL;
+
/* In case of USO, the inner protocol is still unknown and
* `inner_isv6` is just a guess, additional parsing is needed.
* The previous validation ensures that accessing an ipv4 inner
@@ -352,9 +624,29 @@ virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
return -EINVAL;
}
- skb_set_inner_protocol(skb, inner_isv6 ? htons(ETH_P_IPV6) :
- htons(ETH_P_IP));
+ inner_proto = inner_isv6 ? htons(ETH_P_IPV6) : htons(ETH_P_IP);
+
+ /*
+ * We know VIRTIO_NET_HDR_F_NEEDS_CSUM was set so __virtio_net_hdr_to_skb
+ * set CHECKSUM_PARTIAL.
+ */
+ if (unlikely(!virtio_net_csum_partial_ok_at_offset(skb, gso_inner_type,
+ inner_nh,
+ inner_proto))) {
+ /* __virtio_net_hdr_to_skb set the transport header to
+ * csum_start, restore it to a known good value.
+ */
+ skb->transport_header = old_transport;
+
+ return __virtio_net_resolve_csum_partial_mismatch(skb);
+ }
+
+ skb_set_inner_protocol(skb, inner_proto);
if (hdr->flags & VIRTIO_NET_HDR_F_UDP_TUNNEL_CSUM) {
+ /* Checked here, after the validation: a packet completed
+ * in software never uses the feature, so an unnegotiated
+ * request only matters for metadata good enough to act on.
+ */
if (!tnl_csum_negotiated)
return -EINVAL;
@@ -368,6 +660,21 @@ virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
skb->inner_mac_header = inner_nh + skb_headroom(skb);
skb->transport_header = outer_th + skb_headroom(skb);
skb->encapsulation = 1;
+
+ if (!skb_is_gso(skb)) {
+ /* Too small to segment: complete the checksum in place
+ * instead of dropping - senders do tag small packets
+ * with GSO headers, e.g. packetdrill sets gso_type and
+ * gso_size with --mss, whatever the packet size.
+ * The outer UDP checksum is not calculated, but a zero
+ * one is fine: UDP over IPv4 allows a disabled
+ * checksum, and so do tunnels over IPv6 where the
+ * receiver opts in (RFC 6935, RFC 6936).
+ */
+ skb_shinfo(skb)->gso_type &= ~(SKB_GSO_UDP_TUNNEL_CSUM |
+ SKB_GSO_UDP_TUNNEL);
+ }
+
return 0;
}
--
MST