labsim: add IPv6 BGP config layer for the Gateway-API public-v6 rehearsal
Some checks failed
Some checks failed
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:
@@ -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" <<EOF
|
||||
@@ -89,7 +98,7 @@ ethernets:
|
||||
enp1s0:
|
||||
match:
|
||||
name: "en*"
|
||||
addresses: [$ip/24]
|
||||
addresses: [$ip/24, $ip6/64]
|
||||
routes:
|
||||
- to: default
|
||||
via: ${K8S_PREFIX}.1
|
||||
@@ -100,10 +109,13 @@ EOF
|
||||
local k3s_exec
|
||||
if [ "$role" = "server" ]; then
|
||||
# flannel/servicelb/traefik off: Cilium is the CNI under test, and k3s's
|
||||
# own ServiceLB would fight Cilium for LoadBalancer addresses.
|
||||
k3s_exec="server --flannel-backend=none --disable-network-policy --disable=servicelb --disable=traefik --node-ip=$ip --tls-san=$ip --cluster-init"
|
||||
# own ServiceLB would fight Cilium for LoadBalancer addresses. Dual-stack:
|
||||
# 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
|
||||
k3s_exec="agent --server https://${server_ip}:6443 --node-ip=$ip"
|
||||
k3s_exec="agent --server https://${server_ip}:6443 --node-ip=$ip,$ip6"
|
||||
fi
|
||||
|
||||
cat > "$tmp/user-data" <<EOF
|
||||
@@ -170,8 +182,10 @@ create_node() {
|
||||
local n="$1" pubkey="$2"
|
||||
local vm; vm="$(node_name "$n")"
|
||||
local ip; ip="$(node_ip "$n")"
|
||||
local ip6; ip6="$(node_ip6 "$n")"
|
||||
local role="agent"; [ "$n" -eq 1 ] && role="server"
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user