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/bridge/br_arp_nd_proxy.c b/net/bridge/br_arp_nd_proxy.c index 23eb6931a2b4..b6e5a86b6a92 100644 --- a/net/bridge/br_arp_nd_proxy.c +++ b/net/bridge/br_arp_nd_proxy.c @@ -19,6 +19,7 @@ #include #if IS_ENABLED(CONFIG_IPV6) #include +#include #endif #include "br_private.h" @@ -234,21 +235,18 @@ void br_do_proxy_suppress_arp(struct sk_buff *skb, struct net_bridge *br, #endif #if IS_ENABLED(CONFIG_IPV6) -struct nd_msg *br_is_nd_neigh_msg(const struct sk_buff *skb, struct nd_msg *msg) +/* Validate skb as an NS/NA and linearize it for br_nd_send()'s ND + * option parsing; returns the nd_msg, or NULL on failure. + */ +struct nd_msg *br_is_nd_neigh_msg(struct sk_buff *skb) { - struct nd_msg *m; - - m = skb_header_pointer(skb, skb_network_offset(skb) + - sizeof(struct ipv6hdr), sizeof(*msg), msg); - if (!m) + if (ndisc_check_ns_na(skb)) return NULL; - if (m->icmph.icmp6_code != 0 || - (m->icmph.icmp6_type != NDISC_NEIGHBOUR_SOLICITATION && - m->icmph.icmp6_type != NDISC_NEIGHBOUR_ADVERTISEMENT)) + if (skb_linearize(skb)) return NULL; - return m; + return (struct nd_msg *)skb_transport_header(skb); } static void br_nd_send(struct net_bridge *br, struct net_bridge_port *p, @@ -257,17 +255,18 @@ static void br_nd_send(struct net_bridge *br, struct net_bridge_port *p, { struct net_device *dev = request->dev; struct net_bridge_vlan_group *vg; + struct ndisc_options ndopts; struct nd_msg *na, *ns; struct sk_buff *reply; struct ipv6hdr *pip6; int na_olen = 8; /* opt hdr + ETH_ALEN for target */ int ns_olen; - int i, len; u8 *daddr; bool dad; u16 pvid; + int len; - if (!dev || skb_linearize(request)) + if (!dev) return; len = LL_RESERVED_SPACE(dev) + sizeof(struct ipv6hdr) + @@ -284,23 +283,23 @@ static void br_nd_send(struct net_bridge *br, struct net_bridge_port *p, skb_set_mac_header(reply, 0); daddr = eth_hdr(request)->h_source; - ns = (struct nd_msg *)(skb_network_header(request) + - sizeof(struct ipv6hdr)); + ns = (struct nd_msg *)skb_transport_header(request); - /* Do we need option processing ? */ - ns_olen = request->len - (skb_network_offset(request) + - sizeof(struct ipv6hdr)) - sizeof(*ns); - for (i = 0; i < ns_olen - 1; i += (ns->opt[i + 1] << 3)) { - if (!ns->opt[i + 1] || i + (ns->opt[i + 1] << 3) > ns_olen) { - kfree_skb(reply); - return; - } - if (ns->opt[i] == ND_OPT_SOURCE_LL_ADDR) { - if ((ns->opt[i + 1] << 3) >= - sizeof(struct nd_opt_hdr) + ETH_ALEN) - daddr = ns->opt + i + sizeof(struct nd_opt_hdr); - break; - } + /* Derive the option length from the IPv6 payload length so that any + * trailing L2 padding in the skb is not parsed as ND options. + */ + ns_olen = ntohs(ipv6_hdr(request)->payload_len) - sizeof(*ns); + if (!ndisc_parse_options(dev, ns->opt, ns_olen, &ndopts)) { + kfree_skb(reply); + return; + } + + if (ndopts.nd_opts_src_lladdr) { + u8 *lladdr; + + lladdr = ndisc_opt_addr_data(ndopts.nd_opts_src_lladdr, dev); + if (lladdr) + daddr = lladdr; } dad = ipv6_addr_any(&ipv6_hdr(request)->saddr); diff --git a/net/bridge/br_device.c b/net/bridge/br_device.c index e7f343ab22d3..ff55dab73632 100644 --- a/net/bridge/br_device.c +++ b/net/bridge/br_device.c @@ -80,9 +80,9 @@ netdev_tx_t br_dev_xmit(struct sk_buff *skb, struct net_device *dev) pskb_may_pull(skb, sizeof(struct ipv6hdr) + sizeof(struct nd_msg)) && ipv6_hdr(skb)->nexthdr == IPPROTO_ICMPV6) { - struct nd_msg *msg, _msg; + struct nd_msg *msg; - msg = br_is_nd_neigh_msg(skb, &_msg); + msg = br_is_nd_neigh_msg(skb); if (msg) br_do_suppress_nd(skb, br, vid, NULL, msg); } diff --git a/net/bridge/br_input.c b/net/bridge/br_input.c index ddb8f002a40e..d87a5f9fa92b 100644 --- a/net/bridge/br_input.c +++ b/net/bridge/br_input.c @@ -176,9 +176,9 @@ int br_handle_frame_finish(struct net *net, struct sock *sk, struct sk_buff *skb pskb_may_pull(skb, sizeof(struct ipv6hdr) + sizeof(struct nd_msg)) && ipv6_hdr(skb)->nexthdr == IPPROTO_ICMPV6) { - struct nd_msg *msg, _msg; + struct nd_msg *msg; - msg = br_is_nd_neigh_msg(skb, &_msg); + msg = br_is_nd_neigh_msg(skb); if (msg) br_do_suppress_nd(skb, br, vid, p, msg); } diff --git a/net/bridge/br_private.h b/net/bridge/br_private.h index 4cf8b1ab9047..d337b1cfb980 100644 --- a/net/bridge/br_private.h +++ b/net/bridge/br_private.h @@ -2367,7 +2367,7 @@ void br_do_proxy_suppress_arp(struct sk_buff *skb, struct net_bridge *br, u16 vid, struct net_bridge_port *p); void br_do_suppress_nd(struct sk_buff *skb, struct net_bridge *br, u16 vid, struct net_bridge_port *p, struct nd_msg *msg); -struct nd_msg *br_is_nd_neigh_msg(const struct sk_buff *skb, struct nd_msg *m); +struct nd_msg *br_is_nd_neigh_msg(struct sk_buff *skb); bool br_is_neigh_suppress_enabled(const struct net_bridge_port *p, u16 vid); bool br_is_neigh_forward_grat_enabled(const struct net_bridge_port *p, u16 vid); #endif 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.c b/net/ipv6/ndisc.c index fe36b3f51285..2ceb655c4229 100644 --- a/net/ipv6/ndisc.c +++ b/net/ipv6/ndisc.c @@ -283,6 +283,7 @@ struct ndisc_options *ndisc_parse_options(const struct net_device *dev, } return ndopts; } +EXPORT_SYMBOL_GPL(ndisc_parse_options); int ndisc_mc_map(const struct in6_addr *addr, char *buf, struct net_device *dev, int dir) { 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 +#include +#include +#include +#include + +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);