1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
|
// Copyright 2018 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
//
// https://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.
// This file was originally a part of the containernetworking/plugins
// repository.
// It was copied here and modified for local use by the libpod maintainers.
package netns
import (
"crypto/rand"
"errors"
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containers/storage/pkg/homedir"
"github.com/containers/storage/pkg/unshare"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
// 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) {
if unshare.IsRootless() {
rootlessDir, err := homedir.GetRuntimeDir()
if err != nil {
return "", err
}
return filepath.Join(rootlessDir, "netns"), nil
}
return "/run/netns", nil
}
func NewNSAtPath(nsPath string) (ns.NetNS, error) {
return newNSPath(nsPath)
}
// NewNS creates a new persistent (bind-mounted) network namespace and returns
// an object representing that namespace, without switching to it.
func NewNS() (ns.NetNS, error) {
nsRunDir, err := GetNSRunDir()
if err != nil {
return nil, err
}
// Create the directory for mounting network namespaces
// This needs to be a shared mountpoint in case it is mounted in to
// other namespaces (containers)
err = makeNetnsDir(nsRunDir)
if err != nil {
return nil, err
}
for range 10000 {
nsName, err := getRandomNetnsName()
if err != nil {
return nil, err
}
nsPath := path.Join(nsRunDir, nsName)
ns, err := newNSPath(nsPath)
if err == nil {
return ns, nil
}
// retry when the name already exists
if errors.Is(err, os.ErrExist) {
continue
}
return nil, err
}
return nil, errNoFreeName
}
// 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 range 10000 {
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 {
return err
}
// Important, the bind mount setup is racy if two process try to set it up in parallel.
// This can have very bad consequences because we end up with two duplicated mounts
// for the netns file that then might have a different parent mounts.
// Also because as root netns dir is also created by ip netns we should not race against them.
// Use a lock on the netns dir like they do, compare the iproute2 ip netns add code.
// https://github.com/iproute2/iproute2/blob/8b9d9ea42759c91d950356ca43930a975d0c352b/ip/ipnetns.c#L806-L815
dirFD, err := unix.Open(nsRunDir, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
if err != nil {
return &os.PathError{Op: "open", Path: nsRunDir, Err: err}
}
// closing the fd will also unlock so we do not have to call flock(fd,LOCK_UN)
defer unix.Close(dirFD)
err = unix.Flock(dirFD, unix.LOCK_EX)
if err != nil {
return fmt.Errorf("failed to lock %s dir: %w", nsRunDir, err)
}
// Remount the namespace directory shared. This will fail with EINVAL
// if it is not already a mountpoint, so bind-mount it on to itself
// to "upgrade" it to a mountpoint.
err = unix.Mount("", nsRunDir, "none", unix.MS_SHARED|unix.MS_REC, "")
if err == nil {
return nil
}
if err != unix.EINVAL {
return fmt.Errorf("mount --make-rshared %s failed: %q", nsRunDir, err)
}
// Recursively remount /run/netns on itself. The recursive flag is
// so that any existing netns bindmounts are carried over.
err = unix.Mount(nsRunDir, nsRunDir, "none", unix.MS_BIND|unix.MS_REC, "")
if err != nil {
return fmt.Errorf("mount --rbind %s %s failed: %q", nsRunDir, nsRunDir, err)
}
// Now we can make it shared
err = unix.Mount("", nsRunDir, "none", unix.MS_SHARED|unix.MS_REC, "")
if err != nil {
return fmt.Errorf("mount --make-rshared %s failed: %q", nsRunDir, err)
}
return nil
}
// 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 err
}
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
defer func() {
_ = 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
go (func() {
defer wg.Done()
runtime.LockOSThread()
// Don't unlock. By not unlocking, golang will kill the OS thread when the
// goroutine is done (for go1.10+)
// create a new netns on the current thread
err = unix.Unshare(unix.CLONE_NEWNET)
if err != nil {
err = fmt.Errorf("unshare network namespace: %w", err)
return
}
// bind mount the netns from the current thread (from /proc) onto the
// mount point. This causes the namespace to persist, even when there
// are no threads in the ns. Make this a shared mount; it needs to be
// back-propagated to the host
err = unix.Mount(threadNsPath, nsPath, "none", unix.MS_BIND|unix.MS_SHARED|unix.MS_REC, "")
if err != nil {
err = fmt.Errorf("failed to bind mount ns at %s: %v", nsPath, err)
}
})()
wg.Wait()
if err != nil {
return nil, fmt.Errorf("failed to create namespace: %v", err)
}
return ns.GetNS(nsPath)
}
// UnmountNS unmounts the given netns path
func UnmountNS(nsPath string) error {
// Only unmount if it's been bind-mounted (don't touch namespaces in /proc...)
if strings.HasPrefix(nsPath, "/proc/") {
return nil
}
// EINVAL means the path exists but is not mounted, just try to remove the path below
if err := unix.Unmount(nsPath, unix.MNT_DETACH); err != nil && !errors.Is(err, unix.EINVAL) {
// If path does not exists we can return without error as we have nothing to do.
if errors.Is(err, unix.ENOENT) {
return nil
}
return fmt.Errorf("failed to unmount NS: at %s: %w", nsPath, err)
}
var err error
// wait for up to 60s in the loop
for range 6000 {
if err = os.Remove(nsPath); err != nil {
if errors.Is(err, unix.EBUSY) {
// mount is still busy, sleep a moment and try again to remove
logrus.Debugf("Netns %s still busy, try removing it again in 10ms", nsPath)
time.Sleep(10 * time.Millisecond)
continue
}
// If path does not exists we can return without error.
if errors.Is(err, unix.ENOENT) {
return nil
}
return fmt.Errorf("failed to remove ns path: %w", err)
}
return nil
}
return fmt.Errorf("failed to remove ns path (timeout after 60s): %w", err)
}
|