fix(microvm): don't /32 AdvertiseIP via mgmt for internet-facing single-node

buildMicrovmNICs overrode resolveMgmtRoute's deliberate empty return
for the single-node internet-facing case by falling back to
(MgmtBridgeIP, AdvertiseIP). That installed a /32 route to AdvertiseIP
via the mgmt NIC inside the LB microvm. On single-node where the
AWSGW host IP equals the WAN client IP, the /32 also catches reply
traffic for inbound HTTP/TCP to the LB's floating IP — replies leave
via the mgmt NIC with src=data-NIC IP, the host kernel drops the
asymmetric flow, and same-chassis ingress hangs.

resolveMgmtRoute already encodes the correct policy:
  - multi-node any scheme        → explicit MgmtRoute{Gateway,Target}
  - single-node internal         → fallback to (MgmtBridgeIP, AdvertiseIP)
  - single-node internet-facing  → empty (reach AWSGW via VPC SNAT)

Honor it. Matches the legacy lbVMUserData path, whose own comment
flagged the stolen-WAN-return-path symptom this introduced.

Test covers all three branches.
This commit is contained in:
joshsiv-mulga
2026-05-18 13:08:27 +10:00
parent 79e6dcdd2d
commit b59fc4b433
2 changed files with 35 additions and 6 deletions
+32
View File
@@ -436,3 +436,35 @@ func TestBuildMicrovmNICs_ExtraENIs(t *testing.T) {
assert.Equal(t, "02:aa:bb:cc:dd:02", nics[2].MAC)
assert.False(t, nics[2].IsDefault)
}
// TestBuildMicrovmNICs_MgmtRoute covers the same single-node/multi-node matrix
// as TestLBVMUserData_InternalSingleNodeFallback: internet-facing single-node
// must NOT emit a /32 via mgmt to AdvertiseIP because that steals the host's
// WAN return path for reply traffic, breaking same-chassis ingress.
func TestBuildMicrovmNICs_MgmtRoute(t *testing.T) {
mk := func() *ELBv2ServiceImpl {
return &ELBv2ServiceImpl{
VPCService: nil,
MgmtBridgeIP: "10.15.8.1",
AdvertiseIP: "192.168.1.33",
}
}
internal := mk().buildMicrovmNICs("10.0.1.5", "02:aa:bb:cc:dd:01", "subnet-abc", "eni-abc", SchemeInternal, nil, testAccountID)
require.Len(t, internal, 2)
assert.Equal(t, "192.168.1.33", internal[1].RouteDst, "internal single-node forces /32 via mgmt")
assert.Equal(t, "10.15.8.1", internal[1].RouteVia)
inet := mk().buildMicrovmNICs("10.0.1.5", "02:aa:bb:cc:dd:01", "subnet-abc", "eni-abc", SchemeInternetFacing, nil, testAccountID)
require.Len(t, inet, 2)
assert.Empty(t, inet[1].RouteDst, "internet-facing single-node must not /32 AdvertiseIP via mgmt")
assert.Empty(t, inet[1].RouteVia)
multi := mk()
multi.MgmtRouteGateway = "10.15.8.1"
multi.MgmtRouteTarget = "10.15.8.100"
nics := multi.buildMicrovmNICs("10.0.1.5", "02:aa:bb:cc:dd:01", "subnet-abc", "eni-abc", SchemeInternetFacing, nil, testAccountID)
require.Len(t, nics, 2)
assert.Equal(t, "10.15.8.100", nics[1].RouteDst, "multi-node uses explicit MgmtRouteTarget")
assert.Equal(t, "10.15.8.1", nics[1].RouteVia)
}
+3 -6
View File
@@ -329,13 +329,10 @@ func (s *ELBv2ServiceImpl) buildMicrovmNICs(primaryIP, primaryMAC, primarySubnet
// NIC[1]: management NIC — daemon fills MAC/CIDR after IP allocation.
// RouteDst = AWSGW IP (destination), RouteVia = br-mgmt IP (next-hop).
// Microvm always uses mgmt for heartbeats regardless of scheme; VPC SNAT
// is not reliable across all topologies for control-plane traffic.
// Honor resolveMgmtRoute's deliberate empty return for single-node
// internet-facing: adding a /32 to AdvertiseIP via mgmt steals the host's
// WAN return path for reply traffic, breaking same-chassis ingress.
mgmtRouteGW, mgmtRouteTarget := s.resolveMgmtRoute(scheme)
if mgmtRouteGW == "" && s.MgmtBridgeIP != "" && s.AdvertiseIP != "" {
mgmtRouteGW = s.MgmtBridgeIP
mgmtRouteTarget = s.AdvertiseIP
}
nics = append(nics, NICConfig{
IsDefault: false,
RouteDst: mgmtRouteTarget, // AWSGW IP — destination to reach