mirror of
https://github.com/moby/moby.git
synced 2026-08-03 14:41:03 +00:00
Namespace.RemoveInterface closes the Interface's stopCh to stop the
unsolicited ARP/NA sender, but only unregisters the Interface from the
Namespace once the removal has completed. Several failure exits sit in
between, so a removal that fails part-way leaves the Interface
registered with a closed stopCh.
Callers tear interfaces down by iterating over Namespace.Interfaces, so
the same *Interface is handed back to RemoveInterface during a later
teardown, and the second close panics, taking down the API request
being served:
http: panic serving @: close of closed channel
Seen in CI while disconnecting an endpoint during Sandbox.Refresh:
LinkSetNsFd failed with EBADF after the link had already been renamed
back to its source name, so the following re-join couldn't find the
link, and the rollback in Endpoint.sbJoin removed the same Interface
again.
Close stopCh through a sync.Once, so the ARP/NA sender is still
guaranteed to stop whether or not the netlink teardown succeeds, and
however many times removal is attempted. Keep leaving a failed
Interface registered in the Namespace: the link may still be present
there, and generateIfaceName relies on n.iFaces to avoid handing out
its name again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Cory Snider <csnider@mirantis.com>
145 lines
3.9 KiB
Go
145 lines
3.9 KiB
Go
package osl
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/moby/moby/v2/daemon/libnetwork/nlwrap"
|
|
"github.com/moby/moby/v2/internal/sliceutil"
|
|
"github.com/vishvananda/netlink"
|
|
"github.com/vishvananda/netns"
|
|
"gotest.tools/v3/assert"
|
|
is "gotest.tools/v3/assert/cmp"
|
|
)
|
|
|
|
func TestGenerateIfaceName(t *testing.T) {
|
|
testcases := []struct {
|
|
names []string
|
|
want string
|
|
}{
|
|
{names: []string{"test0", "test1"}, want: "test2"},
|
|
{names: []string{"test0", "test2"}, want: "test1"},
|
|
{names: []string{"test2"}, want: "test0"},
|
|
{names: []string{"test-0", "test-1"}, want: "test0"},
|
|
{names: []string{}, want: "test0"},
|
|
}
|
|
|
|
for _, tc := range testcases {
|
|
ns := &Namespace{
|
|
iFaces: sliceutil.Map(tc.names, func(name string) *Interface {
|
|
return &Interface{dstName: name}
|
|
}),
|
|
}
|
|
|
|
got := ns.generateIfaceName("test")
|
|
assert.Equal(t, got, tc.want)
|
|
}
|
|
}
|
|
|
|
// TestAddInterfaceInParallel tests that interface name are correctly generated
|
|
// even when many interfaces are added in parallel.
|
|
func TestAddInterfaceInParallel(t *testing.T) {
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
|
|
nsh, err := netns.NewNamed(t.Name())
|
|
assert.NilError(t, err)
|
|
defer netns.DeleteNamed(t.Name())
|
|
defer nsh.Close()
|
|
|
|
nlh, err := nlwrap.NewHandleAt(nsh)
|
|
assert.NilError(t, err)
|
|
|
|
ns := &Namespace{
|
|
path: "/run/netns/" + t.Name(),
|
|
nlHandle: nlh,
|
|
}
|
|
|
|
// Create a few dummy interfaces with a dummy name. The call to
|
|
// AddInterface below will rename them into their final name (ie. ethX).
|
|
for i := range 10 {
|
|
nlh.LinkAdd(&netlink.Dummy{
|
|
LinkAttrs: netlink.LinkAttrs{
|
|
Name: fmt.Sprintf("dummy%d", i),
|
|
},
|
|
})
|
|
}
|
|
|
|
wg := sync.WaitGroup{}
|
|
for i := range 10 {
|
|
src := fmt.Sprintf("dummy%d", i)
|
|
wg.Go(func() {
|
|
err := ns.AddInterface(context.Background(), src, "eth", "", WithCreatedInContainer(true))
|
|
assert.NilError(t, err)
|
|
})
|
|
}
|
|
wg.Wait()
|
|
|
|
links, err := nlwrap.LinkList()
|
|
assert.NilError(t, err)
|
|
var eths []string
|
|
for _, link := range links {
|
|
if strings.HasPrefix(link.Attrs().Name, "eth") {
|
|
eths = append(eths, link.Attrs().Name)
|
|
}
|
|
}
|
|
|
|
sort.Strings(eths)
|
|
assert.DeepEqual(t, eths, []string{"eth0", "eth1", "eth2", "eth3", "eth4", "eth5", "eth6", "eth7", "eth8", "eth9"})
|
|
}
|
|
|
|
// TestRemoveInterfaceTwice checks that a second attempt to remove an interface
|
|
// returns an error instead of panicking.
|
|
//
|
|
// A failed RemoveInterface leaves the Interface in the Namespace. Callers
|
|
// tear interfaces down by iterating over [Namespace.Interfaces], so the same
|
|
// *Interface is passed to RemoveInterface again during a later teardown - for
|
|
// example, by the rollback in libnetwork's Endpoint.sbJoin after a failed
|
|
// re-join.
|
|
func TestRemoveInterfaceTwice(t *testing.T) {
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
|
|
nsh, err := netns.NewNamed(t.Name())
|
|
assert.NilError(t, err)
|
|
defer netns.DeleteNamed(t.Name())
|
|
defer nsh.Close()
|
|
|
|
nlh, err := nlwrap.NewHandleAt(nsh)
|
|
assert.NilError(t, err)
|
|
defer nlh.Close()
|
|
|
|
ns := &Namespace{
|
|
path: "/run/netns/" + t.Name(),
|
|
nlHandle: nlh,
|
|
}
|
|
|
|
assert.NilError(t, nlh.LinkAdd(&netlink.Dummy{
|
|
LinkAttrs: netlink.LinkAttrs{Name: "dummy0"},
|
|
}))
|
|
assert.NilError(t, ns.AddInterface(context.Background(), "dummy0", "eth", "", WithCreatedInContainer(true)))
|
|
|
|
ifaces := ns.Interfaces()
|
|
assert.Assert(t, is.Len(ifaces, 1))
|
|
|
|
// Delete the link so that the first removal fails part-way through, like the
|
|
// failures seen in CI ("LinkSetNsFd failed for interface vethc9f9c7d: bad
|
|
// file descriptor").
|
|
link, err := nlh.LinkByName(ifaces[0].DstName())
|
|
assert.NilError(t, err)
|
|
assert.NilError(t, nlh.LinkDel(link))
|
|
|
|
assert.Assert(t, ns.RemoveInterface(ifaces[0]) != nil)
|
|
// The interface is still in the Namespace, so it'll be removed again later.
|
|
remaining := ns.Interfaces()
|
|
assert.Assert(t, is.Len(remaining, 1))
|
|
assert.Equal(t, remaining[0], ifaces[0])
|
|
|
|
assert.Assert(t, ns.RemoveInterface(ifaces[0]) != nil)
|
|
}
|