labsim: add IPv6 BGP config layer for the Gateway-API public-v6 rehearsal
Some checks failed
CI/CD / lint (push) Failing after 24s
CI/CD / typecheck (push) Failing after 22s
CI/CD / test (push) Failing after 23s
CI/CD / build (push) Has been skipped
CI/CD / publish-rpm (push) Has been skipped
CI/CD / publish-deb (push) Has been skipped

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMVzWZgiKW2wquf5z8S1yH
This commit is contained in:
Michal
2026-09-10 01:23:03 +01:00
parent a4dcc9b379
commit c729275961
3 changed files with 60 additions and 6 deletions

View File

@@ -38,6 +38,14 @@ K8S_VLAN="${K8S_VLAN:-2}"
K8S_PREFIX="${K8S_PREFIX:-172.31.2}" K8S_PREFIX="${K8S_PREFIX:-172.31.2}"
K8S_NODES="${K8S_NODES:-3}" K8S_NODES="${K8S_NODES:-3}"
K8S_FIRST_OCTET="${K8S_FIRST_OCTET:-11}" 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_MEM="${K8S_MEM:-4096}" # MB — k3s + cilium + a workload
K8S_CPUS="${K8S_CPUS:-2}" K8S_CPUS="${K8S_CPUS:-2}"
K8S_DISK_GB="${K8S_DISK_GB:-12}" 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_name() { echo "labsim-k8s$1"; }
node_ip() { echo "${K8S_PREFIX}.$((K8S_FIRST_OCTET + $1 - 1))"; } node_ip() { echo "${K8S_PREFIX}.$((K8S_FIRST_OCTET + $1 - 1))"; }
node_ip6() { echo "${K8S_PREFIX_V6}::$((K8S_FIRST_OCTET + $1 - 1))"; }
# --- base image ----------------------------------------------------------- # --- base image -----------------------------------------------------------
ensure_base_image() { ensure_base_image() {
@@ -75,7 +84,7 @@ ensure_base_image() {
# before joining, because cloud-init ordering across VMs is not guaranteed and # before joining, because cloud-init ordering across VMs is not guaranteed and
# a failed join leaves an agent that never retries. # a failed join leaves an agent that never retries.
build_k8s_seed() { 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)" local tmp; tmp="$(mktemp -d)"
cat > "$tmp/meta-data" <<EOF cat > "$tmp/meta-data" <<EOF
@@ -89,7 +98,7 @@ ethernets:
enp1s0: enp1s0:
match: match:
name: "en*" name: "en*"
addresses: [$ip/24] addresses: [$ip/24, $ip6/64]
routes: routes:
- to: default - to: default
via: ${K8S_PREFIX}.1 via: ${K8S_PREFIX}.1
@@ -100,10 +109,13 @@ EOF
local k3s_exec local k3s_exec
if [ "$role" = "server" ]; then if [ "$role" = "server" ]; then
# flannel/servicelb/traefik off: Cilium is the CNI under test, and k3s's # flannel/servicelb/traefik off: Cilium is the CNI under test, and k3s's
# own ServiceLB would fight Cilium for LoadBalancer addresses. # own ServiceLB would fight Cilium for LoadBalancer addresses. Dual-stack:
k3s_exec="server --flannel-backend=none --disable-network-policy --disable=servicelb --disable=traefik --node-ip=$ip --tls-san=$ip --cluster-init" # both families in cluster-cidr/service-cidr and a dual node-ip, mirroring
# production. Pod IPs come from Cilium cluster-pool (node.spec.podCIDRs is
# immutable), so cluster-cidr v6 just marks the cluster dual-stack.
k3s_exec="server --flannel-backend=none --disable-network-policy --disable=servicelb --disable=traefik --node-ip=$ip,$ip6 --tls-san=$ip --cluster-cidr=10.42.0.0/16,${CLUSTER_CIDR_V6} --service-cidr=10.43.0.0/16,${SERVICE_CIDR_V6} --cluster-init"
else else
k3s_exec="agent --server https://${server_ip}:6443 --node-ip=$ip" k3s_exec="agent --server https://${server_ip}:6443 --node-ip=$ip,$ip6"
fi fi
cat > "$tmp/user-data" <<EOF cat > "$tmp/user-data" <<EOF
@@ -170,8 +182,10 @@ create_node() {
local n="$1" pubkey="$2" local n="$1" pubkey="$2"
local vm; vm="$(node_name "$n")" local vm; vm="$(node_name "$n")"
local ip; ip="$(node_ip "$n")" local ip; ip="$(node_ip "$n")"
local ip6; ip6="$(node_ip6 "$n")"
local role="agent"; [ "$n" -eq 1 ] && role="server" local role="agent"; [ "$n" -eq 1 ] && role="server"
local server_ip; server_ip="$(node_ip 1)" local server_ip; server_ip="$(node_ip 1)"
local server_ip6; server_ip6="$(node_ip6 1)"
if virsh_q dominfo "$vm" >/dev/null 2>&1; then if virsh_q dominfo "$vm" >/dev/null 2>&1; then
local state; state="$(virsh_q domstate "$vm" 2>/dev/null | head -1 | tr -d '\n')" 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)" 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 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' # 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. # bond0.2 leg, so BGP peering is directly connected exactly as in production.

View File

@@ -45,6 +45,12 @@ VLANS = {
10: ("172.31.10", 23), 10: ("172.31.10", 23),
200: ("172.31.200", 24), 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 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. # .1 becomes the floating VIP, exactly as production will be.
f"delete interfaces bonding {iface} address", f"delete interfaces bonding {iface} address",
f"set interfaces bonding {iface} address '{pfx}.{self_o}/{cidr}'", 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} interface bond0.{vlan}",
f"set high-availability vrrp group {g} vrid {vlan}", f"set high-availability vrrp group {g} vrid {vlan}",
f"set high-availability vrrp group {g} address {pfx}.1/{cidr}", f"set high-availability vrrp group {g} address {pfx}.1/{cidr}",

View File

@@ -37,6 +37,18 @@ K8S_NODES = ["172.31.2.11", "172.31.2.12", "172.31.2.13"]
PEER_GROUP = "K8S" PEER_GROUP = "K8S"
PFX_LIST = "K8S-SERVICE-IPS" PFX_LIST = "K8S-SERVICE-IPS"
RM_IN, RM_OUT = "K8S-IN", "K8S-OUT" 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 # One route per node; ECMP across all three. 4 leaves headroom for a fourth node
# without a config change. # without a config change.
MAX_PATHS = 4 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}", 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] 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("") out.append("")
return out return out