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