From: Danielle Ratson <danieller@nvidia.com>
To: <netdev@vger.kernel.org>
Cc: <dsahern@kernel.org>, <idosch@nvidia.com>, <davem@davemloft.net>,
<edumazet@google.com>, <kuba@kernel.org>, <pabeni@redhat.com>,
<horms@kernel.org>, <razor@blackwall.org>, <ja@ssi.bg>,
<petrm@nvidia.com>, <fw@strlen.de>, <kuniyu@google.com>,
<bridge@lists.linux.dev>, <linux-kernel@vger.kernel.org>,
Danielle Ratson <danieller@nvidia.com>
Subject: [PATCH net-next v2 2/5] ipv6: ndisc: Add ndisc_check_ns_na() validation helper
Date: Mon, 3 Aug 2026 14:25:02 +0300 [thread overview]
Message-ID: <20260803112505.613873-3-danieller@nvidia.com> (raw)
In-Reply-To: <20260803112505.613873-1-danieller@nvidia.com>
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 <petrm@nvidia.com>
Acked-by: Nikolay Aleksandrov <razor@blackwall.org>
Signed-off-by: Danielle Ratson <danieller@nvidia.com>
---
Notes:
v2:
* Use EXPORT_SYMBOL_GPL() instead of EXPORT_SYMBOL() to export
ndisc_check_ns_na().
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..fa86528d5cfe
--- /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_GPL(ndisc_check_ns_na);
--
2.54.0
next prev parent reply other threads:[~2026-08-03 11:25 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-03 11:25 [PATCH net-next v2 0/5] bridge: Validate and clean up IPv6 neighbour suppression Danielle Ratson
2026-08-03 11:25 ` [PATCH net-next v2 1/5] bridge: Use direct pointer in br_is_nd_neigh_msg() Danielle Ratson
2026-08-05 8:06 ` Danielle Ratson
2026-08-03 11:25 ` Danielle Ratson [this message]
2026-08-05 8:06 ` [PATCH net-next v2 2/5] ipv6: ndisc: Add ndisc_check_ns_na() validation helper Danielle Ratson
2026-08-03 11:25 ` [PATCH net-next v2 3/5] bridge: Validate NS/NA messages using ndisc_check_ns_na() Danielle Ratson
2026-08-03 11:25 ` [PATCH net-next v2 4/5] bridge: Linearize skb once the ND message type is validated Danielle Ratson
2026-08-03 15:25 ` Nikolay Aleksandrov
2026-08-03 11:25 ` [PATCH net-next v2 5/5] bridge: Use ndisc_parse_options() to parse ND options in br_nd_send() Danielle Ratson
2026-08-05 8:07 ` Danielle Ratson
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=20260803112505.613873-3-danieller@nvidia.com \
--to=danieller@nvidia.com \
--cc=bridge@lists.linux.dev \
--cc=davem@davemloft.net \
--cc=dsahern@kernel.org \
--cc=edumazet@google.com \
--cc=fw@strlen.de \
--cc=horms@kernel.org \
--cc=idosch@nvidia.com \
--cc=ja@ssi.bg \
--cc=kuba@kernel.org \
--cc=kuniyu@google.com \
--cc=linux-kernel@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=petrm@nvidia.com \
--cc=razor@blackwall.org \
/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