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