From c729275961bb7f87ded010b3303b14f7b567228e Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 10 Sep 2026 01:23:03 +0100 Subject: [PATCH] labsim: add IPv6 BGP config layer for the Gateway-API public-v6 rehearsal The sim's eBGP was IPv4-only; extend it to the v6 family so labsim can rehearse Cilium BGP advertising a public-style v6 LoadBalancer /64 to the VyOS pair before it touches production (approved plan: public IPv6 via Cilium BGP + Gateway API). - sim-net-config.py: bgp6 inside bgp() -- ipv6-unicast peer-group K8S6 over fd00:2::11/12/13, prefix-list6 (le 128, /128 host routes), K8S-IN6/OUT6 route-maps (export deny -- never hand the cluster a default). ULA fd61:1e00::/64 as the sim LB range so nothing leaks into the real HE /48. All v6 lines pass the sim-net-apply.sh whitelist. - sim-ha-config.py: VLANS6 = {2: fd00:2/64} -> routers hold fd00:2::252/253 on bond0.2 so they can peer the nodes over v6 (no v6 VRRP VIP; BGP peers the per-box address, as production). - k8s-up.sh: nodes get fd00:2::11/12/13 + dual-stack k3s (dual node-ip, dual cluster/service CIDR). Peers line up with sim-net-config K8S_NODES_V6. Config-gen verified for all three. Live chunk (Cilium v6+BGP+Gateway install, bring-up, datapath/#26847 proof) is next. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DMVzWZgiKW2wquf5z8S1yH --- labsim/k8s-up.sh | 26 ++++++++++++++++++++------ labsim/sim-ha-config.py | 8 ++++++++ labsim/sim-net-config.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/labsim/k8s-up.sh b/labsim/k8s-up.sh index ce8cb35..53bc0ae 100755 --- a/labsim/k8s-up.sh +++ b/labsim/k8s-up.sh @@ -38,6 +38,14 @@ K8S_VLAN="${K8S_VLAN:-2}" K8S_PREFIX="${K8S_PREFIX:-172.31.2}" K8S_NODES="${K8S_NODES:-3}" K8S_FIRST_OCTET="${K8S_FIRST_OCTET:-11}" +# Dual-stack, mirroring production's shape (not its addresses). VLAN 2 v6 is a +# ULA so nothing here can leak into the real HE /48; node fd00:2::1x matches the +# BGP peers in sim-net-config.py (K8S_NODES_V6). cluster/service v6 are ULAs too; +# the LB pool fd61:1e00::/64 is what Cilium advertises (SERVICE_CIDR_V6 there). +K8S_PREFIX_V6="${K8S_PREFIX_V6:-fd00:2}" # nodes fd00:2::11/12/13, router ::252/::253 +K8S_ROUTER_V6="${K8S_ROUTER_V6:-fd00:2::252}" # v6 next-hop (primary router bond0.2) +CLUSTER_CIDR_V6="${CLUSTER_CIDR_V6:-fd00:42::/56}" +SERVICE_CIDR_V6="${SERVICE_CIDR_V6:-fd00:43::/112}" K8S_MEM="${K8S_MEM:-4096}" # MB — k3s + cilium + a workload K8S_CPUS="${K8S_CPUS:-2}" K8S_DISK_GB="${K8S_DISK_GB:-12}" @@ -53,6 +61,7 @@ CILIUM_VERSION="${CILIUM_VERSION:-1.19.1}" node_name() { echo "labsim-k8s$1"; } node_ip() { echo "${K8S_PREFIX}.$((K8S_FIRST_OCTET + $1 - 1))"; } +node_ip6() { echo "${K8S_PREFIX_V6}::$((K8S_FIRST_OCTET + $1 - 1))"; } # --- base image ----------------------------------------------------------- ensure_base_image() { @@ -75,7 +84,7 @@ ensure_base_image() { # before joining, because cloud-init ordering across VMs is not guaranteed and # a failed join leaves an agent that never retries. build_k8s_seed() { - local iso="$1" vm="$2" ip="$3" role="$4" server_ip="$5" pubkey="$6" + local iso="$1" vm="$2" ip="$3" role="$4" server_ip="$5" pubkey="$6" ip6="$7" server_ip6="$8" local tmp; tmp="$(mktemp -d)" cat > "$tmp/meta-data" < "$tmp/user-data" </dev/null 2>&1; then local state; state="$(virsh_q domstate "$vm" 2>/dev/null | head -1 | tr -d '\n')" @@ -189,7 +203,7 @@ create_node() { log "creating $vm ($ip, $role, ${K8S_MEM}MB/${K8S_CPUS}cpu)" sudo qemu-img create -q -f qcow2 -F qcow2 -b "$DEB_BASE" "$disk" "${K8S_DISK_GB}G" >/dev/null - build_k8s_seed "$seed" "$vm" "$ip" "$role" "$server_ip" "$pubkey" + build_k8s_seed "$seed" "$vm" "$ip" "$role" "$server_ip" "$pubkey" "$ip6" "$server_ip6" # Access port on the k8s VLAN — same broadcast domain as the routers' # bond0.2 leg, so BGP peering is directly connected exactly as in production. diff --git a/labsim/sim-ha-config.py b/labsim/sim-ha-config.py index 2ff3475..2a156d4 100755 --- a/labsim/sim-ha-config.py +++ b/labsim/sim-ha-config.py @@ -45,6 +45,12 @@ VLANS = { 10: ("172.31.10", 23), 200: ("172.31.200", 24), } +# VLAN 2 (k8s) also gets a ULA IPv6, so the routers can peer eBGP with the nodes +# over IPv6 (the neighbors in sim-net-config.py K8S_NODES_V6 = fd00:2::1x). Only +# VLAN 2 needs it for the BGP rehearsal; a ULA keeps sim traffic out of the real +# HE /48. Routers hold ::252 / ::253 (no v6 VRRP VIP -- BGP peers the real per-box +# address, exactly as production). +VLANS6 = {2: ("fd00:2", 64)} DHCP_HA_NAME = "labsim-dhcp-pair" # must not equal either host-name @@ -73,6 +79,8 @@ def build(role: str) -> list[str]: # .1 becomes the floating VIP, exactly as production will be. f"delete interfaces bonding {iface} address", f"set interfaces bonding {iface} address '{pfx}.{self_o}/{cidr}'", + *([f"set interfaces bonding {iface} address '{VLANS6[vlan][0]}::{self_o}/{VLANS6[vlan][1]}'"] + if vlan in VLANS6 else []), f"set high-availability vrrp group {g} interface bond0.{vlan}", f"set high-availability vrrp group {g} vrid {vlan}", f"set high-availability vrrp group {g} address {pfx}.1/{cidr}", diff --git a/labsim/sim-net-config.py b/labsim/sim-net-config.py index d41a20a..efd80e1 100755 --- a/labsim/sim-net-config.py +++ b/labsim/sim-net-config.py @@ -37,6 +37,18 @@ K8S_NODES = ["172.31.2.11", "172.31.2.12", "172.31.2.13"] PEER_GROUP = "K8S" PFX_LIST = "K8S-SERVICE-IPS" RM_IN, RM_OUT = "K8S-IN", "K8S-OUT" + +# IPv6 unicast. Production advertises a public Gateway LoadBalancer /64 out of +# the HE /48 (2001:470:187e:1e00::/64) and peers over VLAN 2 IPv6. The sim proves +# the MECHANISM -- ipv6-unicast eBGP, prefix-list6, /128 host routes, ECMP -- with +# a ULA so nothing here can leak into the real /48. Peering is over VLAN 2 v6 +# (nodes' fd00:2::1x <-> routers' fd00:2::25x, set in sim-ha-config.py / +# k8s-up.sh), directly connected exactly as v4. +SERVICE_CIDR_V6 = "fd61:1e00::/64" # sim analog of prod :1e00::/64 LB pool +K8S_NODES_V6 = ["fd00:2::11", "fd00:2::12", "fd00:2::13"] +PEER_GROUP_V6 = "K8S6" +PFX_LIST_V6 = "K8S-SERVICE-IPS-V6" +RM_IN_V6, RM_OUT_V6 = "K8S-IN6", "K8S-OUT6" # One route per node; ECMP across all three. 4 leaves headroom for a fourth node # without a config change. MAX_PATHS = 4 @@ -123,6 +135,26 @@ def bgp(role: str) -> list[str]: f"set protocols bgp peer-group {PEER_GROUP} address-family ipv4-unicast maximum-prefix {MAX_PREFIX}", ] out += [f"set protocols bgp neighbor {n} peer-group {PEER_GROUP}" for n in K8S_NODES] + # --- IPv6 unicast: same policy shape, over a separate v6 peer-group --- + # `prefix-list6` + `match ipv6 address` are the v6 spellings; `le 128` because + # Cilium advertises each Gateway LoadBalancer address as a /128, not the + # aggregate. Separate peer-group because the neighbors are v6 addresses; the + # export deny is the same safety property (never hand the cluster a default). + out += [ + "", + f"set policy prefix-list6 {PFX_LIST_V6} rule 10 action permit", + f"set policy prefix-list6 {PFX_LIST_V6} rule 10 prefix {SERVICE_CIDR_V6}", + f"set policy prefix-list6 {PFX_LIST_V6} rule 10 le 128", + f"set policy route-map {RM_IN_V6} rule 10 action permit", + f"set policy route-map {RM_IN_V6} rule 10 match ipv6 address prefix-list {PFX_LIST_V6}", + f"set policy route-map {RM_OUT_V6} rule 10 action deny", + f"set protocols bgp address-family ipv6-unicast maximum-paths ebgp {MAX_PATHS}", + f"set protocols bgp peer-group {PEER_GROUP_V6} remote-as {CLUSTER_AS}", + f"set protocols bgp peer-group {PEER_GROUP_V6} address-family ipv6-unicast route-map import {RM_IN_V6}", + f"set protocols bgp peer-group {PEER_GROUP_V6} address-family ipv6-unicast route-map export {RM_OUT_V6}", + f"set protocols bgp peer-group {PEER_GROUP_V6} address-family ipv6-unicast maximum-prefix {MAX_PREFIX}", + ] + out += [f"set protocols bgp neighbor {n} peer-group {PEER_GROUP_V6}" for n in K8S_NODES_V6] out.append("") return out