ipv6: ndisc: Add ndisc_check_ns_na() validation helper

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>
Link: https://patch.msgid.link/20260803112505.613873-3-danieller@nvidia.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
Danielle Ratson
2026-08-03 14:25:02 +03:00
committed by Jakub Kicinski
parent 2cad8e3d9d
commit 9dfa6cca89
3 changed files with 193 additions and 1 deletions

View File

@@ -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
*/

View File

@@ -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

190
net/ipv6/ndisc_snoop.c Normal file
View File

@@ -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);