Skip to content

Commit 9dd153d

Browse files
authored
Merge pull request #180 from datum-cloud/fix/issue-159
fix: protect cleanupContainerNetnsrom deleting non-veth interfaces
2 parents f59ed22 + dea41b6 commit 9dd153d

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)