xref: /linux/tools/testing/selftests/net/exception_cache.sh (revision c923c14942b164cfc2c1efa4e6324214f2fc248a)
1#!/bin/bash
2# SPDX-License-Identifier: GPL-2.0
3#
4# Test that the state of the route exception cache after an ICMP error is
5# processed does not depend on whether the quoted packet was matched to a
6# socket. Otherwise, an off-path attacker can probe the cache to discover the
7# ephemeral port used by a connected UDP socket.
8#
9# When the quoted packet is not matched to a socket, the same exception is
10# created as when it is matched, so that neither its presence nor its contents
11# reveal the result of socket matching.
12#
13#                              +----+
14#                    +---------| r1 |
15#                    |         +----+
16#  +----+   +--------+            | .1
17#  | h1 |---| bridge |            |    198.51.100.0/30
18#  +----+   +--------+            |    2001:db8:2::/64
19#      .1            |            | .2
20#                    |         +----+           +----+
21#                    +---------| r2 |-----------| h2 |
22#                    .2   .3   +----+ .1     .2 +----+
23#                                   203.0.113.0/24
24#                                   2001:db8:3::/64
25#         192.0.2.0/24
26#         2001:db8:1::/64
27#
28# Traffic from h1 to h2 is routed via r1, which reaches h2's network via r2
29# over the point-to-point link. The MTU of the r2 - h2 link is lowered so that
30# r2 emits ICMP errors towards h1.
31#
32# For the redirect tests r1's route to h2's network is replaced with one via r2
33# on the shared segment, so that r1 forwards the packet back to the segment it
34# arrived from and emits a redirect towards h1.
35#
36# The packets that provoke the ICMP errors are injected with a packet socket so
37# that no socket is ever associated with them. A socket is created separately,
38# with socat, when a test needs the ICMP error to be matched.
39
40# shellcheck disable=SC1091,SC2034,SC2154,SC2329
41source lib.sh
42
43require_command jq
44require_command mausezahn
45require_command nstat
46require_command socat
47
48ALL_TESTS="
49	pmtu_no_socket_ipv4
50	pmtu_no_socket_ipv6
51	pmtu_socket_ipv4
52	pmtu_socket_ipv6
53	pmtu_omit_ipv4
54	pmtu_omit_ipv6
55	redirect_no_socket_ipv4
56	redirect_no_socket_ipv6
57	redirect_socket_ipv4
58	redirect_socket_ipv6
59"
60
61# Shared segment.
62H1_ADDR4=192.0.2.1
63R1_ADDR4=192.0.2.2
64R2_ADDR4=192.0.2.3
65H1_ADDR6=2001:db8:1::1
66R1_ADDR6=2001:db8:1::2
67R2_ADDR6=2001:db8:1::3
68
69# r1 - r2 link.
70R2_R1_ADDR4=198.51.100.2
71R2_R1_ADDR6=2001:db8:2::2
72
73# r2 - h2 link.
74H2_ADDR4=203.0.113.2
75H2_NET4=203.0.113.0/24
76H2_ADDR6=2001:db8:3::2
77H2_NET6=2001:db8:3::/64
78
79SPORT=12345
80DPORT=54321
81
82# The MTU of the shared segment and of the r1 - r2 link. Large enough for the
83# injected packets to reach r2 intact.
84SEGMENT_MTU=2000
85# Size of the injected packets. The PMTU tests need a size that exceeds every
86# MTU used for the r2 - h2 link, so that r2 responds with an ICMP error. The
87# redirect tests need a size that does not, otherwise r2 would respond with an
88# ICMP error in addition to the redirect emitted by r1.
89PMTU_PACKET_SIZE=1800
90REDIRECT_PACKET_SIZE=100
91
92# The MTUs used for the r2 - h2 link. All of them must be at least
93# IPV6_MIN_MTU, otherwise IPv6 silently ignores the error instead of creating
94# an exception.
95MTU_MID=1400
96MTU_LOW=1300
97
98# Values for the IP{,V6}_MTU_DISCOVER socket option.
99PMTUDISC_DONT=0
100PMTUDISC_OMIT=5
101
102SOCAT_PID=
103
104linklocal_get()
105{
106	local ns=$1; shift
107	local dev=$1; shift
108
109	ip -n "$ns" -j -6 addr show dev "$dev" | \
110		jq -r '.[]["addr_info"][] | select(.scope == "link") | .local'
111}
112
113linklocal_exists()
114{
115	local ns=$1; shift
116	local dev=$1; shift
117
118	[ -n "$(linklocal_get "$ns" "$dev")" ]
119}
120
121family_vars_set()
122{
123	local family=$1; shift
124
125	FAMILY=$family
126
127	if [ "$family" -eq 4 ]; then
128		H1_ADDR=$H1_ADDR4
129		H2_ADDR=$H2_ADDR4
130		MZ_FAMILY_OPT=()
131		# Without the Don't Fragment bit set r2 fragments the packet
132		# instead of reporting the MTU of the next hop.
133		MZ_IP_OPTS="df,"
134		SOCAT_DST="UDP4-CONNECT:$H2_ADDR4:$DPORT"
135		SOCAT_BIND="bind=$H1_ADDR4:$SPORT"
136		SOCAT_PMTUDISC="ip-mtu-discover"
137	else
138		H1_ADDR=$H1_ADDR6
139		H2_ADDR=$H2_ADDR6
140		MZ_FAMILY_OPT=(-6)
141		MZ_IP_OPTS=
142		SOCAT_DST="UDP6-CONNECT:[$H2_ADDR6]:$DPORT"
143		SOCAT_BIND="bind=[$H1_ADDR6]:$SPORT"
144		SOCAT_PMTUDISC="ipv6-mtu-discover"
145	fi
146}
147
148topology_setup()
149{
150	local ns
151
152	setup_ns h1 r1 r2 h2 sw
153	defer cleanup_all_ns
154
155	# Link-local addresses are generated from the MAC address and read
156	# back during setup, so request that generation mode explicitly and
157	# make the addresses available as soon as the devices are brought up.
158	for ns in "$h1" "$r1" "$r2" "$h2" "$sw"; do
159		ip netns exec "$ns" sysctl -qw \
160			net.ipv6.conf.default.addr_gen_mode=0 \
161			net.ipv6.conf.default.accept_dad=0 \
162			net.ipv6.conf.all.accept_dad=0
163	done
164
165	ip -n "$sw" link add name br0 type bridge
166	ip -n "$sw" link set dev br0 mtu "$SEGMENT_MTU" up
167
168	ip -n "$h1" link add name eth0 mtu "$SEGMENT_MTU" type veth \
169		peer name swp1 mtu "$SEGMENT_MTU" netns "$sw"
170	ip -n "$r1" link add name eth0 mtu "$SEGMENT_MTU" type veth \
171		peer name swp2 mtu "$SEGMENT_MTU" netns "$sw"
172	ip -n "$r2" link add name eth0 mtu "$SEGMENT_MTU" type veth \
173		peer name swp3 mtu "$SEGMENT_MTU" netns "$sw"
174	ip -n "$r1" link add name eth1 mtu "$SEGMENT_MTU" type veth \
175		peer name eth1 mtu "$SEGMENT_MTU" netns "$r2"
176	ip -n "$r2" link add name eth2 type veth peer name eth0 netns "$h2"
177
178	ip -n "$sw" link set dev swp1 master br0 up
179	ip -n "$sw" link set dev swp2 master br0 up
180	ip -n "$sw" link set dev swp3 master br0 up
181
182	ip -n "$h1" link set dev eth0 up
183	ip -n "$r1" link set dev eth0 up
184	ip -n "$r1" link set dev eth1 up
185	ip -n "$r2" link set dev eth0 up
186	ip -n "$r2" link set dev eth1 up
187	ip -n "$r2" link set dev eth2 up
188	ip -n "$h2" link set dev eth0 up
189
190	ip -n "$h1" address add "$H1_ADDR4/24" dev eth0
191	ip -n "$r1" address add "$R1_ADDR4/24" dev eth0
192	ip -n "$r2" address add "$R2_ADDR4/24" dev eth0
193	ip -n "$r1" address add 198.51.100.1/30 dev eth1
194	ip -n "$r2" address add "$R2_R1_ADDR4/30" dev eth1
195	ip -n "$r2" address add 203.0.113.1/24 dev eth2
196	ip -n "$h2" address add "$H2_ADDR4/24" dev eth0
197
198	ip -n "$h1" -6 address add "$H1_ADDR6/64" dev eth0 nodad
199	ip -n "$r1" -6 address add "$R1_ADDR6/64" dev eth0 nodad
200	ip -n "$r2" -6 address add "$R2_ADDR6/64" dev eth0 nodad
201	ip -n "$r1" -6 address add 2001:db8:2::1/64 dev eth1 nodad
202	ip -n "$r2" -6 address add "$R2_R1_ADDR6/64" dev eth1 nodad
203	ip -n "$r2" -6 address add 2001:db8:3::1/64 dev eth2 nodad
204	ip -n "$h2" -6 address add "$H2_ADDR6/64" dev eth0 nodad
205
206	ip netns exec "$r1" sysctl -qw net.ipv4.ip_forward=1
207	ip netns exec "$r1" sysctl -qw net.ipv4.conf.all.send_redirects=1
208	ip netns exec "$r1" sysctl -qw net.ipv6.conf.all.forwarding=1
209	ip netns exec "$r2" sysctl -qw net.ipv4.ip_forward=1
210	ip netns exec "$r2" sysctl -qw net.ipv6.conf.all.forwarding=1
211
212	ip netns exec "$h1" sysctl -qw net.ipv4.conf.all.accept_redirects=1
213	ip netns exec "$h1" sysctl -qw net.ipv4.conf.eth0.accept_redirects=1
214	ip netns exec "$h1" sysctl -qw net.ipv6.conf.all.accept_redirects=1
215	ip netns exec "$h1" sysctl -qw net.ipv6.conf.eth0.accept_redirects=1
216
217	slowwait 5 linklocal_exists "$r1" eth0
218	check_err $? "r1: link-local address was not generated"
219	slowwait 5 linklocal_exists "$r2" eth0
220	check_err $? "r2: link-local address was not generated"
221
222	R1_LLADDR=$(linklocal_get "$r1" eth0)
223	R2_LLADDR=$(linklocal_get "$r2" eth0)
224	R1_MAC=$(ip -n "$r1" -j link show dev eth0 | jq -r '.[]["address"]')
225	R2_MAC=$(ip -n "$r2" -j link show dev eth0 | jq -r '.[]["address"]')
226
227	ip -n "$h1" route add "$H2_NET4" via "$R1_ADDR4" dev eth0
228	ip -n "$h1" -6 route add "$H2_NET6" via "$R1_LLADDR" dev eth0
229	ip -n "$r1" route add "$H2_NET4" via "$R2_R1_ADDR4" dev eth1
230	ip -n "$r1" -6 route add "$H2_NET6" via "$R2_R1_ADDR6" dev eth1
231	ip -n "$h2" route add default via 203.0.113.1 dev eth0
232	ip -n "$h2" -6 route add default via 2001:db8:3::1 dev eth0
233
234	far_mtu_set "$MTU_MID"
235}
236
237# Make r1 forward towards h2's network over the segment it receives the packet
238# from, so that it emits a redirect towards h1.
239redirect_route_set()
240{
241	ip -n "$r1" route replace "$H2_NET4" via "$R2_ADDR4" dev eth0
242	ip -n "$r1" -6 route replace "$H2_NET6" via "$R2_LLADDR" dev eth0
243
244	# __ip_do_redirect() only creates an exception if the new gateway is
245	# already a valid neighbour. Otherwise it merely triggers address
246	# resolution. IPv6 resolves the target itself, in rt6_do_redirect().
247	ip -n "$h1" neigh replace "$R2_ADDR4" lladdr "$R2_MAC" dev eth0 \
248		nud permanent
249}
250
251far_mtu_set()
252{
253	local mtu=$1; shift
254
255	ip -n "$r2" link set dev eth2 mtu "$mtu"
256	ip -n "$h2" link set dev eth0 mtu "$mtu"
257}
258
259socket_is_open()
260{
261	ip netns exec "$h1" ss -uHn "sport = :$SPORT" | grep -q .
262}
263
264socket_start()
265{
266	# Disable PMTU discovery by default so that ICMP errors are not
267	# reported to the socket. Otherwise socat would exit when the first one
268	# arrives and later packets in the same test would not be matched to a
269	# socket. The exception is still created, as ip{,6}_sk_accept_pmtu()
270	# only rejects IP{,V6}_PMTUDISC_{INTERFACE,OMIT}.
271	local pmtudisc=${1:-$PMTUDISC_DONT}
272
273	# Send socat's diagnostics to /dev/null. It reports the ICMP errors
274	# that reach the socket, which is exactly what the tests provoke.
275	ip netns exec "$h1" socat -u -lf/dev/null \
276		"$SOCAT_DST,$SOCAT_BIND,$SOCAT_PMTUDISC=$pmtudisc" \
277		OPEN:/dev/null,wronly=1 &
278	SOCAT_PID=$!
279	defer socket_stop
280
281	slowwait 5 socket_is_open
282	check_err $? "socket did not open"
283}
284
285socket_stop()
286{
287	[ -z "$SOCAT_PID" ] && return 0
288
289	kill "$SOCAT_PID" &> /dev/null
290	wait "$SOCAT_PID" 2> /dev/null
291	SOCAT_PID=
292}
293
294# Inject a packet towards h2 with a packet socket. No socket is associated with
295# it, so an ICMP error quoting it is matched to a socket only if one was
296# created separately with the same source port.
297packet_send()
298{
299	local size=$1; shift
300
301	ip netns exec "$h1" mausezahn "${MZ_FAMILY_OPT[@]}" eth0 \
302		-a own -b "$R1_MAC" -A "$H1_ADDR" -B "$H2_ADDR" \
303		-t udp "${MZ_IP_OPTS}sp=$SPORT,dp=$DPORT" \
304		-p "$size" -c 1 -q
305}
306
307exception_show()
308{
309	if [ "$FAMILY" -eq 4 ]; then
310		# IPv4 exceptions without a bound route are not dumped, but
311		# "route get" reports the exception and binds a route to it.
312		ip -n "$h1" route get "$H2_ADDR"
313	else
314		# IPv6 does not report a cache indication in "route get"
315		# output, so dump the exceptions instead.
316		ip -n "$h1" -6 route show cache | grep -F "$H2_ADDR" || true
317	fi
318}
319
320exception_mtu_get()
321{
322	exception_show | grep -o "mtu [0-9]*" | cut -d ' ' -f 2
323}
324
325exception_gw_get()
326{
327	exception_show | grep -o "via [0-9a-f.:]*" | cut -d ' ' -f 2
328}
329
330exception_mtu_check()
331{
332	local expected=$1; shift
333
334	[ "$(exception_mtu_get)" = "$expected" ]
335}
336
337icmp_errors_get()
338{
339	local ctr=IcmpInDestUnreachs
340
341	[ "$FAMILY" -eq 6 ] && ctr=Icmp6InPktTooBigs
342
343	ip netns exec "$h1" nstat -asz "$ctr" | \
344		awk -v ctr="$ctr" '$1 == ctr { print $2 }'
345}
346
347exception_pmtu_check()
348{
349	local mtu=$1; shift
350	local desc=$1; shift
351
352	busywait "$BUSYWAIT_TIMEOUT" exception_mtu_check "$mtu"
353	check_err $? "$desc: exception does not carry an MTU of $mtu"
354}
355
356pmtu_no_socket()
357{
358	local family=$1; shift
359
360	RET=0
361	family_vars_set "$family"
362	topology_setup
363
364	packet_send "$PMTU_PACKET_SIZE"
365	exception_pmtu_check "$MTU_MID" "No socket"
366
367	log_test "IPv$family: PMTU: exception without a matching socket"
368}
369
370pmtu_no_socket_ipv4()
371{
372	pmtu_no_socket 4
373}
374
375pmtu_no_socket_ipv6()
376{
377	pmtu_no_socket 6
378}
379
380pmtu_socket()
381{
382	local family=$1; shift
383	local t0
384
385	RET=0
386	family_vars_set "$family"
387	topology_setup
388	socket_start
389
390	packet_send "$PMTU_PACKET_SIZE"
391	exception_pmtu_check "$MTU_MID" "Matching socket"
392
393	# A lower PMTU replaces the one currently stored in the exception.
394	far_mtu_set "$MTU_LOW"
395	packet_send "$PMTU_PACKET_SIZE"
396	exception_pmtu_check "$MTU_LOW" "Lower PMTU"
397
398	# A higher PMTU is ignored, so the exception is left as it is. Wait
399	# for the error to be received, as otherwise the check below would
400	# pass even if it never was.
401	far_mtu_set "$MTU_MID"
402	t0=$(icmp_errors_get)
403	packet_send "$PMTU_PACKET_SIZE"
404	busywait "$BUSYWAIT_TIMEOUT" until_counter_is ">= $((t0 + 1))" \
405		icmp_errors_get > /dev/null
406	check_err $? "Higher PMTU: ICMP error was not received"
407
408	exception_mtu_check "$MTU_LOW"
409	check_err $? "Higher PMTU: exception does not carry an MTU of $MTU_LOW"
410
411	log_test "IPv$family: PMTU: exception with a matching socket"
412}
413
414pmtu_socket_ipv4()
415{
416	pmtu_socket 4
417}
418
419pmtu_socket_ipv6()
420{
421	pmtu_socket 6
422}
423
424pmtu_omit()
425{
426	local family=$1; shift
427
428	RET=0
429	family_vars_set "$family"
430	topology_setup
431	socket_start "$PMTUDISC_OMIT"
432
433	packet_send "$PMTU_PACKET_SIZE"
434	exception_pmtu_check "$MTU_MID" "PMTU discovery disabled"
435
436	log_test "IPv$family: PMTU: exception with a socket ignoring it"
437}
438
439pmtu_omit_ipv4()
440{
441	pmtu_omit 4
442}
443
444pmtu_omit_ipv6()
445{
446	pmtu_omit 6
447}
448
449exception_gw_check()
450{
451	local expected=$1; shift
452
453	[ -n "$expected" ] && [ "$(exception_gw_get)" = "$expected" ]
454}
455
456redirect_gw_new()
457{
458	if [ "$FAMILY" -eq 4 ]; then
459		echo "$R2_ADDR4"
460	else
461		echo "$R2_LLADDR"
462	fi
463}
464
465redirect_no_socket()
466{
467	local family=$1; shift
468
469	RET=0
470	family_vars_set "$family"
471	topology_setup
472	redirect_route_set
473
474	packet_send "$REDIRECT_PACKET_SIZE"
475	busywait "$BUSYWAIT_TIMEOUT" exception_gw_check "$(redirect_gw_new)"
476	check_err $? "No socket: exception does not carry the new gateway"
477
478	log_test "IPv$family: Redirect: exception without a matching socket"
479}
480
481redirect_no_socket_ipv4()
482{
483	redirect_no_socket 4
484}
485
486redirect_no_socket_ipv6()
487{
488	redirect_no_socket 6
489}
490
491redirect_socket()
492{
493	local family=$1; shift
494
495	RET=0
496	family_vars_set "$family"
497	topology_setup
498	redirect_route_set
499	socket_start
500
501	packet_send "$REDIRECT_PACKET_SIZE"
502	busywait "$BUSYWAIT_TIMEOUT" exception_gw_check "$(redirect_gw_new)"
503	check_err $? "Matching socket: exception does not carry the new gateway"
504
505	log_test "IPv$family: Redirect: exception with a matching socket"
506}
507
508redirect_socket_ipv4()
509{
510	redirect_socket 4
511}
512
513redirect_socket_ipv6()
514{
515	redirect_socket 6
516}
517
518trap defer_scopes_cleanup EXIT
519tests_run
520
521exit "$EXIT_STATUS"
522