Skip to content

Commit

Permalink
pkg/netns: add NewNSFrom()
Browse files Browse the repository at this point in the history
The new API should replace the logic from Podman's setupNetNS() which
did several things incorrect. First and foremost it never bind mounted
the netns dir causing problems when the netns dir was later bind mounted
duplicating the netns mounts on diffeent parents which then caused all
unmount attempts to fail[1].

Also the Podman code did never check for conlicts so we way end up
overwriting an existing netns (super unlikely as we use 16 random bytes
but still). At the very least it duplicates the name logic for no reason
there so having this all here makes much more sense.

Of course we still have to port podman over to this new API.

[1] https://issues.redhat.com/browse/RHEL-59620
Backport for https://issues.redhat.com/browse/RHEL-59703

Signed-off-by: Paul Holzinger <[email protected]>
  • Loading branch information
Luap99 committed Sep 20, 2024
1 parent 50870e9 commit 8a5b951
Showing 1 changed file with 69 additions and 11 deletions.
80 changes: 69 additions & 11 deletions pkg/netns/netns_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import (
// threadNsPath is the /proc path to the current netns handle for the current thread
const threadNsPath = "/proc/thread-self/ns/net"

var errNoFreeName = errors.New("failed to find free netns path name")

// GetNSRunDir returns the dir of where to create the netNS. When running
// rootless, it needs to be at a location writable by user.
func GetNSRunDir() (string, error) {
Expand All @@ -61,12 +63,10 @@ func NewNSAtPath(nsPath string) (ns.NetNS, error) {
// an object representing that namespace, without switching to it.
func NewNS() (ns.NetNS, error) {
for i := 0; i < 10000; i++ {
b := make([]byte, 16)
_, err := rand.Reader.Read(b)
nsName, err := getRandomNetnsName()
if err != nil {
return nil, fmt.Errorf("failed to generate random netns name: %v", err)
return nil, err
}
nsName := fmt.Sprintf("netns-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
ns, err := NewNSWithName(nsName)
if err == nil {
return ns, nil
Expand All @@ -77,7 +77,7 @@ func NewNS() (ns.NetNS, error) {
}
return nil, err
}
return nil, errors.New("failed to find free netns path name")
return nil, errNoFreeName
}

// NewNSWithName creates a new persistent (bind-mounted) network namespace and returns
Expand All @@ -100,6 +100,58 @@ func NewNSWithName(name string) (ns.NetNS, error) {
return newNSPath(nsPath)
}

// NewNSFrom creates a persistent (bind-mounted) network namespace from the
// given netns path, i.e. /proc/<pid>/ns/net, and returns the new full path to
// the bind mounted file in the netns run dir.
func NewNSFrom(fromNetns string) (string, error) {
nsRunDir, err := GetNSRunDir()
if err != nil {
return "", err
}

err = makeNetnsDir(nsRunDir)
if err != nil {
return "", err
}

for i := 0; i < 10000; i++ {
nsName, err := getRandomNetnsName()
if err != nil {
return "", err
}
nsPath := filepath.Join(nsRunDir, nsName)

// create an empty file to use as at the mount point
err = createNetnsFile(nsPath)
if err != nil {
// retry when the name already exists
if errors.Is(err, os.ErrExist) {
continue
}
return "", err
}

err = unix.Mount(fromNetns, nsPath, "none", unix.MS_BIND|unix.MS_SHARED|unix.MS_REC, "")
if err != nil {
// Do not leak the ns on errors
_ = os.RemoveAll(nsPath)
return "", fmt.Errorf("failed to bind mount ns at %s: %v", nsPath, err)
}
return nsPath, nil
}

return "", errNoFreeName
}

func getRandomNetnsName() (string, error) {
b := make([]byte, 16)
_, err := rand.Reader.Read(b)
if err != nil {
return "", fmt.Errorf("failed to generate random netns name: %v", err)
}
return fmt.Sprintf("netns-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]), nil
}

func makeNetnsDir(nsRunDir string) error {
err := os.MkdirAll(nsRunDir, 0o755)
if err != nil {
Expand Down Expand Up @@ -151,16 +203,22 @@ func makeNetnsDir(nsRunDir string) error {
return nil
}

func newNSPath(nsPath string) (ns.NetNS, error) {
// create an empty file at the mount point
mountPointFd, err := os.OpenFile(nsPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o600)
// createNetnsFile created the file with O_EXCL to ensure there are no conflicts with others
// Callers should check for ErrExist and loop over it to find a free file.
func createNetnsFile(path string) error {
mountPointFd, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
return nil, err
return err
}
if err := mountPointFd.Close(); err != nil {
return mountPointFd.Close()
}

func newNSPath(nsPath string) (ns.NetNS, error) {
// create an empty file to use as at the mount point
err := createNetnsFile(nsPath)
if err != nil {
return nil, err
}

// Ensure the mount point is cleaned up on errors; if the namespace
// was successfully mounted this will have no effect because the file
// is in-use
Expand Down

0 comments on commit 8a5b951

Please sign in to comment.