xref: /linux/net/ipv6/ndisc_snoop.c (revision 91ec2035134982b98fab0609a9fd8480e8217dc1)
1 // SPDX-License-Identifier: GPL-2.0-only
2 
3 #include <linux/skbuff.h>
4 #include <net/addrconf.h>
5 #include <net/ip6_checksum.h>
6 #include <net/ipv6.h>
7 #include <net/ndisc.h>
8 
ndisc_check_ip6hdr(struct sk_buff * skb)9 static int ndisc_check_ip6hdr(struct sk_buff *skb)
10 {
11 	const struct ipv6hdr *ip6h;
12 	unsigned int offset, len;
13 
14 	offset = skb_network_offset(skb) + sizeof(*ip6h);
15 	if (!pskb_may_pull(skb, offset))
16 		return -EINVAL;
17 
18 	ip6h = ipv6_hdr(skb);
19 
20 	if (ip6h->version != 6)
21 		return -EINVAL;
22 
23 	if (ip6h->nexthdr != IPPROTO_ICMPV6)
24 		return -ENOMSG;
25 
26 	/* RFC 4861 7.1.1 / 7.1.2: must not have been forwarded by a router */
27 	if (ip6h->hop_limit != 255)
28 		return -EINVAL;
29 
30 	len = offset + ntohs(ip6h->payload_len);
31 	if (skb->len < len || len <= offset)
32 		return -EINVAL;
33 
34 	skb_set_transport_header(skb, offset);
35 
36 	return 0;
37 }
38 
ndisc_validate_checksum(struct sk_buff * skb)39 static __sum16 ndisc_validate_checksum(struct sk_buff *skb)
40 {
41 	return skb_checksum_validate(skb, IPPROTO_ICMPV6, ip6_compute_pseudo);
42 }
43 
ndisc_check_icmpv6(struct sk_buff * skb)44 static int ndisc_check_icmpv6(struct sk_buff *skb)
45 {
46 	unsigned int len = skb_transport_offset(skb) + sizeof(struct icmp6hdr);
47 	unsigned int transport_len = ipv6_transport_len(skb);
48 	struct sk_buff *skb_chk;
49 	struct icmp6hdr *hdr;
50 
51 	if (!pskb_may_pull(skb, len))
52 		return -EINVAL;
53 
54 	/* RFC 4861 7.1.1 / 7.1.2: the ICMPv6 checksum must be valid */
55 	skb_chk = skb_checksum_trimmed(skb, transport_len,
56 				       ndisc_validate_checksum);
57 	if (!skb_chk)
58 		return -EINVAL;
59 
60 	if (skb_chk != skb)
61 		kfree_skb(skb_chk);
62 
63 	/* RFC 4861 7.1.1 / 7.1.2: Code must be 0 */
64 	hdr = (struct icmp6hdr *)skb_transport_header(skb);
65 	if (hdr->icmp6_code != 0)
66 		return -EINVAL;
67 
68 	return 0;
69 }
70 
ndisc_check_options(struct sk_buff * skb,unsigned int opts_len,bool reject_slla)71 static int ndisc_check_options(struct sk_buff *skb, unsigned int opts_len,
72 			       bool reject_slla)
73 {
74 	unsigned int offset = skb_transport_offset(skb) + sizeof(struct nd_msg);
75 	struct nd_opt_hdr *opt, _opt;
76 
77 	while (opts_len > 0) {
78 		if (opts_len < sizeof(*opt))
79 			return -EINVAL;
80 
81 		opt = skb_header_pointer(skb, offset, sizeof(_opt), &_opt);
82 		if (!opt)
83 			return -EINVAL;
84 
85 		/* RFC 4861 7.1.1 / 7.1.2: all option lengths must be > 0 */
86 		if (!opt->nd_opt_len)
87 			return -EINVAL;
88 
89 		/* RFC 4861 7.1.1: DAD NS must not contain a source link-layer
90 		 * address option
91 		 */
92 		if (reject_slla && opt->nd_opt_type == ND_OPT_SOURCE_LL_ADDR)
93 			return -EINVAL;
94 
95 		if (opt->nd_opt_len * 8 > opts_len)
96 			return -EINVAL;
97 
98 		offset += opt->nd_opt_len * 8;
99 		opts_len -= opt->nd_opt_len * 8;
100 	}
101 
102 	return 0;
103 }
104 
ndisc_check_nd_msg(struct sk_buff * skb)105 static int ndisc_check_nd_msg(struct sk_buff *skb)
106 {
107 	unsigned int len = skb_transport_offset(skb) + sizeof(struct nd_msg);
108 	unsigned int transport_len = ipv6_transport_len(skb);
109 	bool reject_slla = false;
110 	const struct nd_msg *msg;
111 
112 	if (!pskb_may_pull(skb, len))
113 		return -EINVAL;
114 
115 	/* RFC 4861 7.1.1 / 7.1.2: ICMP length is at least sizeof(nd_msg) */
116 	if (transport_len < sizeof(struct nd_msg))
117 		return -EINVAL;
118 
119 	msg = (struct nd_msg *)skb_transport_header(skb);
120 
121 	/* RFC 4861 7.1.1 / 7.1.2: Target Address must not be a
122 	 * multicast address
123 	 */
124 	if (ipv6_addr_is_multicast(&msg->target))
125 		return -EINVAL;
126 
127 	switch (msg->icmph.icmp6_type) {
128 	case NDISC_NEIGHBOUR_SOLICITATION:
129 		if (ipv6_addr_any(&ipv6_hdr(skb)->saddr)) {
130 			/* RFC 4861 7.1.1: DAD NS destination must be a
131 			 * solicited-node multicast address
132 			 */
133 			if (!ipv6_addr_is_solict_mult(&ipv6_hdr(skb)->daddr))
134 				return -EINVAL;
135 			/* RFC 4861 7.1.1: DAD NS must not contain a source
136 			 * link-layer address option
137 			 */
138 			reject_slla = true;
139 		}
140 		break;
141 	case NDISC_NEIGHBOUR_ADVERTISEMENT:
142 		/* RFC 4861 7.1.2: Solicited flag must be 0 for
143 		 * multicast destinations
144 		 */
145 		if (ipv6_addr_is_multicast(&ipv6_hdr(skb)->daddr) &&
146 		    msg->icmph.icmp6_solicited)
147 			return -EINVAL;
148 		break;
149 	default:
150 		return -ENODATA;
151 	}
152 
153 	return ndisc_check_options(skb, transport_len - sizeof(struct nd_msg),
154 				   reject_slla);
155 }
156 
157 /**
158  * ndisc_check_ns_na - validate an NS/NA packet and set its transport header
159  * @skb: the skb to validate
160  *
161  * Validates an IPv6 packet for compliance with RFC 4861 sections 7.1.1
162  * (Neighbor Solicitation) and 7.1.2 (Neighbor Advertisement). If valid,
163  * sets the skb transport header.
164  *
165  * Caller needs to set the skb network header.
166  *
167  * Return:
168  * * 0        - valid NS/NA; the skb transport header has been set.
169  * * -EINVAL  - a broken packet was detected, i.e. it violates some
170  *              internet standard.
171  * * -ENOMSG  - IP header validation succeeded but it is not an ICMPv6
172  *              packet.
173  * * -ENODATA - IP+ICMPv6 header validation succeeded but it is not a
174  *              Neighbor Solicitation or Neighbor Advertisement.
175  */
ndisc_check_ns_na(struct sk_buff * skb)176 int ndisc_check_ns_na(struct sk_buff *skb)
177 {
178 	int ret;
179 
180 	ret = ndisc_check_ip6hdr(skb);
181 	if (ret < 0)
182 		return ret;
183 
184 	ret = ndisc_check_icmpv6(skb);
185 	if (ret < 0)
186 		return ret;
187 
188 	return ndisc_check_nd_msg(skb);
189 }
190 EXPORT_SYMBOL_GPL(ndisc_check_ns_na);
191