projects/linux·blob master · refs

521 lines · 13.5 KiB

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"