fix(pool): RHEL containers need nmcli IPv6 config, not just networkd
poolContainerScript only rendered Debian systemd-networkd files (/etc/systemd/network/eth0.network binding the public /128). RHEL-family images use NetworkManager, which ignores networkd config, so a pool-mode Alma/Rocky container never bound its routed-NIC /128 and had no public IPv6 — while Debian worked. Verified on a whitelist-provider host: the nmcli path (eth0 = manual IPv6, /128, fe80::1 gateway, IPv4 disabled; eth1 = DHCPv4) brings up the /128, the default route and v4/v6/DNS connectivity, and the connection survives reboots. - routed_ipv6.go: poolContainerScript branches on the guest stack — RHEL/NetworkManager gets a static nmcli connection matched by DEVICE (name-grep fallback), Debian keeps the networkd files. Idempotent and self-healing on reinstall/ipv6-reapply. - ipv6_test.go: TestPoolContainerScript asserts both branches render the /128, fe80::1 gateway and DHCPv4 markers.
This commit is contained in:
1 parent
555401ea5b
commit
0adc50a4db
2 files changed
+73
-13
No files matched your search
@@ -233,3 +233,39 @@ func TestCheckIPv6BlockCollision(t *testing.T) {
|
||||
t.Errorf("nil block should be a no-op: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The pool container script must configure BOTH guest stacks: Debian gets the
|
||||
// systemd-networkd files binding the public /128 + fe80::1 route on eth0 and
|
||||
// DHCPv4 on eth1; RHEL-family images (NetworkManager) get the same layout as a
|
||||
// static nmcli connection — the networkd files alone would be ignored there,
|
||||
// leaving the routed NIC without its /128 and the container with no public IPv6.
|
||||
func TestPoolContainerScript(t *testing.T) {
|
||||
c := cfg.Default()
|
||||
m := &Manager{cfg: c}
|
||||
addr := "2a03:b0c0:2:f0:0:1:dbc2:1006"
|
||||
script, err := m.poolContainerScript(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Both stacks: the branch selection and the address/gateway/DHCP markers.
|
||||
for _, want := range []string{
|
||||
"nmcli", // RHEL path present
|
||||
"ipv6.method manual", // RHEL: NM owns the IPv6 stack
|
||||
"ipv6.addresses " + addr + "/128", // public /128 bound on eth0
|
||||
"ipv6.gateway fe80::1", // routed-NIC gateway
|
||||
"ipv4.method disabled", // v4 lives on eth1 only
|
||||
"systemd-networkd", // Debian path present
|
||||
"Address=" + addr + "/128", // Debian: static bind
|
||||
"Gateway=fe80::1", // Debian: default route
|
||||
"DHCP=ipv4", // eth1 DHCPv4 on both stacks
|
||||
} {
|
||||
if !strings.Contains(script, want) {
|
||||
t.Errorf("script missing %q:\n%s", want, script)
|
||||
}
|
||||
}
|
||||
// An empty address is a no-op (V4-only container) — the empty check lives in
|
||||
// ipv6ContainerScriptFor, the pool script's only caller.
|
||||
if s, _ := m.ipv6ContainerScriptFor("", ""); s != "" {
|
||||
t.Errorf("expected empty script for empty address, got %q", s)
|
||||
}
|
||||
}
|
||||
@@ -127,21 +127,45 @@ fi
|
||||
return script, nil
|
||||
}
|
||||
|
||||
// poolContainerScript renders the Debian systemd-networkd config for a pool
|
||||
// container: eth0 (routed NIC) statically binds the public /128 with the
|
||||
// fe80::1 gateway, eth1 (bridged NIC) runs DHCPv4 on the private bridge.
|
||||
// eth0 also gets public IPv6 nameservers: the routed NIC has no DHCPv6/RA DNS
|
||||
// source, and systemd-resolved's stub (127.0.0.53) cannot resolve v6-only
|
||||
// names without an upstream v6 DNS. Idempotent and self-healing.
|
||||
// poolContainerScript renders the guest-side network config for a pool
|
||||
// container, on whichever stack the image ships:
|
||||
//
|
||||
// - eth0 — routed NIC: the public /128 bound statically with fe80::1 (Incus's
|
||||
// routed-NIC gateway) as the default route, no DHCPv6/RA.
|
||||
// - eth1 — bridged NIC on the private bridge: DHCPv4 for the shared IPv4.
|
||||
//
|
||||
// RHEL-family images use NetworkManager, which ignores the systemd-networkd
|
||||
// files Debian relies on — so there the eth0 connection is declared a STATIC
|
||||
// IPv6 profile via nmcli (manual method + /128 + gateway fe80::1 + no IPv4)
|
||||
// and eth1 keeps DHCPv4. Debian gets the networkd files, which also carry the
|
||||
// public IPv6 nameservers (the routed NIC has no DHCPv6/RA DNS source, and
|
||||
// systemd-resolved's 127.0.0.53 stub cannot resolve v6-only names without an
|
||||
// upstream v6 DNS). Both paths are idempotent and self-healing on reapply; the
|
||||
// nmcli profile survives reboots, so the /128 comes back on every boot.
|
||||
func (m *Manager) poolContainerScript(ipv6 string) (string, error) {
|
||||
return fmt.Sprintf(`set -e
|
||||
for i in $(seq 1 40); do
|
||||
[ -S /run/systemd/private ] && break
|
||||
sleep 0.5
|
||||
done
|
||||
mkdir -p /etc/systemd/network
|
||||
# eth0: public routed /128, static, no DHCP (v4 lives on eth1).
|
||||
cat > /etc/systemd/network/eth0.network <<'EOF'
|
||||
if command -v nmcli >/dev/null 2>&1 && ! systemctl is-active systemd-networkd >/dev/null 2>&1; then
|
||||
# RHEL-family (NetworkManager): the routed eth0 carries only the public /128
|
||||
# (v4 lives on eth1), so its connection is manual IPv6 + disabled IPv4 with
|
||||
# the fe80::1 gateway. eth1 keeps DHCPv4. Match the connection by DEVICE
|
||||
# (names vary between distros) and fall back to a name grep.
|
||||
CONN0=$(nmcli -t -f NAME,DEVICE con show 2>/dev/null | awk -F: '$2 == "eth0" {print $1; exit}')
|
||||
[ -z "$CONN0" ] && CONN0=$(nmcli -t -f NAME con show 2>/dev/null | grep -i eth0 | head -1)
|
||||
[ -n "$CONN0" ] && nmcli con mod "$CONN0" ipv6.method manual ipv6.addresses %s/128 ipv6.gateway fe80::1 ipv4.method disabled 2>/dev/null || true
|
||||
[ -n "$CONN0" ] && nmcli con up "$CONN0" >/dev/null 2>&1 || true
|
||||
CONN1=$(nmcli -t -f NAME,DEVICE con show 2>/dev/null | awk -F: '$2 == "eth1" {print $1; exit}')
|
||||
[ -z "$CONN1" ] && CONN1=$(nmcli -t -f NAME con show 2>/dev/null | grep -i eth1 | head -1)
|
||||
[ -n "$CONN1" ] && nmcli con mod "$CONN1" ipv4.method auto 2>/dev/null || true
|
||||
[ -n "$CONN1" ] && nmcli con up "$CONN1" >/dev/null 2>&1 || true
|
||||
else
|
||||
# Debian (systemd-networkd): eth0 binds the public /128 statically with the
|
||||
# public IPv6 nameservers; eth1 runs DHCPv4 on the private bridge.
|
||||
mkdir -p /etc/systemd/network
|
||||
cat > /etc/systemd/network/eth0.network <<'EOF'
|
||||
[Match]
|
||||
Name=eth0
|
||||
|
||||
@@ -158,16 +182,16 @@ Address=%s/128
|
||||
Destination=::/0
|
||||
Gateway=fe80::1
|
||||
EOF
|
||||
# eth1: private bridge, DHCPv4.
|
||||
cat > /etc/systemd/network/eth1.network <<'EOF'
|
||||
cat > /etc/systemd/network/eth1.network <<'EOF'
|
||||
[Match]
|
||||
Name=eth1
|
||||
|
||||
[Network]
|
||||
DHCP=ipv4
|
||||
EOF
|
||||
systemctl restart systemd-networkd || true
|
||||
`, ipv6), nil
|
||||
systemctl restart systemd-networkd || true
|
||||
fi
|
||||
`, ipv6, ipv6), nil
|
||||
}
|
||||
|
||||
// ConfigureContainerIPv6 applies the host-routed IPv6 setup to one container
|
||||
|
||||
Reference in new issue
Block a user