mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
fix(network): restore centralised EIP gateway-port neigh prime (#355)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user