From f558fe6efe1f2ccc254940b6b63849d39b53c89d Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 3 Nov 2026 18:01:01 +0000 Subject: [PATCH] feat: fail closed on nodes without host-only ports Render galactic-tap-host-only as the tap plugin type whenever an attachment carries host-only ports. Only a Galactic that enforces the list installs that plugin, so an older node fails ADD instead of starting the guest with the exec agent port open. Key changes: - Name galactic-tap-host-only in conflists that list host-only ports - Keep galactic-tap for attachments without them - Document that a node rolled back past the plugin fails DEL until it returns --- internal/config/cell.go | 12 ++++++++---- internal/galactic/galactic/galactic.go | 8 ++++++++ internal/galactic/galactic/galactic_test.go | 7 +++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/internal/config/cell.go b/internal/config/cell.go index a4c86246..2b2a75a8 100644 --- a/internal/config/cell.go +++ b/internal/config/cell.go @@ -114,10 +114,14 @@ type GalacticCellConfig struct { // exec agent. Galactic drops traffic to them unless it is addressed to the // guest's link-local address. // - // Set it only once every node in the cell runs a Galactic that supports - // host-only ports. An older tap plugin ignores the list and leaves the - // ports open, and a newer one whose datapath is not loaded refuses the - // attachment. + // Set it only once every node in the cell runs a Galactic that installs + // galactic-tap-host-only. The conflist names that plugin, so a node + // without it fails the attachment rather than leaving the ports open, as + // does a node whose datapath is not loaded. + // + // Rolling a node back to a Galactic without that plugin also fails DEL + // for sandboxes whose conflist names it, so their teardown retries until + // the plugin is installed again. HypervisorHostOnlyPorts []uint16 `json:"hypervisorHostOnlyPorts,omitempty"` } diff --git a/internal/galactic/galactic/galactic.go b/internal/galactic/galactic/galactic.go index b08c80a2..365da3fa 100644 --- a/internal/galactic/galactic/galactic.go +++ b/internal/galactic/galactic/galactic.go @@ -40,6 +40,11 @@ const ( // PluginTap is the master plugin for virtual machine guests. PluginTap = "galactic-tap" + // PluginTapHostOnly is the tap plugin under the name only a node that + // enforces hostOnlyPorts installs, so an older node fails ADD instead of + // starting the guest with those ports open. + PluginTapHostOnly = "galactic-tap-host-only" + // PluginBGP publishes the attachment into BGP and SRv6. PluginBGP = "galactic-bgp" @@ -138,6 +143,9 @@ func Conflist( DAN: dan, HostOnlyPorts: hostOnlyPorts, } + if plugin == PluginTap && len(hostOnlyPorts) > 0 { + master.Type = PluginTapHostOnly + } if len(addresses) > 0 { master.IPAM = &IPAM{Type: PluginIPAM, Addresses: addresses} } diff --git a/internal/galactic/galactic/galactic_test.go b/internal/galactic/galactic/galactic_test.go index 54e161ab..8c2fcb6e 100644 --- a/internal/galactic/galactic/galactic_test.go +++ b/internal/galactic/galactic/galactic_test.go @@ -179,6 +179,13 @@ func TestConflistCarriesHostOnlyPorts(t *testing.T) { if _, present := masterStanza(t, open)["hostOnlyPorts"]; present { t.Errorf("attachment without host-only ports carries the field: %s", open) } + if got := masterStanza(t, fenced)["type"]; got != PluginTapHostOnly { + t.Errorf("fenced attachment: plugin %v, want %q so a node that cannot enforce the ports fails ADD", + got, PluginTapHostOnly) + } + if got := masterStanza(t, open)["type"]; got != PluginTap { + t.Errorf("attachment without host-only ports: plugin %v, want %q", got, PluginTap) + } } func masterStanza(t *testing.T, raw string) map[string]any {