fix(network): restore centralised EIP gateway-port neigh prime (#355)

This commit is contained in:
Julian Sommer
2026-06-12 14:23:01 +10:00
committed by GitHub
parent a2ede7c476
commit 2c6f65b2e9
2 changed files with 66 additions and 11 deletions
+28 -8
View File
@@ -171,7 +171,7 @@ func (m *natManager) AddEIP(ctx context.Context, eip EIPSpec) error {
"router", router, "external_ip", eip.ExternalIP, "logical_ip", eip.LogicalIP)
// Skip row churn but still re-prime: stop->start re-attaches the same EIP
// and the host neigh stays dark until ARP times out without a fresh prime.
m.primeReachability(eip, distributed)
m.primeReachability(ctx, eip, distributed)
return nil
}
@@ -188,22 +188,42 @@ func (m *natManager) AddEIP(ctx context.Context, eip EIPSpec) error {
if err := m.barrier(); err != nil {
slog.Warn("policy: AddEIP flows barrier failed", "external_ip", eip.ExternalIP, "logical_ip", eip.LogicalIP, "err", err)
}
m.primeReachability(eip, distributed)
m.primeReachability(ctx, eip, distributed)
return nil
}
// primeReachability re-announces an EIP: distributed mode programs the ARP entry
// directly; centralised mode (no MAC) flushes and re-arms ARP. Best-effort.
func (m *natManager) primeReachability(eip EIPSpec, distributed bool) {
if distributed {
if err := m.neighPrime(eip); err != nil {
// primeReachability programs the host neigh entry to the MAC owning the EIP on
// the WAN segment — external_mac (distributed) or gateway router-port MAC
// (centralised) — falling back to an ARP flush when no MAC resolves.
func (m *natManager) primeReachability(ctx context.Context, eip EIPSpec, distributed bool) {
primeMAC := eip.MAC
if !distributed {
primeMAC = m.gatewayPortMAC(ctx, eip.VPCID)
}
if primeMAC != "" {
primed := eip
primed.MAC = primeMAC
if err := m.neighPrime(primed); err != nil {
slog.Warn("policy: AddEIP neighbour prime failed", "external_ip", eip.ExternalIP, "logical_ip", eip.LogicalIP, "err", err)
}
} else if err := m.neigh(eip.ExternalIP); err != nil {
return
}
if err := m.neigh(eip.ExternalIP); err != nil {
slog.Warn("policy: AddEIP neighbour flush failed", "external_ip", eip.ExternalIP, "logical_ip", eip.LogicalIP, "err", err)
}
}
// gatewayPortMAC returns the MAC of the VPC gateway router's external port, the
// L2 owner of a centralised EIP on the WAN segment. Empty on lookup miss so the
// caller falls back to an ARP flush.
func (m *natManager) gatewayPortMAC(ctx context.Context, vpcID string) string {
lrp, err := m.ovn.GetLogicalRouterPort(ctx, topology.GatewayRouterPort(vpcID))
if err != nil || lrp == nil {
return ""
}
return lrp.MAC
}
func (m *natManager) DeleteEIP(ctx context.Context, vpcID, externalIP, logicalIP string) error {
router := topology.VPCRouter(vpcID)
if err := m.ovn.DeleteNAT(ctx, router, "dnat_and_snat", logicalIP); err != nil {
+38 -3
View File
@@ -13,6 +13,13 @@ import (
"github.com/mulgadc/spinifex/spinifex/network/topology"
)
func seedGatewayPort(t *testing.T, m *mock.Client, vpcID, mac string) {
t.Helper()
require.NoError(t, m.CreateLogicalRouterPort(context.Background(),
topology.VPCRouter(vpcID),
&nbdb.LogicalRouterPort{Name: topology.GatewayRouterPort(vpcID), MAC: mac}))
}
func seedRouter(t *testing.T, m *mock.Client, vpcID string) string {
t.Helper()
name := topology.VPCRouter(vpcID)
@@ -142,10 +149,11 @@ func TestNATManager_NeighPrime_OnDistributedAttach_FlushOnDetach(t *testing.T) {
"detach must still flush the released external IP")
}
func TestNATManager_NeighFlush_OnCentralizedAttach(t *testing.T) {
func TestNATManager_NeighPrime_OnCentralizedAttach(t *testing.T) {
ctx := context.Background()
m := mock.New()
seedRouter(t, m, "vpc-1")
seedGatewayPort(t, m, "vpc-1", "02:gw:00:00:00:01")
var primed []EIPSpec
var flushed []string
nm, err := NewNATManager(m, NATModeCentralized,
@@ -162,9 +170,36 @@ func TestNATManager_NeighFlush_OnCentralizedAttach(t *testing.T) {
require.NoError(t, nm.AddEIP(ctx, EIPSpec{
VPCID: "vpc-1", ExternalIP: "1.2.3.4", LogicalIP: "10.0.0.5",
}))
assert.Empty(t, primed, "centralized attach has no external_mac to prime")
require.Len(t, primed, 1,
"centralized attach must prime the host neighbour with the gateway port MAC")
assert.Equal(t, "02:gw:00:00:00:01", primed[0].MAC,
"centralized prime must use the VPC gateway router-port MAC")
assert.Empty(t, flushed, "a successful prime must not also flush")
}
func TestNATManager_NeighFlush_CentralizedNoGatewayMAC(t *testing.T) {
ctx := context.Background()
m := mock.New()
seedRouter(t, m, "vpc-1") // no gateway port seeded → MAC unresolvable
var primed []EIPSpec
var flushed []string
nm, err := NewNATManager(m, NATModeCentralized,
WithNeighPrimer(func(eip EIPSpec) error {
primed = append(primed, eip)
return nil
}),
WithNeighFlusher(func(externalIP string) error {
flushed = append(flushed, externalIP)
return nil
}))
require.NoError(t, err)
require.NoError(t, nm.AddEIP(ctx, EIPSpec{
VPCID: "vpc-1", ExternalIP: "1.2.3.4", LogicalIP: "10.0.0.5",
}))
assert.Empty(t, primed, "no gateway MAC to prime")
assert.Equal(t, []string{"1.2.3.4"}, flushed,
"centralized attach must fall back to a neighbour flush")
"centralized attach must fall back to a neighbour flush when no MAC resolves")
}
func TestNATManager_NeighHooks_FailureNonFatal(t *testing.T) {