From 5fe02a8d50ba44b3b2992c366683f33457be112f Mon Sep 17 00:00:00 2001 From: Weilu Jia Date: Wed, 24 Jun 2026 20:01:45 -0700 Subject: [PATCH] feat(orchestrator/network): list slot namespaces Export the network-namespace directory and naming helpers (NetNamespacesDir, NamespaceName, SlotIndexFromNamespace) and add ListSlotNamespaces to enumerate ns- namespaces left by leaked sandboxes. --- .../orchestrator/pkg/sandbox/network/slot.go | 8 +-- .../pkg/sandbox/network/storage_local.go | 55 ++++++++++++++++++- .../pkg/sandbox/network/storage_local_test.go | 38 +++++++++++++ 3 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 packages/orchestrator/pkg/sandbox/network/storage_local_test.go diff --git a/packages/orchestrator/pkg/sandbox/network/slot.go b/packages/orchestrator/pkg/sandbox/network/slot.go index 1228b9a5e5..35c90ea165 100644 --- a/packages/orchestrator/pkg/sandbox/network/slot.go +++ b/packages/orchestrator/pkg/sandbox/network/slot.go @@ -267,7 +267,7 @@ func (s *Slot) ConfigureInternet(ctx context.Context, network *orchestrator.Sand s.firewallCustomRules.Store(true) - n, err := ns.GetNS(filepath.Join(netNamespacesDir, s.NamespaceID())) + n, err := ns.GetNS(filepath.Join(NetNamespacesDir, s.NamespaceID())) if err != nil { return fmt.Errorf("failed to get slot network namespace '%s': %w", s.NamespaceID(), err) } @@ -294,7 +294,7 @@ func (s *Slot) UpdateInternet(ctx context.Context, egress *orchestrator.SandboxN deniedCIDRs := egress.GetDeniedCidrs() hasBYOP := egress.GetEgressProxyAddress() != "" - n, err := ns.GetNS(filepath.Join(netNamespacesDir, s.NamespaceID())) + n, err := ns.GetNS(filepath.Join(NetNamespacesDir, s.NamespaceID())) if err != nil { return fmt.Errorf("failed to get slot network namespace '%s': %w", s.NamespaceID(), err) } @@ -329,7 +329,7 @@ func (s *Slot) DenyEgress(ctx context.Context) error { return fmt.Errorf("firewall is not initialized for slot '%s'", s.NamespaceID()) } - n, err := ns.GetNS(filepath.Join(netNamespacesDir, s.NamespaceID())) + n, err := ns.GetNS(filepath.Join(NetNamespacesDir, s.NamespaceID())) if err != nil { return fmt.Errorf("failed to get slot network namespace '%s': %w", s.NamespaceID(), err) } @@ -358,7 +358,7 @@ func (s *Slot) ResetInternet(ctx context.Context) error { return nil } - n, err := ns.GetNS(filepath.Join(netNamespacesDir, s.NamespaceID())) + n, err := ns.GetNS(filepath.Join(NetNamespacesDir, s.NamespaceID())) if err != nil { return fmt.Errorf("failed to get slot network namespace '%s': %w", s.NamespaceID(), err) } diff --git a/packages/orchestrator/pkg/sandbox/network/storage_local.go b/packages/orchestrator/pkg/sandbox/network/storage_local.go index 675e782de1..e28d78d3d3 100644 --- a/packages/orchestrator/pkg/sandbox/network/storage_local.go +++ b/packages/orchestrator/pkg/sandbox/network/storage_local.go @@ -8,7 +8,9 @@ import ( "fmt" "os" "path/filepath" + "slices" "strconv" + "strings" "sync" "time" @@ -26,7 +28,7 @@ type StorageLocal struct { egressProxy EgressProxy } -const netNamespacesDir = "/var/run/netns" +const NetNamespacesDir = "/var/run/netns" func NewStorageLocal(ctx context.Context, config Config, egressProxy EgressProxy) (*StorageLocal, error) { // get namespaces that we want to always skip @@ -118,7 +120,7 @@ func (s *StorageLocal) Release(ips *Slot) error { } func isNamespaceAvailable(name string) (bool, error) { - nsPath := filepath.Join(netNamespacesDir, name) + nsPath := filepath.Join(NetNamespacesDir, name) _, err := os.Stat(nsPath) if os.IsNotExist(err) { @@ -136,7 +138,7 @@ func isNamespaceAvailable(name string) (bool, error) { func getForeignNamespaces() ([]string, error) { var ns []string - files, err := os.ReadDir(netNamespacesDir) + files, err := os.ReadDir(NetNamespacesDir) if err != nil { // Folder does not exist, so we can assume no namespaces are in use if os.IsNotExist(err) { @@ -168,6 +170,53 @@ func getSlotName(slotIdx int) string { return fmt.Sprintf("ns-%s", slotIdxStr) } +func NamespaceName(slotIdx int) string { + return getSlotName(slotIdx) +} + +func SlotIndexFromNamespace(name string) (int, bool) { + idxStr, ok := strings.CutPrefix(name, "ns-") + if !ok || idxStr == "" { + return 0, false + } + + idx, err := strconv.Atoi(idxStr) + if err != nil || idx < 1 || idx > vrtSlotsSize { + return 0, false + } + + return idx, true +} + +func ListSlotNamespaces(dir string) ([]int, error) { + files, err := os.ReadDir(dir) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + + return nil, fmt.Errorf("error reading netns directory: %w", err) + } + + indices := make([]int, 0, len(files)) + for _, file := range files { + if file.IsDir() { + continue + } + + idx, ok := SlotIndexFromNamespace(file.Name()) + if !ok { + continue + } + + indices = append(indices, idx) + } + + slices.Sort(indices) + + return indices, nil +} + func getLocalKey(slotIdx int) string { return strconv.Itoa(slotIdx) } diff --git a/packages/orchestrator/pkg/sandbox/network/storage_local_test.go b/packages/orchestrator/pkg/sandbox/network/storage_local_test.go new file mode 100644 index 0000000000..5b4a8297d1 --- /dev/null +++ b/packages/orchestrator/pkg/sandbox/network/storage_local_test.go @@ -0,0 +1,38 @@ +//go:build linux + +package network + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSlotIndexFromNamespace(t *testing.T) { + t.Parallel() + + idx, ok := SlotIndexFromNamespace("ns-2") + require.True(t, ok) + require.Equal(t, 2, idx) + + for _, name := range []string{"host", "ns-0", "ns-nope", "other-2"} { + _, ok := SlotIndexFromNamespace(name) + require.False(t, ok) + } +} + +func TestListSlotNamespaces(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + for _, name := range []string{"ns-10", "ns-2", "host", "ns-bad"} { + require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte("x"), 0o600)) + } + require.NoError(t, os.Mkdir(filepath.Join(dir, "ns-3"), 0o700)) + + indices, err := ListSlotNamespaces(dir) + require.NoError(t, err) + require.Equal(t, []int{2, 10}, indices) +}