| // Copyright 2015-2017 CNI authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package ns |
| |
| import ( |
| "fmt" |
| "os" |
| "runtime" |
| "sync" |
| "syscall" |
| |
| "golang.org/x/sys/unix" |
| ) |
| |
| // Returns an object representing the current OS thread's network namespace |
| func GetCurrentNS() (NetNS, error) { |
| // Lock the thread in case other goroutine executes in it and changes its |
| // network namespace after getCurrentThreadNetNSPath(), otherwise it might |
| // return an unexpected network namespace. |
| runtime.LockOSThread() |
| defer runtime.UnlockOSThread() |
| return getCurrentNSNoLock() |
| } |
| |
| func getCurrentNSNoLock() (NetNS, error) { |
| return GetNS(getCurrentThreadNetNSPath()) |
| } |
| |
| 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()) |
| } |
| |
| func (ns *netNS) Close() error { |
| if err := ns.errorIfClosed(); err != nil { |
| return err |
| } |
| |
| if err := ns.file.Close(); err != nil { |
| return fmt.Errorf("Failed to close %q: %v", ns.file.Name(), err) |
| } |
| ns.closed = true |
| |
| return nil |
| } |
| |
| func (ns *netNS) Set() error { |
| if err := ns.errorIfClosed(); err != nil { |
| return err |
| } |
| |
| if err := unix.Setns(int(ns.Fd()), unix.CLONE_NEWNET); err != nil { |
| return fmt.Errorf("Error switching to ns %v: %v", ns.file.Name(), err) |
| } |
| |
| return nil |
| } |
| |
| type NetNS interface { |
| // Executes the passed closure in this object's network namespace, |
| // attempting to restore the original namespace before returning. |
| // However, since each OS thread can have a different network namespace, |
| // and Go's thread scheduling is highly variable, callers cannot |
| // guarantee any specific namespace is set unless operations that |
| // require that namespace are wrapped with Do(). Also, no code called |
| // from Do() should call runtime.UnlockOSThread(), or the risk |
| // of executing code in an incorrect namespace will be greater. See |
| // https://github.com/golang/go/wiki/LockOSThread for further details. |
| Do(toRun func(NetNS) error) error |
| |
| // Sets the current network namespace to this object's network namespace. |
| // Note that since Go's thread scheduling is highly variable, callers |
| // cannot guarantee the requested namespace will be the current namespace |
| // after this function is called; to ensure this wrap operations that |
| // require the namespace with Do() instead. |
| 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 |
| closed bool |
| } |
| |
| // netNS implements the NetNS interface |
| var _ NetNS = &netNS{} |
| |
| const ( |
| // https://github.com/torvalds/linux/blob/master/include/uapi/linux/magic.h |
| NSFS_MAGIC = unix.NSFS_MAGIC |
| PROCFS_MAGIC = unix.PROC_SUPER_MAGIC |
| ) |
| |
| type NSPathNotExistErr struct{ msg string } |
| |
| func (e NSPathNotExistErr) Error() string { return e.msg } |
| |
| type NSPathNotNSErr struct{ msg string } |
| |
| func (e NSPathNotNSErr) Error() string { return e.msg } |
| |
| func IsNSorErr(nspath string) error { |
| stat := syscall.Statfs_t{} |
| if err := syscall.Statfs(nspath, &stat); err != nil { |
| if os.IsNotExist(err) { |
| err = NSPathNotExistErr{msg: fmt.Sprintf("failed to Statfs %q: %v", nspath, err)} |
| } else { |
| err = fmt.Errorf("failed to Statfs %q: %v", nspath, err) |
| } |
| return err |
| } |
| |
| switch stat.Type { |
| case PROCFS_MAGIC, NSFS_MAGIC: |
| return nil |
| default: |
| return NSPathNotNSErr{msg: fmt.Sprintf("unknown FS magic on %q: %x", nspath, stat.Type)} |
| } |
| } |
| |
| // Returns an object representing the namespace referred to by @path |
| func GetNS(nspath string) (NetNS, error) { |
| err := IsNSorErr(nspath) |
| if err != nil { |
| return nil, err |
| } |
| |
| fd, err := os.Open(nspath) |
| if err != nil { |
| return nil, err |
| } |
| |
| return &netNS{file: fd}, nil |
| } |
| |
| // Returns a new empty NetNS. |
| // Calling Close() let the kernel garbage collect the network namespace. |
| func TempNetNS() (NetNS, error) { |
| var tempNS NetNS |
| var err error |
| var wg sync.WaitGroup |
| wg.Add(1) |
| |
| // Create the new namespace in a new goroutine so that if we later fail |
| // to switch the namespace back to the original one, we can safely |
| // leave the thread locked to die without a risk of the current thread |
| // left lingering with incorrect namespace. |
| go func() { |
| defer wg.Done() |
| runtime.LockOSThread() |
| |
| var threadNS NetNS |
| // save a handle to current network namespace |
| threadNS, err = getCurrentNSNoLock() |
| if err != nil { |
| err = fmt.Errorf("failed to open current namespace: %v", err) |
| return |
| } |
| defer threadNS.Close() |
| |
| // create the temporary network namespace |
| err = unix.Unshare(unix.CLONE_NEWNET) |
| if err != nil { |
| return |
| } |
| |
| // get a handle to the temporary network namespace |
| tempNS, err = getCurrentNSNoLock() |
| |
| err2 := threadNS.Set() |
| if err2 == nil { |
| // Unlock the current thread only when we successfully switched back |
| // to the original namespace; otherwise leave the thread locked which |
| // will force the runtime to scrap the current thread, that is maybe |
| // not as optimal but at least always safe to do. |
| runtime.UnlockOSThread() |
| } |
| }() |
| |
| wg.Wait() |
| return tempNS, err |
| } |
| |
| func (ns *netNS) Path() string { |
| return ns.file.Name() |
| } |
| |
| func (ns *netNS) Fd() uintptr { |
| return ns.file.Fd() |
| } |
| |
| func (ns *netNS) errorIfClosed() error { |
| if ns.closed { |
| return fmt.Errorf("%q has already been closed", ns.file.Name()) |
| } |
| return nil |
| } |
| |
| func (ns *netNS) Do(toRun func(NetNS) error) error { |
| if err := ns.errorIfClosed(); err != nil { |
| return err |
| } |
| |
| containedCall := func(hostNS NetNS) error { |
| threadNS, err := getCurrentNSNoLock() |
| 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 func() { |
| err := threadNS.Set() // switch back |
| if err == nil { |
| // Unlock the current thread only when we successfully switched back |
| // to the original namespace; otherwise leave the thread locked which |
| // will force the runtime to scrap the current thread, that is maybe |
| // not as optimal but at least always safe to do. |
| runtime.UnlockOSThread() |
| } |
| }() |
| |
| return toRun(hostNS) |
| } |
| |
| // save a handle to current network namespace |
| hostNS, err := GetCurrentNS() |
| if err != nil { |
| return fmt.Errorf("Failed to open current namespace: %v", err) |
| } |
| defer hostNS.Close() |
| |
| var wg sync.WaitGroup |
| wg.Add(1) |
| |
| // Start the callback in a new green thread so that if we later fail |
| // to switch the namespace back to the original one, we can safely |
| // leave the thread locked to die without a risk of the current thread |
| // left lingering with incorrect namespace. |
| var innerError error |
| go func() { |
| defer wg.Done() |
| runtime.LockOSThread() |
| innerError = containedCall(hostNS) |
| }() |
| wg.Wait() |
| |
| return innerError |
| } |
| |
| // 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 err |
| } |
| defer ns.Close() |
| return ns.Do(toRun) |
| } |