While playing around with the code below I noticed that a network namespace can be kept alive without neither a process attached to it nor any "direct" reference by an open file descriptor to the nsfs inode.
How can I discover the network namespace an open (RT)NETLINK socket refers to, given only the NETLINK socket?
Running the following code with go run -exec sudo main.go
will first create a new network namespace in a separate OS-locked goroutine. The same thread will then open a NETLINK socket for RTNETLINK in this new network namespace. The file descriptor is passed back to the initial (and also locked) thread/goroutine and the separate goroutine/thread terminates. This will leave the process with a set of threads (tasks) that are attached to the original network namespace when starting the code. There is now no thread anymore directly attached to the new network namespace nor any direct namespace fd reference.
There finally is only the NETLINK/RTNETLINK socket left and only indirectly references the new network namespace. Yet, the network namespace is kept alive, as can be seen from the fact that on each query every 30s the code can still successfully list the network interfaces in the new network namespace.
package main
import (
"runtime"
"strings"
"syscall"
"time"
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
)
// creator creates a new network namespace, enters it and passed back a
// reference to it, then waits to be signalled to exit the network namespace so
// it might collapse if everything goes to plan. (which plan???)
//
// This function must be called on its own goroutine.
func creator(done <-chan struct{}, handle chan<- *netlink.Handle) {
println("creator thread:", syscall.Gettid())
// Make sure to lock this goroutine to its current OS-level thread, as the
// unshare syscall will affect only the thread it is called from. Switching
// threads would be ... slightly bad. We don't unlock, so upon return and
// fallout off the edge of our disk world this OS thread will be killed and
// never reused.
runtime.LockOSThread()
println("creator: locked OS-level thread")
err := syscall.Unshare(syscall.CLONE_NEWNET)
if err != nil {
panic("cannot create and enter new network namespace: " + err.Error())
}
println("creator: in new network namespace")
nlHandle, err := netlink.NewHandle(unix.NETLINK_ROUTE)
if err != nil {
panic("cannot open RTNETLINK connection: " + err.Error())
}
println("creator: sending RTNETLINK handle")
handle <- nlHandle
println("creator: handle sent")
<-done // wait for channel to be closed.
println("creator: falling off")
// ...simply fall off the edge.
}
func main() {
runtime.LockOSThread()
println("main thread:", syscall.Gettid())
done := make(chan struct{})
handle := make(chan *netlink.Handle)
go creator(done, handle)
nlHandle := <-handle
println("received RTNETLINK handle")
println("telling creator to stop")
close(done)
for {
links, err := nlHandle.LinkList()
if err != nil {
panic("RTNETLINK failed: " + err.Error())
}
var names []string
for _, link := range links {
names = append(names, link.Attrs().Name)
}
println("nifs found:", strings.Join(names, ", "))
time.Sleep(30 * time.Second)
}
}