feat: veth pair bridge — link Linux bridge to OVS without conversion

setup-ovn.sh now detects whether the default route is on a Linux bridge
or OVS bridge. When it's a Linux bridge (e.g. br-wan from cloud-init),
instead of destructively converting it, the script creates an OVS bridge
(br-ext) and links them with a veth pair. The Linux bridge keeps its IP,
routes, and ports untouched — no WAN interruption.

Also fixes all pre-existing lint issues (gofmt, ineffassign, errchkjson,
gosec, modernize, whitespace) to bring golangci-lint to 0 issues.

Tested: pool mode with static IP range on single-node (ben-banksia).
VM gets public IP 192.168.1.201, SSH works, outbound internet works.
This commit is contained in:
Ben Duncan
2026-03-25 10:01:37 +10:00
parent c2992ebe5e
commit 4d56565c3f
18 changed files with 167 additions and 86 deletions
+26 -21
View File
@@ -843,13 +843,13 @@ func runAdminInit(cmd *cobra.Command, args []string) {
OVNNBAddr: "tcp:127.0.0.1:6641",
OVNSBAddr: "tcp:127.0.0.1:6642",
ExternalMode: externalMode,
ExternalIface: externalIface,
WanBridge: detectedWanBridge(detectedNet),
ExternalDHCP: useExternalDHCP,
PoolName: "wan",
PoolStart: poolStart,
PoolEnd: poolEnd,
ExternalMode: externalMode,
ExternalIface: externalIface,
WanBridge: detectedWanBridge(detectedNet),
ExternalDHCP: useExternalDHCP,
PoolName: "wan",
PoolStart: poolStart,
PoolEnd: poolEnd,
PoolGateway: externalGateway,
PoolGatewayIP: gatewayIP,
PoolPrefixLen: externalPrefixLen,
@@ -1704,18 +1704,23 @@ func writeBootstrapFilesWithAdmin(configDir string, masterKey []byte, accessKey,
return handlers_iam.SaveBootstrapData(filepath.Join(configDir, "bootstrap.json"), bd)
}
// detectDNSServers reads the DNS servers configured on the host for the given
// detectedWanBridge returns the WAN bridge name from the detected network
// topology. If the WAN interface is already a bridge, returns its name.
// Otherwise returns "br-wan" as the default.
// detectedWanBridge returns the OVS bridge name that OVN bridge-mappings should
// use for WAN traffic. If the WAN interface is already an OVS bridge, returns
// its name directly. If it's a Linux bridge, returns "br-ext" — setup-ovn.sh
// will create this OVS bridge and link it to the Linux bridge via a veth pair.
// If it's a physical NIC, returns "br-wan" as the default.
func detectedWanBridge(detected *admin.DetectedNetwork) string {
if detected == nil || detected.WAN == nil {
return ""
}
// Check if the WAN interface name looks like a bridge (starts with "br-")
// If so, it's already set up — use it directly.
if strings.HasPrefix(detected.WAN.Name, "br-") {
return detected.WAN.Name
name := detected.WAN.Name
if strings.HasPrefix(name, "br-") {
// Check if it's an OVS bridge (use directly) or Linux bridge (need br-ext)
if out, err := exec.Command("sudo", "ovs-vsctl", "br-exists", name).CombinedOutput(); err == nil && len(out) == 0 {
return name // OVS bridge — use directly
}
// Linux bridge — setup-ovn.sh creates br-ext + veth pair
return "br-ext"
}
return "br-wan"
}
@@ -1748,7 +1753,7 @@ func detectDNSServers(iface string) []string {
data, err := os.ReadFile("/etc/resolv.conf")
if err == nil {
var servers []string
for _, line := range strings.Split(string(data), "\n") {
for line := range strings.SplitSeq(string(data), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "nameserver ") {
ip := strings.TrimSpace(strings.TrimPrefix(line, "nameserver"))
@@ -1774,14 +1779,14 @@ func detectDNSServers(iface string) []string {
// Format: "Link 2 (enp0s3): 192.168.1.1 8.8.8.8 1.1.1.1"
func parseDNSFromResolvectl(output string) []string {
var servers []string
for _, line := range strings.Split(output, "\n") {
for line := range strings.SplitSeq(output, "\n") {
// Find the colon separator, IPs come after it
idx := strings.Index(line, ":")
if idx < 0 {
_, after, ok := strings.Cut(line, ":")
if !ok {
continue
}
fields := strings.Fields(line[idx+1:])
for _, f := range fields {
fields := strings.FieldsSeq(after)
for f := range fields {
if net.ParseIP(f) != nil && f != "127.0.0.53" && f != "127.0.0.1" {
servers = append(servers, f)
}
+6
View File
@@ -101,6 +101,12 @@ else
echo " /var/lib/ovn not found, skipping OVN DB cleanup"
fi
# Remove veth pair created by setup-ovn.sh (veth mode — Linux bridge ↔ OVS bridge)
if ip link show veth-wan-br >/dev/null 2>&1; then
echo " Deleting veth pair: veth-wan-br ↔ veth-wan-ovs"
sudo ip link del veth-wan-br 2>/dev/null || true
fi
# Remove macvlan interfaces created by setup-ovn.sh
for iface in $(ip -o link show type macvlan 2>/dev/null | awk -F': ' '{print $2}' | grep '^spx-ext-'); do
echo " Deleting macvlan: $iface"
+75 -12
View File
@@ -26,7 +26,9 @@
#
# WAN Bridge Auto-Detection:
# When no --wan-bridge is given, the script checks the default route interface:
# - If it's already a bridge (e.g. br-wan from cloud-init) → use it directly
# - If it's an OVS bridge → use it directly for bridge-mappings
# - If it's a Linux bridge → create OVS br-ext + veth pair to link them
# (non-destructive, Linux bridge keeps IP/routes, no interruption)
# - If it's a physical NIC → stop and print guidance (cannot safely move NIC)
#
# Examples:
@@ -81,7 +83,8 @@ done
# --- WAN bridge auto-detection ---
# Determine the WAN bridge name and how to set it up.
WAN_BRIDGE_MODE="" # "existing", "direct", "macvlan", or ""
WAN_BRIDGE_MODE="" # "existing", "veth", "direct", "macvlan", or ""
LINUX_BRIDGE="" # Set when WAN_BRIDGE_MODE="veth" — the Linux bridge behind the veth pair
detect_wan_bridge() {
# If --wan-bridge was explicitly given, use it
@@ -130,10 +133,19 @@ detect_wan_bridge() {
fi
if [ "$is_bridge" = true ]; then
# Default route goes through a bridge — use it directly
WAN_BRIDGE="$default_dev"
WAN_BRIDGE_MODE="existing"
echo " Auto-detected WAN bridge: $WAN_BRIDGE (default route interface)"
if sudo ovs-vsctl br-exists "$default_dev" 2>/dev/null; then
# Already an OVS bridge — use it directly for bridge-mappings
WAN_BRIDGE="$default_dev"
WAN_BRIDGE_MODE="existing"
echo " Auto-detected WAN bridge: $WAN_BRIDGE (OVS bridge, default route)"
else
# Linux bridge — link to OVS via veth pair (non-destructive)
LINUX_BRIDGE="$default_dev"
WAN_BRIDGE="br-ext"
WAN_BRIDGE_MODE="veth"
echo " Auto-detected Linux bridge: $LINUX_BRIDGE (default route)"
echo " Will create OVS bridge br-ext + veth pair to link them"
fi
return
fi
@@ -196,6 +208,9 @@ echo "=== Spinifex OVN Compute Node Setup ==="
echo " Management node: $MANAGEMENT"
if [ -n "$WAN_BRIDGE" ]; then
echo " WAN bridge: $WAN_BRIDGE ($WAN_BRIDGE_MODE)"
if [ -n "$LINUX_BRIDGE" ]; then
echo " Linux bridge: $LINUX_BRIDGE (linked via veth pair)"
fi
if [ -n "$WAN_IFACE" ]; then
echo " WAN interface: $WAN_IFACE"
fi
@@ -280,16 +295,56 @@ if [ -n "$WAN_BRIDGE" ]; then
case "$WAN_BRIDGE_MODE" in
existing)
# Bridge already exists (e.g. br-wan from cloud-init).
# Ensure it's registered as an OVS bridge. If it's a Linux bridge
# that OVS doesn't know about, create a new OVS bridge with the
# same name — OVS will take over.
# Already an OVS bridge (from a previous run or explicit --wan-bridge).
if ! sudo ovs-vsctl br-exists "$WAN_BRIDGE" 2>/dev/null; then
sudo ovs-vsctl --may-exist add-br "$WAN_BRIDGE"
echo " created OVS bridge: $WAN_BRIDGE"
fi
sudo ip link set "$WAN_BRIDGE" up
echo " $WAN_BRIDGE: existing bridge, up"
echo " $WAN_BRIDGE: OVS bridge, up"
;;
veth)
# Linux bridge detected (e.g. br-wan from cloud-init/netplan).
# OVN bridge-mappings require an OVS bridge. Rather than converting
# the Linux bridge (destructive, causes WAN interruption), we create
# a separate OVS bridge and link them with a veth pair:
#
# br-wan (Linux, keeps IP/routes) ←→ veth pair ←→ br-ext (OVS, for OVN)
#
# No network interruption. The Linux bridge is untouched.
# Create OVS bridge
if ! sudo ovs-vsctl br-exists "$WAN_BRIDGE" 2>/dev/null; then
sudo ovs-vsctl --may-exist add-br "$WAN_BRIDGE"
echo " created OVS bridge: $WAN_BRIDGE"
fi
sudo ip link set "$WAN_BRIDGE" up
# Create veth pair (idempotent)
if ! ip link show veth-wan-br >/dev/null 2>&1; then
sudo ip link add veth-wan-br type veth peer name veth-wan-ovs
echo " created veth pair: veth-wan-br ↔ veth-wan-ovs"
else
echo " veth pair already exists: veth-wan-br ↔ veth-wan-ovs"
fi
# Enslave veth-wan-br to the Linux bridge
if ! ip link show veth-wan-br 2>/dev/null | grep -q "master $LINUX_BRIDGE"; then
sudo ip link set veth-wan-br master "$LINUX_BRIDGE"
echo " veth-wan-br → $LINUX_BRIDGE (Linux bridge)"
fi
sudo ip link set veth-wan-br up
# Add veth-wan-ovs to the OVS bridge
if ! sudo ovs-vsctl port-to-br veth-wan-ovs >/dev/null 2>&1; then
sudo ovs-vsctl --may-exist add-port "$WAN_BRIDGE" veth-wan-ovs
echo " veth-wan-ovs → $WAN_BRIDGE (OVS bridge)"
fi
sudo ip link set veth-wan-ovs up
echo " $LINUX_BRIDGE (Linux) ↔ veth pair ↔ $WAN_BRIDGE (OVS)"
echo " $LINUX_BRIDGE keeps its IP and routes — no interruption"
;;
direct)
@@ -587,7 +642,15 @@ fi
if [ -n "$WAN_BRIDGE" ]; then
if sudo ovs-vsctl br-exists "$WAN_BRIDGE"; then
echo " $WAN_BRIDGE:$(printf '%*s' $((15 - ${#WAN_BRIDGE})) '') OK"
if [ "$WAN_BRIDGE_MODE" = "direct" ]; then
if [ "$WAN_BRIDGE_MODE" = "veth" ]; then
if ip link show veth-wan-br >/dev/null 2>&1 && ip link show veth-wan-ovs >/dev/null 2>&1; then
echo " veth pair: OK (veth-wan-br ↔ veth-wan-ovs)"
echo " linux bridge: $LINUX_BRIDGE (untouched)"
else
echo " veth pair: FAILED (veth-wan-br/veth-wan-ovs not found)"
OK=false
fi
elif [ "$WAN_BRIDGE_MODE" = "direct" ]; then
if sudo ovs-vsctl port-to-br "$WAN_IFACE" >/dev/null 2>&1; then
echo " direct bridge: OK ($WAN_IFACE on $WAN_BRIDGE)"
else
+16 -5
View File
@@ -150,9 +150,10 @@ has_service "predastore" && stop_service "predastore" "$PID_DIR"
has_service "nats" && stop_service "nats" "$PID_DIR"
# Stop OVN networking (prevents idle CPU burn when spinifex isn't running)
# CRITICAL: Do NOT stop openvswitch-switch if br-external has an IP.
# When setup-ovn.sh migrates the host's WAN IP to br-external, stopping OVS
# destroys the bridge and the host loses all WAN connectivity (unreachable via SSH).
# CRITICAL: Do NOT stop openvswitch-switch if OVS carries WAN traffic.
# This can happen when an OVS bridge has a WAN IP (direct bridge mode) or
# when a veth pair links an OVS bridge to a Linux WAN bridge (veth mode).
# Stopping OVS would destroy the bridge and break WAN connectivity.
# OVS is infrastructure — only stop ovn-controller and ovn-central.
if pidof systemd >/dev/null 2>&1; then
echo "🛑 Stopping OVN networking..."
@@ -161,9 +162,13 @@ if pidof systemd >/dev/null 2>&1; then
sudo systemctl stop ovn-central 2>/dev/null && echo "✅ ovn-central stopped" || true
fi
# Only stop OVS if the WAN bridge does NOT have an IP (safe to tear down).
# Check all OVS bridges — any bridge with a WAN IP means OVS is infrastructure.
# Only stop OVS if tearing it down won't kill WAN connectivity.
# Two cases where OVS is load-bearing for WAN:
# 1. An OVS bridge has a WAN IP directly (direct bridge mode)
# 2. A veth pair links a Linux bridge to an OVS bridge (veth mode) —
# stopping OVS destroys the OVS bridge end, breaking the link
WAN_BRIDGE_HAS_IP=false
VETH_LINKS_WAN=false
for br in $(sudo ovs-vsctl list-br 2>/dev/null); do
if [ "$br" = "br-int" ]; then continue; fi
if ip -4 addr show "$br" 2>/dev/null | grep -q "inet "; then
@@ -171,8 +176,14 @@ if pidof systemd >/dev/null 2>&1; then
break
fi
done
# Check if a veth pair links an OVS bridge to a Linux bridge with a WAN IP
if ip link show veth-wan-br >/dev/null 2>&1; then
VETH_LINKS_WAN=true
fi
if [ "$WAN_BRIDGE_HAS_IP" = true ]; then
echo "⚠️ Skipping openvswitch-switch stop ($br has WAN IP — stopping would kill connectivity)"
elif [ "$VETH_LINKS_WAN" = true ]; then
echo "⚠️ Skipping openvswitch-switch stop (veth pair links OVS to WAN bridge)"
else
sudo systemctl stop openvswitch-switch 2>/dev/null && echo "✅ openvswitch-switch stopped" || true
fi
+11 -11
View File
@@ -65,17 +65,17 @@ type ConfigSettings struct {
OVNSBAddr string
// External networking for public subnets
ExternalMode string // "pool", "nat", or "" (disabled)
ExternalIface string // WAN NIC name (e.g., "eth0", "eth1")
WanBridge string // OVS bridge for WAN traffic (default "br-wan")
ExternalDHCP bool // Obtain gateway IP via DHCP on macvlan/bridge
PoolName string // External pool name (e.g., "wan")
PoolStart string // First IP in external pool range
PoolEnd string // Last IP in external pool range
PoolGateway string // WAN gateway IP
PoolGatewayIP string // Explicit SNAT IP (for nat mode without DHCP)
PoolPrefixLen int // Subnet prefix length (default 24)
PoolDNSServers []string // DNS servers for VM DHCP (auto-detected from host)
ExternalMode string // "pool", "nat", or "" (disabled)
ExternalIface string // WAN NIC name (e.g., "eth0", "eth1")
WanBridge string // OVS bridge for WAN traffic (default "br-wan")
ExternalDHCP bool // Obtain gateway IP via DHCP on macvlan/bridge
PoolName string // External pool name (e.g., "wan")
PoolStart string // First IP in external pool range
PoolEnd string // Last IP in external pool range
PoolGateway string // WAN gateway IP
PoolGatewayIP string // Explicit SNAT IP (for nat mode without DHCP)
PoolPrefixLen int // Subnet prefix length (default 24)
PoolDNSServers []string // DNS servers for VM DHCP (auto-detected from host)
// Other nodes in the cluster (for config source of truth)
RemoteNodes []RemoteNode
+3 -3
View File
@@ -62,7 +62,7 @@ func DetectNetwork() (*DetectedNetwork, error) {
seen := make(map[string]bool)
var interfaces []DetectedInterface
for _, line := range strings.Split(strings.TrimSpace(string(allRoutes)), "\n") {
for line := range strings.SplitSeq(strings.TrimSpace(string(allRoutes)), "\n") {
if line == "" {
continue
}
@@ -169,10 +169,10 @@ func SuggestPoolRange(wan *DetectedInterface) (start, end string) {
for i := 3; i >= 0 && carry > 0; i-- {
val := int(startIP[i]) - carry
if val < 0 {
startIP[i] = byte(256 + val)
startIP[i] = byte(256 + val) //nolint:gosec // val is in [-256,0), so 256+val is in [0,256)
carry = 1
} else {
startIP[i] = byte(val)
startIP[i] = byte(val) //nolint:gosec // val is non-negative and <= 255
carry = 0
}
}
+1 -1
View File
@@ -90,7 +90,7 @@ type VPCDConfig struct {
OVNNBAddr string `mapstructure:"ovn_nb_addr"` // OVN Northbound DB address (e.g., "tcp:127.0.0.1:6641")
OVNSBAddr string `mapstructure:"ovn_sb_addr"` // OVN Southbound DB address (e.g., "tcp:127.0.0.1:6642")
ExternalInterface string `mapstructure:"external_interface"` // WAN NIC name (e.g., "eth1", "enp0s3") — the physical NIC on the WAN bridge
WanBridge string `mapstructure:"wan_bridge"` // OVS bridge for WAN traffic (default "br-wan", maps to OVN "external" network)
WanBridge string `mapstructure:"wan_bridge"` // OVS bridge for WAN traffic (default "br-wan", maps to OVN "external" network)
BridgeMode string `mapstructure:"bridge_mode"` // "direct" or "macvlan" (auto-detected if empty)
}
+1 -1
View File
@@ -32,8 +32,8 @@ import (
"github.com/mulgadc/spinifex/spinifex/awserrors"
"github.com/mulgadc/spinifex/spinifex/config"
handlers_ec2_account "github.com/mulgadc/spinifex/spinifex/handlers/ec2/account"
handlers_ec2_eip "github.com/mulgadc/spinifex/spinifex/handlers/ec2/eip"
handlers_ec2_eigw "github.com/mulgadc/spinifex/spinifex/handlers/ec2/eigw"
handlers_ec2_eip "github.com/mulgadc/spinifex/spinifex/handlers/ec2/eip"
handlers_ec2_igw "github.com/mulgadc/spinifex/spinifex/handlers/ec2/igw"
handlers_ec2_image "github.com/mulgadc/spinifex/spinifex/handlers/ec2/image"
handlers_ec2_instance "github.com/mulgadc/spinifex/spinifex/handlers/ec2/instance"
-1
View File
@@ -188,4 +188,3 @@ func (d *Daemon) ensureDefaultVPCInfrastructure() {
}
}
}
+1 -1
View File
@@ -14,6 +14,7 @@ import (
"github.com/mulgadc/spinifex/spinifex/awserrors"
gateway_ec2_account "github.com/mulgadc/spinifex/spinifex/gateway/ec2/account"
gateway_ec2_eigw "github.com/mulgadc/spinifex/spinifex/gateway/ec2/eigw"
gateway_ec2_eip "github.com/mulgadc/spinifex/spinifex/gateway/ec2/eip"
gateway_ec2_igw "github.com/mulgadc/spinifex/spinifex/gateway/ec2/igw"
gateway_ec2_image "github.com/mulgadc/spinifex/spinifex/gateway/ec2/image"
gateway_ec2_instance "github.com/mulgadc/spinifex/spinifex/gateway/ec2/instance"
@@ -22,7 +23,6 @@ import (
gateway_ec2_snapshot "github.com/mulgadc/spinifex/spinifex/gateway/ec2/snapshot"
gateway_ec2_tags "github.com/mulgadc/spinifex/spinifex/gateway/ec2/tags"
gateway_ec2_volume "github.com/mulgadc/spinifex/spinifex/gateway/ec2/volume"
gateway_ec2_eip "github.com/mulgadc/spinifex/spinifex/gateway/ec2/eip"
gateway_ec2_vpc "github.com/mulgadc/spinifex/spinifex/gateway/ec2/vpc"
gateway_ec2_zone "github.com/mulgadc/spinifex/spinifex/gateway/ec2/zone"
"github.com/mulgadc/spinifex/spinifex/utils"
@@ -87,7 +87,6 @@ func (s *IGWServiceImpl) CreateInternetGateway(input *ec2.CreateInternetGatewayI
}
func (s *IGWServiceImpl) createIGW(input *ec2.CreateInternetGatewayInput, accountID, igwID string) (*ec2.CreateInternetGatewayOutput, error) {
record := IGWRecord{
InternetGatewayId: igwID,
State: "available",
+1 -1
View File
@@ -21,7 +21,7 @@ const (
type ExternalIPAllocation struct {
Type string `json:"type"` // "gateway", "auto_assign", "elastic_ip"
AllocationID string `json:"allocation_id,omitempty"` // For elastic IPs
Association string `json:"association,omitempty"` // ENI ID for elastic IPs
Association string `json:"association,omitempty"` // ENI ID for elastic IPs
ENIId string `json:"eni_id,omitempty"` // ENI owning this IP
InstanceId string `json:"instance_id,omitempty"` // Instance owning this IP
Note string `json:"note,omitempty"` // Human-readable note
+6 -6
View File
@@ -450,12 +450,12 @@ func (s *VPCServiceImpl) RevokeSecurityGroupEgress(input *ec2.RevokeSecurityGrou
// sgRecordToEC2 converts a SecurityGroupRecord to an EC2 SecurityGroup.
func (s *VPCServiceImpl) sgRecordToEC2(record *SecurityGroupRecord, accountID string) *ec2.SecurityGroup {
sg := &ec2.SecurityGroup{
GroupId: aws.String(record.GroupId),
GroupName: aws.String(record.GroupName),
Description: aws.String(record.Description),
VpcId: aws.String(record.VpcId),
OwnerId: aws.String(accountID),
IpPermissions: sgRulesToIpPermissions(record.IngressRules),
GroupId: aws.String(record.GroupId),
GroupName: aws.String(record.GroupName),
Description: aws.String(record.Description),
VpcId: aws.String(record.VpcId),
OwnerId: aws.String(accountID),
IpPermissions: sgRulesToIpPermissions(record.IngressRules),
IpPermissionsEgress: sgRulesToIpPermissions(record.EgressRules),
}
+4 -2
View File
@@ -731,11 +731,13 @@ func (s *IAMServiceImpl) seedAdminAccount(admin *AdminBootstrapData) error {
// this account. Without this, the admin account has no default VPC/subnet
// and the user must create one manually.
if s.natsConn != nil {
evt, _ := json.Marshal(struct {
evt, err := json.Marshal(struct {
AccountID string `json:"account_id"`
AccountName string `json:"account_name"`
}{AccountID: admin.AccountID, AccountName: admin.AccountName})
if err := s.natsConn.Publish("iam.account.created", evt); err != nil {
if err != nil {
slog.Warn("Failed to marshal account creation event", "accountID", admin.AccountID, "error", err)
} else if err := s.natsConn.Publish("iam.account.created", evt); err != nil {
slog.Warn("Failed to publish account creation event for admin account", "accountID", admin.AccountID, "error", err)
}
}
+2 -2
View File
@@ -96,8 +96,8 @@ type LogicalRouterStaticRoute struct {
type PortGroup struct {
UUID string `ovsdb:"_uuid"`
Name string `ovsdb:"name"`
Ports []string `ovsdb:"ports"` // UUIDs of logical switch ports
ACLs []string `ovsdb:"acls"` // UUIDs of ACLs
Ports []string `ovsdb:"ports"` // UUIDs of logical switch ports
ACLs []string `ovsdb:"acls"` // UUIDs of ACLs
ExternalIDs map[string]string `ovsdb:"external_ids"`
}
+1 -1
View File
@@ -39,7 +39,7 @@ func TestReconcile_CreatesBootstrapTopology(t *testing.T) {
_ = ovn.Connect(context.Background())
topo := NewTopologyHandler(ovn,
WithExternalNetwork("pool", []ExternalPoolConfig{{
Name: "wan",
Name: "wan",
RangeStart: "192.168.1.200",
RangeEnd: "192.168.1.250",
Gateway: "192.168.1.1",
+10 -16
View File
@@ -655,7 +655,6 @@ func (h *TopologyHandler) handleIGWAttach(msg *nats.Msg) {
gatewayIP := "169.254.0.1"
gatewayNetwork := "169.254.0.1/30"
wanGateway := "169.254.0.2"
prefixLen := 30
if pool != nil {
gip := pool.GatewayIP
@@ -663,7 +662,7 @@ func (h *TopologyHandler) handleIGWAttach(msg *nats.Msg) {
gip = pool.RangeStart // Default: first IP in range
}
gatewayIP = gip
prefixLen = pool.PrefixLen
prefixLen := pool.PrefixLen
if prefixLen == 0 {
prefixLen = 24
}
@@ -759,10 +758,9 @@ func (h *TopologyHandler) handleIGWAttach(msg *nats.Msg) {
// 7. Schedule gateway chassis for HA — tells OVN which hosts can handle external traffic
if len(h.chassisNames) > 0 {
for i, chassis := range h.chassisNames {
priority := 20 - (i * 5) // First chassis gets highest priority
if priority < 1 {
priority = 1
}
priority := max(
// First chassis gets highest priority
20-(i*5), 1)
if err := h.ovn.SetGatewayChassis(ctx, gwPortName, chassis, priority); err != nil {
slog.Warn("vpcd: failed to set gateway chassis", "port", gwPortName, "chassis", chassis, "priority", priority, "err", err)
} else {
@@ -1060,7 +1058,6 @@ func (h *TopologyHandler) reconcileIGW(ctx context.Context, vpcId, igwId string)
gatewayIP := "169.254.0.1"
gatewayNetwork := "169.254.0.1/30"
wanGateway := "169.254.0.2"
prefixLen := 30
if pool != nil {
gip := pool.GatewayIP
@@ -1068,7 +1065,7 @@ func (h *TopologyHandler) reconcileIGW(ctx context.Context, vpcId, igwId string)
gip = pool.RangeStart
}
gatewayIP = gip
prefixLen = pool.PrefixLen
prefixLen := pool.PrefixLen
if prefixLen == 0 {
prefixLen = 24
}
@@ -1106,10 +1103,10 @@ func (h *TopologyHandler) reconcileIGW(ctx context.Context, vpcId, igwId string)
reconcileLocalnetOpts["nat-addresses"] = "router"
}
localnetPort := &nbdb.LogicalSwitchPort{
Name: extPortName,
Type: "localnet",
Addresses: []string{"unknown"},
Options: reconcileLocalnetOpts,
Name: extPortName,
Type: "localnet",
Addresses: []string{"unknown"},
Options: reconcileLocalnetOpts,
ExternalIDs: portExtIDs,
}
if err := h.ovn.CreateLogicalSwitchPort(ctx, extSwitchName, localnetPort); err != nil {
@@ -1183,10 +1180,7 @@ func (h *TopologyHandler) reconcileIGW(ctx context.Context, vpcId, igwId string)
// 7. Schedule gateway chassis
for i, chassis := range h.chassisNames {
priority := 20 - (i * 5)
if priority < 1 {
priority = 1
}
priority := max(20-(i*5), 1)
if err := h.ovn.SetGatewayChassis(ctx, gwPortName, chassis, priority); err != nil {
slog.Warn("vpcd reconcile: failed to set gateway chassis", "chassis", chassis, "err", err)
}
+3 -1
View File
@@ -64,8 +64,10 @@ type Config struct {
// ExternalInterface is the WAN NIC name (e.g., "enp0s3"). Used to align
// the macvlan MAC with the OVN gateway MAC for inbound traffic.
ExternalInterface string
// WanBridge is the OVS bridge name for WAN traffic (default "br-wan").
// WanBridge is the OVS bridge name for WAN traffic.
// Maps to OVN logical network "external" via ovn-bridge-mappings.
// Typically "br-ext" (veth mode linking Linux bridge to OVS) or the
// bridge name itself when the default route is already on an OVS bridge.
WanBridge string
// BridgeMode is "direct" or "macvlan". Direct bridge adds the WAN NIC
// directly to the WAN bridge; macvlan creates a sub-interface. When empty,