Skip to content

Commit dea41b6

Browse files
committed
fix: protect cleanupContainerNetns from deleting non-veth interfaces
The cleanupContainerNetns function in the galactic-cni plugin deleted any interface matching the expected name without verifying it was the veth endpoint created by this plugin. If another tool or race condition created an interface with the same name, it could be accidentally removed, breaking network connectivity for an unrelated workload. - Add a type assertion to verify the link is a *netlink.Veth before deletion - Return a descriptive error when a non-veth interface is found at the expected name - Document the veth-only deletion behavior in the function comment - Add integration tests covering non-veth rejection, idempotent missing-interface handling, and successful veth cleanup fixes #159
1 parent 46bb2cf commit dea41b6

2 files changed

Lines changed: 202 additions & 1 deletion

File tree

internal/cni/netns.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,12 @@ func readGuestInterface(netnsPath, ifName string) (string, int, error) {
8787
return mac, mtu, err
8888
}
8989

90-
// cleanupContainerNetns removes any existing interface with the given name
90+
// cleanupContainerNetns removes any existing veth interface with the given name
9191
// from the container network namespace. This is needed to handle stale state
9292
// from previous CNI ADD runs that may have left interfaces behind.
93+
//
94+
// Only *netlink.Veth interfaces are deleted; other types produce a clear error
95+
// to prevent accidental deletion of unrelated interfaces.
9396
func cleanupContainerNetns(netnsPath, ifName string) error {
9497
containerNS, err := ns.GetNS(netnsPath)
9598
if err != nil {
@@ -109,6 +112,9 @@ func cleanupContainerNetns(netnsPath, ifName string) error {
109112
// Interface does not exist in container netns — nothing to clean up.
110113
return nil
111114
}
115+
if _, ok := link.(*netlink.Veth); !ok {
116+
return fmt.Errorf("interface %q is not a veth (type: %T), refusing to delete", ifName, link)
117+
}
112118
if err := handle.LinkDel(link); err != nil {
113119
return fmt.Errorf("delete stale interface %q in container netns: %w", ifName, err)
114120
}

internal/cni/netns_test.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// Copyright 2025 Datum Cloud, Inc.
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
package cni
6+
7+
import (
8+
"fmt"
9+
"os"
10+
"strings"
11+
"testing"
12+
13+
"github.com/containernetworking/plugins/pkg/ns"
14+
"github.com/vishvananda/netlink"
15+
)
16+
17+
// requireRoot skips the test when not running as root.
18+
// Network namespace operations (unshare, veth creation) require CAP_SYS_ADMIN.
19+
func requireRoot(t *testing.T) {
20+
t.Helper()
21+
if os.Getuid() != 0 {
22+
t.Skip("skipping: requires root (CAP_SYS_ADMIN)")
23+
}
24+
}
25+
26+
// createTestNetnsWithDummy creates a temporary network namespace, creates a
27+
// dummy interface inside it, and returns the netns path and a cleanup function.
28+
func createTestNetnsWithDummy(t *testing.T) (netnsPath string, cleanup func()) {
29+
t.Helper()
30+
31+
requireRoot(t)
32+
33+
nsObj, err := ns.TempNetNS()
34+
if err != nil {
35+
t.Fatalf("create new netns: %v", err)
36+
}
37+
38+
err = nsObj.Do(func(_ ns.NetNS) error {
39+
handle, err := netlink.NewHandle()
40+
if err != nil {
41+
return err
42+
}
43+
defer handle.Close() //nolint:errcheck // best-effort cleanup
44+
45+
dummy := &netlink.Dummy{
46+
LinkAttrs: netlink.LinkAttrs{Name: "test-dummy"},
47+
}
48+
if err := handle.LinkAdd(dummy); err != nil {
49+
return fmt.Errorf("add dummy link: %w", err)
50+
}
51+
if err := handle.LinkSetUp(dummy); err != nil {
52+
return fmt.Errorf("set dummy link up: %w", err)
53+
}
54+
return nil
55+
})
56+
if err != nil {
57+
nsObj.Close() //nolint:errcheck // best-effort cleanup
58+
t.Fatalf("setup dummy interface: %v", err)
59+
}
60+
61+
cleanup = func() {
62+
_ = nsObj.Do(func(_ ns.NetNS) error {
63+
handle, err := netlink.NewHandle()
64+
if err != nil {
65+
return err
66+
}
67+
defer handle.Close() //nolint:errcheck // best-effort cleanup
68+
link, err := handle.LinkByName("test-dummy")
69+
if err != nil {
70+
return nil // already gone
71+
}
72+
return handle.LinkDel(link)
73+
})
74+
nsObj.Close() //nolint:errcheck // best-effort cleanup
75+
}
76+
77+
return nsObj.Path(), cleanup
78+
}
79+
80+
func TestCleanupContainerNetnsNonVeth(t *testing.T) {
81+
requireRoot(t)
82+
83+
netnsPath, cleanup := createTestNetnsWithDummy(t)
84+
defer cleanup()
85+
86+
err := cleanupContainerNetns(netnsPath, "test-dummy")
87+
if err == nil {
88+
t.Fatal("expected error for non-veth interface, got nil")
89+
}
90+
if !strings.Contains(err.Error(), "is not a veth") {
91+
t.Fatalf("error %q does not contain 'is not a veth'", err.Error())
92+
}
93+
if !strings.Contains(err.Error(), "test-dummy") {
94+
t.Fatalf("error %q does not contain interface name 'test-dummy'", err.Error())
95+
}
96+
}
97+
98+
func TestCleanupContainerNetnsNonExistent(t *testing.T) {
99+
requireRoot(t)
100+
101+
netnsPath, cleanup := createTestNetnsWithDummy(t)
102+
defer cleanup()
103+
104+
// Cleanup a non-existent interface should return nil (idempotent).
105+
err := cleanupContainerNetns(netnsPath, "does-not-exist")
106+
if err != nil {
107+
t.Fatalf("expected nil for non-existent interface, got: %v", err)
108+
}
109+
}
110+
111+
func TestCleanupContainerNetnsVeth(t *testing.T) {
112+
requireRoot(t)
113+
114+
// Create a temporary network namespace with a veth pair.
115+
nsObj, err := ns.TempNetNS()
116+
if err != nil {
117+
t.Fatalf("create new netns: %v", err)
118+
}
119+
defer nsObj.Close() //nolint:errcheck // best-effort cleanup
120+
121+
// Create a veth pair: one end in the new namespace ("test-veth"),
122+
// the other end in the host namespace ("test-veth-peer").
123+
hostVethName := "test-veth-host"
124+
guestVethName := "test-veth"
125+
126+
err = nsObj.Do(func(_ ns.NetNS) error {
127+
handle, err := netlink.NewHandle()
128+
if err != nil {
129+
return err
130+
}
131+
defer handle.Close() //nolint:errcheck // best-effort cleanup
132+
133+
veth := &netlink.Veth{
134+
LinkAttrs: netlink.LinkAttrs{Name: guestVethName},
135+
PeerName: hostVethName,
136+
}
137+
if err := handle.LinkAdd(veth); err != nil {
138+
return fmt.Errorf("add veth link: %w", err)
139+
}
140+
if err := handle.LinkSetUp(veth); err != nil {
141+
return fmt.Errorf("set veth up: %w", err)
142+
}
143+
return nil
144+
})
145+
if err != nil {
146+
t.Fatalf("setup veth pair: %v", err)
147+
}
148+
149+
// Verify the peer exists in host namespace.
150+
hostHandle, err := netlink.NewHandle()
151+
if err != nil {
152+
t.Fatalf("create host netlink handle: %v", err)
153+
}
154+
defer hostHandle.Close() //nolint:errcheck // best-effort cleanup
155+
156+
_, err = hostHandle.LinkByName(hostVethName)
157+
if err != nil {
158+
t.Fatalf("veth peer %q not found in host ns: %v", hostVethName, err)
159+
}
160+
161+
// Now call cleanupContainerNetns — it should succeed and delete the veth.
162+
err = cleanupContainerNetns(nsObj.Path(), guestVethName)
163+
if err != nil {
164+
t.Fatalf("cleanupContainerNetns(veth) returned error: %v", err)
165+
}
166+
167+
// Verify the veth is gone from the container namespace.
168+
err = nsObj.Do(func(_ ns.NetNS) error {
169+
handle, err := netlink.NewHandle()
170+
if err != nil {
171+
return err
172+
}
173+
defer handle.Close() //nolint:errcheck // best-effort cleanup
174+
_, err = handle.LinkByName(guestVethName)
175+
if err == nil {
176+
return fmt.Errorf("interface %q still exists after cleanup", guestVethName)
177+
}
178+
return nil
179+
})
180+
if err != nil {
181+
t.Fatal(err)
182+
}
183+
184+
// Verify the host-side peer still exists (only guest veth was deleted).
185+
_, err = hostHandle.LinkByName(hostVethName)
186+
if err != nil {
187+
t.Fatalf("veth peer %q was unexpectedly deleted from host ns: %v", hostVethName, err)
188+
}
189+
190+
// Clean up the host-side peer.
191+
peer, _ := hostHandle.LinkByName(hostVethName)
192+
if peer != nil {
193+
_ = hostHandle.LinkDel(peer) //nolint:errcheck // best-effort cleanup
194+
}
195+
}

0 commit comments

Comments
 (0)