Carry IPv4 routes over IPv6 sessions with IPv6 next hops (RFC 8950)
For a link that has only IPv6 addresses. - The Extended Next Hop capability is offered on every session over IPv6 whose families include IPv4 unicast, and used if the peer offers it too. Nothing to configure. - On such a session our IPv4 routes go out in MP_REACH_NLRI with the session's IPv6 address as next hop, and are withdrawn in MP_UNREACH_NLRI. Without the agreement they are not sent, as before. - IPv4 routes are read with an IPv6 next hop, global or global with link-local. - The kernel installer resolves an IPv4 route's IPv6 next hop like any other and installs it: by way of an IPv6 nexthop object, or with RTA_VIA where routes carry their own gateway. Tested against a real kernel in both modes, with a link-local next hop too, and in the lab against FRR and BIRD: the new `v6only` phase takes the IPv4 addresses off the exchange LAN, runs one IPv6 session with each router for both families, and pings IPv4 across. Sessions between link-local addresses are not supported yet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
321af51e4b
commit
92e013ca01
14 files changed
+511
-23
No files matched your search
+141
-3
@@ -29,7 +29,7 @@ cd "$(dirname "$0")/../.."
|
||||
bgp="${CARGO_TARGET_DIR:-target}/release/bgp"
|
||||
work=/tmp/ferrix-lab
|
||||
pathspace=ferrixlab
|
||||
all_phases="basics roles rpki refresh flaps restart scale limits"
|
||||
all_phases="basics roles rpki refresh flaps restart scale limits v6only"
|
||||
phases="${PHASES:-$all_phases}"
|
||||
scale_routes="${SCALE_ROUTES:-200000}"
|
||||
|
||||
@@ -170,6 +170,29 @@ http_endpoint { address: "127.0.0.1" port: 8080 }
|
||||
END
|
||||
}
|
||||
|
||||
dut_config_v6only() {
|
||||
cat > "$work/dut.textpb" <<END
|
||||
router_id: "10.9.0.1"
|
||||
local_asn: 64601
|
||||
listen_endpoints { address: "2001:db8:9::1" port: 179 }
|
||||
connect_retry_seconds: 1
|
||||
peers {
|
||||
name: "frr" endpoint { address: "2001:db8:9::2" } remote_asn: 64602
|
||||
role: PEER
|
||||
import { accept_bogons: true max_prefixes: 1000 }
|
||||
}
|
||||
peers {
|
||||
name: "bird" endpoint { address: "2001:db8:9::3" } remote_asn: 4200000003
|
||||
role: PROVIDER
|
||||
import { accept_bogons: true max_prefixes: 1000 }
|
||||
}
|
||||
originate { prefix: "10.1.0.0/24" }
|
||||
originate { prefix: "2001:db8:1::/48" }
|
||||
kernel { stale_seconds: 3 }
|
||||
http_endpoint { address: "127.0.0.1" port: 8080 }
|
||||
END
|
||||
}
|
||||
|
||||
dut_up() {
|
||||
sudo ip netns exec lab-dut \
|
||||
setpriv --reuid "$(id -u)" --regid "$(id -g)" --init-groups \
|
||||
@@ -197,7 +220,7 @@ dut_down() {
|
||||
|
||||
# --- FRR ------------------------------------------------------------------------
|
||||
|
||||
frr_up() {
|
||||
frr_config() {
|
||||
sudo mkdir -p "/etc/frr/$pathspace" "/var/run/frr/$pathspace"
|
||||
sudo tee "/etc/frr/$pathspace/frr.conf" > /dev/null <<END
|
||||
frr defaults traditional
|
||||
@@ -229,6 +252,41 @@ router bgp 64602
|
||||
exit-address-family
|
||||
!
|
||||
END
|
||||
}
|
||||
|
||||
# One session, over IPv6, for both families: IPv4 routes go with IPv6 next
|
||||
# hops (RFC 8950).
|
||||
frr_config_v6only() {
|
||||
sudo tee "/etc/frr/$pathspace/frr.conf" > /dev/null <<END
|
||||
frr defaults traditional
|
||||
hostname frr
|
||||
log file $work/frr.log
|
||||
!
|
||||
router bgp 64602
|
||||
bgp router-id 10.9.0.2
|
||||
no bgp ebgp-requires-policy
|
||||
no bgp network import-check
|
||||
timers bgp 3 9
|
||||
neighbor 2001:db8:9::1 remote-as 64601
|
||||
neighbor 2001:db8:9::1 local-role peer
|
||||
neighbor 2001:db8:9::1 timers connect 1
|
||||
neighbor 2001:db8:9::1 capability extended-nexthop
|
||||
!
|
||||
address-family ipv4 unicast
|
||||
network 10.2.0.0/24
|
||||
network 10.2.5.0/24
|
||||
neighbor 2001:db8:9::1 activate
|
||||
exit-address-family
|
||||
!
|
||||
address-family ipv6 unicast
|
||||
network 2001:db8:2::/48
|
||||
neighbor 2001:db8:9::1 activate
|
||||
exit-address-family
|
||||
!
|
||||
END
|
||||
}
|
||||
|
||||
frr_up() {
|
||||
sudo touch "/etc/frr/$pathspace/vtysh.conf" "$work/frr.log"
|
||||
sudo chown -R frr:frr "/etc/frr/$pathspace" "/var/run/frr/$pathspace" "$work/frr.log"
|
||||
for daemon in zebra bgpd; do
|
||||
@@ -294,6 +352,29 @@ protocol bgp dut6 from dut {
|
||||
END
|
||||
}
|
||||
|
||||
bird_config_v6only() {
|
||||
cat > "$work/bird.conf" <<END
|
||||
log "$work/bird.log" all;
|
||||
router id 10.9.0.3;
|
||||
protocol device {}
|
||||
protocol kernel kernel4 { ipv4 { export where source = RTS_BGP; }; }
|
||||
protocol kernel kernel6 { ipv6 { export where source = RTS_BGP; }; }
|
||||
protocol static static4 { ipv4; route 10.3.0.0/24 blackhole; }
|
||||
protocol static static6 { ipv6; route 2001:db8:3::/48 blackhole; }
|
||||
protocol bgp dut {
|
||||
local as 4200000003;
|
||||
neighbor 2001:db8:9::1 as 64601;
|
||||
source address 2001:db8:9::3;
|
||||
local role provider;
|
||||
connect retry time 1;
|
||||
hold time 9;
|
||||
graceful restart off;
|
||||
ipv4 { import all; export where source = RTS_STATIC; extended next hop on; };
|
||||
ipv6 { import all; export where source = RTS_STATIC; };
|
||||
}
|
||||
END
|
||||
}
|
||||
|
||||
bird_up() {
|
||||
sudo ip netns exec lab-bird bird -c "$work/bird.conf" -s "$work/bird.ctl" -P "$work/bird.pid"
|
||||
}
|
||||
@@ -325,7 +406,7 @@ lab_up() {
|
||||
[ -x "$bgp" ] || fail "$bgp not found: cargo build --release -p bgp"
|
||||
network_up
|
||||
routinator_up
|
||||
frr_up
|
||||
frr_config; frr_up
|
||||
bird_config; bird_up
|
||||
dut_config; dut_up
|
||||
}
|
||||
@@ -644,6 +725,63 @@ phase_limits() {
|
||||
ok "back to normal"
|
||||
}
|
||||
|
||||
# The exchange LAN loses its IPv4 addresses. One session over IPv6 with
|
||||
# each router carries both families, and IPv4 routes have IPv6 next hops
|
||||
# (RFC 8950), in BGP and in the kernel. Run last: the lab stays that way.
|
||||
phase_v6only() {
|
||||
dut_down TERM
|
||||
frr_down TERM
|
||||
sudo kill "$(sudo cat "$work/bird.pid")"; sleep 0.5; sudo rm -f "$work/bird.ctl"
|
||||
local n=0 name
|
||||
for name in dut frr bird; do
|
||||
n=$((n + 1))
|
||||
in_ns "$name" ip addr del "10.9.0.$n/24" dev ix0
|
||||
done
|
||||
frr_config_v6only; frr_up
|
||||
bird_config_v6only; bird_up
|
||||
dut_config_v6only; dut_up
|
||||
|
||||
wait_for "two sessions over IPv6" established 2
|
||||
api /api/peers | jq -e 'all(.[]; .extended_next_hop and (.address_families | length == 2))' >/dev/null \
|
||||
|| fail "the sessions did not agree on IPv4 with IPv6 next hops: $(api /api/peers | jq -c '[.[] | {name, address_families, extended_next_hop}]')"
|
||||
ok "both sessions carry IPv4 and IPv6, with IPv6 next hops for IPv4 (RFC 8950)"
|
||||
|
||||
# What they send us: IPv4 routes whose next hop is an IPv6 address,
|
||||
# and the same in the kernel.
|
||||
wait_for "FRR's IPv4 route" dut_has 10.2.0.0/24 frr
|
||||
wait_for "BIRD's IPv4 route" dut_has 10.3.0.0/24 bird
|
||||
api "/api/routes/received?prefix=10.2.0.0/24" | jq -e '.routes[0].summary.next_hop | startswith("2001:db8:9::2")' >/dev/null \
|
||||
|| fail "FRR's IPv4 route has the next hop $(api "/api/routes/received?prefix=10.2.0.0/24" | jq '.routes[0].summary.next_hop')"
|
||||
wait_for "FRR's IPv4 route in the kernel" dut_kernel -4 "10.2.0.0/24 via inet6 2001:db8:9::2 dev ix0 metric 20"
|
||||
wait_for "BIRD's IPv4 route in the kernel" dut_kernel -4 "10.3.0.0/24 via inet6 2001:db8:9::3 dev ix0 metric 20"
|
||||
wait_for "FRR's IPv6 route in the kernel" dut_kernel -6 "2001:db8:2::/48 via 2001:db8:9::2 dev ix0 metric 20"
|
||||
ok "their IPv4 routes arrive with IPv6 next hops and are in the kernel that way"
|
||||
|
||||
# What we send them.
|
||||
wait_for "our IPv4 route at FRR" frr_has ipv4 10.1.0.0/24
|
||||
vty -c "show bgp ipv4 unicast 10.1.0.0/24 json" | jq -e '.paths[0].nexthops | any(.ip == "2001:db8:9::1" and .afi == "ipv6")' >/dev/null \
|
||||
|| fail "FRR's copy of our IPv4 route has the next hops $(vty -c 'show bgp ipv4 unicast 10.1.0.0/24 json' | jq -c '.paths[0].nexthops')"
|
||||
wait_for "our IPv4 route at BIRD" bird_has 10.1.0.0/24
|
||||
bird_ctl show route for 10.1.0.0/24 all | grep -q "BGP.next_hop: 2001:db8:9::1" \
|
||||
|| fail "BIRD's copy of our IPv4 route: $(bird_ctl show route for 10.1.0.0/24 all)"
|
||||
wait_for "our IPv6 route at FRR" frr_has ipv6 2001:db8:1::/48
|
||||
ok "FRR and BIRD have our IPv4 route with our IPv6 address as next hop"
|
||||
|
||||
# IPv4 packets across a link that has no IPv4 addresses.
|
||||
in_ns dut ping -q -c 2 -W 2 -I 10.1.0.1 10.2.0.1 >/dev/null || fail "no IPv4 ping to FRR's lan"
|
||||
in_ns dut ping -q -c 2 -W 2 -I 10.1.0.1 10.3.0.1 >/dev/null || fail "no IPv4 ping to BIRD's lan"
|
||||
in_ns frr ping -q -c 2 -W 2 -I 10.2.0.1 10.1.0.1 >/dev/null || fail "no IPv4 ping from FRR's lan"
|
||||
in_ns dut ping -q -c 2 -W 2 -I 2001:db8:1::1 2001:db8:2::1 >/dev/null || fail "no IPv6 ping to FRR's lan"
|
||||
ok "IPv4 packets cross the IPv6-only link, both ways"
|
||||
|
||||
# A withdrawal by FRR (in MP_UNREACH_NLRI) takes the route out.
|
||||
frr_configure "router bgp 64602" "address-family ipv4 unicast" "no network 10.2.5.0/24"
|
||||
wait_for "FRR's withdrawal" dut_lacks 10.2.5.0/24
|
||||
wait_for "it out of the kernel" dut_kernel_lacks -4 10.2.5.0/24
|
||||
dut_has 10.2.0.0/24 frr >/dev/null || fail "the withdrawal took the other route along"
|
||||
ok "an IPv4 route withdrawn over the IPv6 session goes"
|
||||
}
|
||||
|
||||
# --- main -----------------------------------------------------------------------
|
||||
|
||||
case "${1:-test}" in
|
||||
|
||||
Reference in new issue
Block a user