Add ndisc_check_ns_na(), a standalone NS/NA packet validator modeled
after ipv6_mc_check_mld(). It performs the RFC 4861 section 7.1.1
(Neighbor Solicitation) and 7.1.2 (Neighbor Advertisement) mandatory
checks that are relevant for software operating at the bridge level,
where packets bypass the normal IPv6 stack path:

 - Hop Limit must be 255 (packet was not forwarded by a router)
 - ICMPv6 checksum is valid
 - ICMP Code is 0
 - ICMP length is at least 24 octets (sizeof(struct nd_msg))
 - Target Address must not be a multicast address
 - All included options have a length that is greater than zero
 - NS/DAD: destination must be a solicited-node multicast address
 - NS/DAD: no Source Link-Layer Address option when source is unspecified
 - NA: Solicited flag must be 0 when IP Destination is multicast

On success the function sets the skb transport header and returns 0,
matching the convention of ipv6_mc_check_mld().

Reviewed-by: Petr Machata <[email protected]>
Signed-off-by: Danielle Ratson <[email protected]>
---
 include/net/ndisc.h    |   2 +
 net/ipv6/Makefile      |   2 +-
 net/ipv6/ndisc_snoop.c | 190 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 193 insertions(+), 1 deletion(-)
 create mode 100644 net/ipv6/ndisc_snoop.c

diff --git a/include/net/ndisc.h b/include/net/ndisc.h
index 3da1a6f8d3f9..9e5379ad2d8e 100644
--- a/include/net/ndisc.h
+++ b/include/net/ndisc.h
@@ -430,6 +430,8 @@ void ndisc_update(const struct net_device *dev, struct 
neighbour *neigh,
                  const u8 *lladdr, u8 new, u32 flags, u8 icmp6_type,
                  struct ndisc_options *ndopts);
 
+int ndisc_check_ns_na(struct sk_buff *skb);
+
 /*
  *     IGMP
  */
diff --git a/net/ipv6/Makefile b/net/ipv6/Makefile
index 5b0cd6488021..cf5e01f83ce3 100644
--- a/net/ipv6/Makefile
+++ b/net/ipv6/Makefile
@@ -51,7 +51,7 @@ obj-$(subst m,y,$(CONFIG_IPV6)) += inet6_hashtables.o
 
 ifneq ($(CONFIG_IPV6),)
 obj-$(CONFIG_NET_UDP_TUNNEL) += ip6_udp_tunnel.o
-obj-y += mcast_snoop.o
+obj-y += mcast_snoop.o ndisc_snoop.o
 obj-$(CONFIG_TCP_AO) += tcp_ao.o
 endif
 
diff --git a/net/ipv6/ndisc_snoop.c b/net/ipv6/ndisc_snoop.c
new file mode 100644
index 000000000000..a93eed27a1f6
--- /dev/null
+++ b/net/ipv6/ndisc_snoop.c
@@ -0,0 +1,190 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <linux/skbuff.h>
+#include <net/addrconf.h>
+#include <net/ip6_checksum.h>
+#include <net/ipv6.h>
+#include <net/ndisc.h>
+
+static int ndisc_check_ip6hdr(struct sk_buff *skb)
+{
+       const struct ipv6hdr *ip6h;
+       unsigned int offset, len;
+
+       offset = skb_network_offset(skb) + sizeof(*ip6h);
+       if (!pskb_may_pull(skb, offset))
+               return -EINVAL;
+
+       ip6h = ipv6_hdr(skb);
+
+       if (ip6h->version != 6)
+               return -EINVAL;
+
+       if (ip6h->nexthdr != IPPROTO_ICMPV6)
+               return -ENOMSG;
+
+       /* RFC 4861 7.1.1 / 7.1.2: must not have been forwarded by a router */
+       if (ip6h->hop_limit != 255)
+               return -EINVAL;
+
+       len = offset + ntohs(ip6h->payload_len);
+       if (skb->len < len || len <= offset)
+               return -EINVAL;
+
+       skb_set_transport_header(skb, offset);
+
+       return 0;
+}
+
+static __sum16 ndisc_validate_checksum(struct sk_buff *skb)
+{
+       return skb_checksum_validate(skb, IPPROTO_ICMPV6, ip6_compute_pseudo);
+}
+
+static int ndisc_check_icmpv6(struct sk_buff *skb)
+{
+       unsigned int len = skb_transport_offset(skb) + sizeof(struct icmp6hdr);
+       unsigned int transport_len = ipv6_transport_len(skb);
+       struct sk_buff *skb_chk;
+       struct icmp6hdr *hdr;
+
+       if (!pskb_may_pull(skb, len))
+               return -EINVAL;
+
+       /* RFC 4861 7.1.1 / 7.1.2: the ICMPv6 checksum must be valid */
+       skb_chk = skb_checksum_trimmed(skb, transport_len,
+                                      ndisc_validate_checksum);
+       if (!skb_chk)
+               return -EINVAL;
+
+       if (skb_chk != skb)
+               kfree_skb(skb_chk);
+
+       /* RFC 4861 7.1.1 / 7.1.2: Code must be 0 */
+       hdr = (struct icmp6hdr *)skb_transport_header(skb);
+       if (hdr->icmp6_code != 0)
+               return -EINVAL;
+
+       return 0;
+}
+
+static int ndisc_check_options(struct sk_buff *skb, unsigned int opts_len,
+                              bool reject_slla)
+{
+       unsigned int offset = skb_transport_offset(skb) + sizeof(struct nd_msg);
+       struct nd_opt_hdr *opt, _opt;
+
+       while (opts_len > 0) {
+               if (opts_len < sizeof(*opt))
+                       return -EINVAL;
+
+               opt = skb_header_pointer(skb, offset, sizeof(_opt), &_opt);
+               if (!opt)
+                       return -EINVAL;
+
+               /* RFC 4861 7.1.1 / 7.1.2: all option lengths must be > 0 */
+               if (!opt->nd_opt_len)
+                       return -EINVAL;
+
+               /* RFC 4861 7.1.1: DAD NS must not contain a source link-layer
+                * address option
+                */
+               if (reject_slla && opt->nd_opt_type == ND_OPT_SOURCE_LL_ADDR)
+                       return -EINVAL;
+
+               if (opt->nd_opt_len * 8 > opts_len)
+                       return -EINVAL;
+
+               offset += opt->nd_opt_len * 8;
+               opts_len -= opt->nd_opt_len * 8;
+       }
+
+       return 0;
+}
+
+static int ndisc_check_nd_msg(struct sk_buff *skb)
+{
+       unsigned int len = skb_transport_offset(skb) + sizeof(struct nd_msg);
+       unsigned int transport_len = ipv6_transport_len(skb);
+       bool reject_slla = false;
+       const struct nd_msg *msg;
+
+       if (!pskb_may_pull(skb, len))
+               return -EINVAL;
+
+       /* RFC 4861 7.1.1 / 7.1.2: ICMP length is at least sizeof(nd_msg) */
+       if (transport_len < sizeof(struct nd_msg))
+               return -EINVAL;
+
+       msg = (struct nd_msg *)skb_transport_header(skb);
+
+       /* RFC 4861 7.1.1 / 7.1.2: Target Address must not be a
+        * multicast address
+        */
+       if (ipv6_addr_is_multicast(&msg->target))
+               return -EINVAL;
+
+       switch (msg->icmph.icmp6_type) {
+       case NDISC_NEIGHBOUR_SOLICITATION:
+               if (ipv6_addr_any(&ipv6_hdr(skb)->saddr)) {
+                       /* RFC 4861 7.1.1: DAD NS destination must be a
+                        * solicited-node multicast address
+                        */
+                       if (!ipv6_addr_is_solict_mult(&ipv6_hdr(skb)->daddr))
+                               return -EINVAL;
+                       /* RFC 4861 7.1.1: DAD NS must not contain a source
+                        * link-layer address option
+                        */
+                       reject_slla = true;
+               }
+               break;
+       case NDISC_NEIGHBOUR_ADVERTISEMENT:
+               /* RFC 4861 7.1.2: Solicited flag must be 0 for
+                * multicast destinations
+                */
+               if (ipv6_addr_is_multicast(&ipv6_hdr(skb)->daddr) &&
+                   msg->icmph.icmp6_solicited)
+                       return -EINVAL;
+               break;
+       default:
+               return -ENODATA;
+       }
+
+       return ndisc_check_options(skb, transport_len - sizeof(struct nd_msg),
+                                  reject_slla);
+}
+
+/**
+ * ndisc_check_ns_na - validate an NS/NA packet and set its transport header
+ * @skb: the skb to validate
+ *
+ * Validates an IPv6 packet for compliance with RFC 4861 sections 7.1.1
+ * (Neighbor Solicitation) and 7.1.2 (Neighbor Advertisement). If valid,
+ * sets the skb transport header.
+ *
+ * Caller needs to set the skb network header.
+ *
+ * Return:
+ * * 0        - valid NS/NA; the skb transport header has been set.
+ * * -EINVAL  - a broken packet was detected, i.e. it violates some
+ *              internet standard.
+ * * -ENOMSG  - IP header validation succeeded but it is not an ICMPv6
+ *              packet.
+ * * -ENODATA - IP+ICMPv6 header validation succeeded but it is not a
+ *              Neighbor Solicitation or Neighbor Advertisement.
+ */
+int ndisc_check_ns_na(struct sk_buff *skb)
+{
+       int ret;
+
+       ret = ndisc_check_ip6hdr(skb);
+       if (ret < 0)
+               return ret;
+
+       ret = ndisc_check_icmpv6(skb);
+       if (ret < 0)
+               return ret;
+
+       return ndisc_check_nd_msg(skb);
+}
+EXPORT_SYMBOL(ndisc_check_ns_na);
-- 
2.54.0


Reply via email to