
The current ns package code is very careful about not leaving the calling thread with the overridden namespace set, for example when origns.Set() fails. This is achieved by starting a new green thread, locking its OS thread, and never unlocking it. Which makes golang runtime to scrap the OS thread backing the green thread after the go routine exits. While this works, it's probably not as optimal: stopping and starting a new OS thread is expensive and may be avoided if we unlock the thread after resetting network namespace to the original. On the other hand, if resetting fails, it's better to leave the thread locked and die. While it won't work in all cases, we can still make an attempt to reuse the OS thread when resetting the namespace succeeds. This can be achieved by unlocking the thread conditionally to the namespace reset success. Signed-off-by: Ihar Hrachyshka <ihrachys@redhat.com>
230 lines
6.2 KiB
Go
230 lines
6.2 KiB
Go
// 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) {
|
|
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 = 0x6e736673
|
|
PROCFS_MAGIC = 0x9fa0
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
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 := GetCurrentNS()
|
|
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)
|
|
}
|