mirror of
https://github.com/torvalds/linux.git
synced 2026-09-22 04:34:03 +02:00
Merge branch 'bridge-validate-and-clean-up-ipv6-neighbour-suppression'
Danielle Ratson says: ==================== bridge: Validate and clean up IPv6 neighbour suppression The bridge implements IPv6 neighbour suppression by snooping Neighbour Solicitation and Neighbour Advertisement messages, but it previously only checked the ICMPv6 type and code before acting on them. This leaves it open to acting on malformed or spoofed packets that any RFC 4861 compliant node should reject, and the option parsing in br_nd_send() open-codes a loop that has historically been a source of bugs. This series hardens and cleans up that path: Add ndisc_check_ns_na(), a standalone NS/NA validator modeled after ipv6_mc_check_mld(), implementing the RFC 4861 section 7.1.1 / 7.1.2 mandatory receive checks (hop limit, checksum, code, length, target and option validation). Wire the bridge into it so NS/NA messages are validated to the same standard MLD already enjoys. Replace the manual ND option parsing loop in br_nd_send() with ndisc_parse_options() and ndisc_opt_addr_data(), and linearize the skb once it has been validated as an NS/NA message so that this and any future ND message handling operate on a linear buffer. The first patch is a small preparatory cleanup that drops the now-unnecessary skb_header_pointer() fallback from br_is_nd_neigh_msg(). No functional change is intended for well-formed packets. Patchset overview: Patch #1: drop the skb_header_pointer() fallback. Patches #2-#3: add ndisc_check_ns_na() and validate NS/NA with it. Patch #4: linearize once the ND message type is validated. Patch #5: parse options via ndisc_parse_options(). ==================== Link: https://patch.msgid.link/20260803112505.613873-1-danieller@nvidia.com Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
commit
f59ee23b98
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#include <net/addrconf.h>
|
||||
#if IS_ENABLED(CONFIG_IPV6)
|
||||
#include <net/ip6_checksum.h>
|
||||
#include <net/ndisc.h>
|
||||
#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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
190
net/ipv6/ndisc_snoop.c
Normal file
190
net/ipv6/ndisc_snoop.c
Normal 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);
|
||||
Loading…
Reference in New Issue
Block a user