Skip to content

Commit

Permalink
ns: make namespaces an interface and add NetNS creation capability
Browse files Browse the repository at this point in the history
This also removes the thread-locking arguments from the ns package
per containernetworking#183 by doing all the namespace
changes in a separate goroutine that locks/unlocks itself, instead of
the caller having to track OS thread locking.
  • Loading branch information
dcbw committed Apr 19, 2016
1 parent 57b28f3 commit dafdc4a
Show file tree
Hide file tree
Showing 15 changed files with 299 additions and 384 deletions.
6 changes: 3 additions & 3 deletions pkg/ip/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func RandomVethName() (string, error) {
// SetupVeth sets up a virtual ethernet link.
// Should be in container netns, and will switch back to hostNS to set the host
// veth end up.
func SetupVeth(contVethName string, mtu int, hostNS *os.File) (hostVeth, contVeth netlink.Link, err error) {
func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (hostVeth, contVeth netlink.Link, err error) {
var hostVethName string
hostVethName, contVeth, err = makeVeth(contVethName, mtu)
if err != nil {
Expand All @@ -104,10 +104,10 @@ func SetupVeth(contVethName string, mtu int, hostNS *os.File) (hostVeth, contVet
return
}

err = ns.WithNetNS(hostNS, false, func(_ *os.File) error {
err = hostNS.Do(func(_ ns.NetNS) error {
hostVeth, err := netlink.LinkByName(hostVethName)
if err != nil {
return fmt.Errorf("failed to lookup %q in %q: %v", hostVethName, hostNS.Name(), err)
return fmt.Errorf("failed to lookup %q in %q: %v", hostVethName, hostNS.Path(), err)
}

if err = netlink.LinkSetUp(hostVeth); err != nil {
Expand Down
214 changes: 165 additions & 49 deletions pkg/ns/ns.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,79 +15,195 @@
package ns

import (
"crypto/rand"
"fmt"
"os"
"path"
"runtime"
"syscall"
"sync"

"golang.org/x/sys/unix"
)

var setNsMap = map[string]uintptr{
"386": 346,
"amd64": 308,
"arm": 374,
type NetNS interface {
// Executes the passed closure in this object's network namespace,
// restoring the original namespace afterwards. If the closure returns
// an error, Do attempts to restore the original namespace before
// returning.
Do(toRun func(NetNS) error) error

// Sets the current network namespace to this object's network namespace
Set() error

// Returns the filesystem path representing this object's network namespace
Path() string

// Returns a file descriptor representing this object's network namespace
Fd() uintptr

// Cleans up this instance of the network namespace; if this instance
// is the last user the namespace will be destroyed
Close() error
}

type netNS struct {
file *os.File
mounted bool
}

// SetNS sets the network namespace on a target file.
func SetNS(f *os.File, flags uintptr) error {
if runtime.GOOS != "linux" {
return fmt.Errorf("unsupported OS: %s", runtime.GOOS)
func getCurrentThreadNetNSPath() string {
// /proc/self/ns/net returns the namespace of the main thread, not
// of whatever thread this goroutine is running on. Make sure we
// use the thread's net namespace since the thread is switching around
return fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid())
}

// Returns an object representing the namespace referred to by @path
func GetNS(nspath string) (NetNS, error) {
fd, err := os.Open(nspath)
if err != nil {
return nil, err
}
return &netNS{file: fd}, nil
}

// Creates a new network namespace, switches to it, and returns an object
// representing that network namespace
func NewNS() (NetNS, error) {
const nsRunDir = "/var/run/netns"

b := make([]byte, 16)
_, err := rand.Reader.Read(b)
if err != nil {
return nil, fmt.Errorf("failed to generate random netns name: %v", err)
}

trap, ok := setNsMap[runtime.GOARCH]
if !ok {
return fmt.Errorf("unsupported arch: %s", runtime.GOARCH)
err = os.MkdirAll(nsRunDir, 0755)
if err != nil {
return nil, err
}

_, _, err := syscall.RawSyscall(trap, f.Fd(), flags, 0)
if err != 0 {
return err
// create an empty file at the mount point
nsName := fmt.Sprintf("cni-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
nsPath := path.Join(nsRunDir, nsName)
mountPointFd, err := os.Create(nsPath)
if err != nil {
return nil, err
}
mountPointFd.Close()

return nil
// Ensure the mount point is cleaned up on errors; if the namespace
// was successfully mounted this will have no effect
defer os.RemoveAll(nsPath)

var wg sync.WaitGroup
wg.Add(1)

// do namespace work in a dedicated goroutine, so that we can safely
// Lock/Unlock OSThread without upsetting the lock/unlock state of
// the caller of this function
var fd *os.File
go (func() {
defer wg.Done()
runtime.LockOSThread()

// create a new netns on the current thread
err = unix.Unshare(unix.CLONE_NEWNET)
if err != nil {
return
}

// bind mount the new netns from the current thread onto the mount point
err = unix.Mount(getCurrentThreadNetNSPath(), nsPath, "none", unix.MS_BIND, "")
if err != nil {
return
}

fd, err = os.Open(nsPath)
})()
wg.Wait()

return &netNS{file: fd, mounted: true}, err
}

// WithNetNSPath executes the passed closure under the given network
// namespace, restoring the original namespace afterwards.
// Changing namespaces must be done on a goroutine that has been
// locked to an OS thread. If lockThread arg is true, this function
// locks the goroutine prior to change namespace and unlocks before
// returning
func WithNetNSPath(nspath string, lockThread bool, f func(*os.File) error) error {
ns, err := os.Open(nspath)
if err != nil {
return fmt.Errorf("Failed to open %v: %v", nspath, err)
func (ns *netNS) Path() string {
return ns.file.Name()
}

func (ns *netNS) Fd() uintptr {
return ns.file.Fd()
}

func (ns *netNS) Close() error {
ns.file.Close()

if ns.mounted {
if err := unix.Unmount(ns.file.Name(), unix.MNT_DETACH); err != nil {
return fmt.Errorf("Failed to unmount namespace %s: %v", ns.file.Name(), err)
}
if err := os.RemoveAll(ns.file.Name()); err != nil {
return fmt.Errorf("Failed to clean up namespace %s: %v", ns.file.Name(), err)
}
}
defer ns.Close()
return WithNetNS(ns, lockThread, f)
return nil
}

// WithNetNS executes the passed closure under the given network
// namespace, restoring the original namespace afterwards.
// Changing namespaces must be done on a goroutine that has been
// locked to an OS thread. If lockThread arg is true, this function
// locks the goroutine prior to change namespace and unlocks before
// returning. If the closure returns an error, WithNetNS attempts to
// restore the original namespace before returning.
func WithNetNS(ns *os.File, lockThread bool, f func(*os.File) error) error {
if lockThread {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
func (ns *netNS) Do(toRun func(NetNS) error) error {
containedCall := func(hostNS NetNS) error {
threadNS, err := GetNS(getCurrentThreadNetNSPath())
if err != nil {
return fmt.Errorf("failed to open current netns: %v", err)
}
defer threadNS.Close()

// switch to target namespace
if err = ns.Set(); err != nil {
return fmt.Errorf("error switching to ns %v: %v", ns.file.Name(), err)
}
defer threadNS.Set() // switch back

return toRun(hostNS)
}
// save a handle to current (host) network namespace
thisNS, err := os.Open("/proc/self/ns/net")

// save a handle to current network namespace
hostNS, err := GetNS(getCurrentThreadNetNSPath())
if err != nil {
return fmt.Errorf("Failed to open /proc/self/ns/net: %v", err)
return fmt.Errorf("Failed to open current namespace: %v", err)
}
defer thisNS.Close()
defer hostNS.Close()

if err = SetNS(ns, syscall.CLONE_NEWNET); err != nil {
return fmt.Errorf("Error switching to ns %v: %v", ns.Name(), err)
}
defer SetNS(thisNS, syscall.CLONE_NEWNET) // switch back
var wg sync.WaitGroup
wg.Add(1)

if err = f(thisNS); err != nil {
return err
var innerError error
go func() {
defer wg.Done()
runtime.LockOSThread()
innerError = containedCall(hostNS)
}()
wg.Wait()

return innerError
}

func (ns *netNS) Set() error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

if _, _, err := unix.Syscall(unix.SYS_SETNS, ns.Fd(), uintptr(unix.CLONE_NEWNET), 0); err != 0 {
return fmt.Errorf("Error switching to ns %v: %v", ns.file.Name(), err)
}

return nil
}

// WithNetNSPath executes the passed closure under the given network
// namespace, restoring the original namespace afterwards.
func WithNetNSPath(nspath string, toRun func(NetNS) error) error {
ns, err := GetNS(nspath)
if err != nil {
return fmt.Errorf("Failed to open %v: %v", nspath, err)
}
defer ns.Close()
return ns.Do(toRun)
}
Loading

0 comments on commit dafdc4a

Please sign in to comment.