Snippets:Ctrtool unprivileged network namespace creation

Requires ctrtool and Linux kernel 4.9 or higher with user namespace support (i.e. CONFIG_USER_NS=y).

#!/bin/sh

# create-unprivileged-netns.sh
# Public domain.

set -eu
TARGET_UID="$(printf '%u' "${1:-$SUDO_UID}")"
TARGET_GID="$(printf '%u' "${3:-$SUDO_GID}")"
TARGET_NS_NAME="${2}"
export TARGET_NS_NAME

mkdir -p /run/netns /run/userns

exec ctrtool launcher --escape -U -n -O "$TARGET_UID" \
    --uid-map="0.$TARGET_UID.1" --gid-map="0.$TARGET_GID.1" \
    --no-clear-groups --disable-setgroups --script-is-shell \
    --script='/usr/bin/env ctrtool mount_seq \
-m "/run/netns/$TARGET_NS_NAME" -f -s "/proc/self/fd/$2/ns/net" -K -Ob \
-m "/run/userns/$TARGET_NS_NAME" -f -s "/proc/self/fd/$2/ns/user" -K -Ob' \
    --wait --no-exec

Invoke this shell script as

sudo create-unprivileged-netns.sh "$UID" "netns0"

where $UID is the user ID you want to create the network namespace for (or "" for the invoking sudo user) and netns0 is the name of the network namespace you'd like to create (exactly as if you used ip netns add). For a user name, use "$(id -u USERNAME)".

You can manage this network namespace just as if you used the ip tool to create it. For example, you can add one half of a veth pair into the new namespace with admin privileges (since you also need to create the other half on the host netns).

However, unlike namespaces created with ip netns add, the user with the given UID can also run

nsenter --user="/run/userns/NAMESPACE" --net="/run/netns/NAMESPACE" --preserve-credentials COMMAND

without sudo to perform roughly the same operation as ip netns exec NAMESPACE COMMAND.

Or that same user can also do:

ctrtool ns_open_file -nN /run/netns/NAMESPACE -U -6 ::,80,a -l 4096 COMMAND

and then (given COMMAND = node index.js) run the following Node.js program (just an example):

const express = require("express");
var app = express();

app.use("/", (req, res) => { res.send(200, "Hello world!"); });

app.listen({fd: Number(process.env.CTRTOOL_NS_OPEN_FILE_FD_0)});

An interesting quirk about this method is that although the server will listen on connections in the new network namespace, outbound connections will be from the perspective of the host. So (for the node.js example) if you have a connection listener event handler, it will handle connections within the network namespace, but if you use net.createConnection or anything derived from it (e.g. http.request) to make an outbound connection, it will actually be from the host's network namespace.

(More information about ns_open_file can be found at Help:Ctrtool/ns_open_file.)

Unlike most other examples of unprivileged network namespace management, this method does not require the use of setuid, setgid, or file capabilities (other than sudo to set up the namespace). Instead, we wrap the network namespace in a user namespace whose owner UID is manipulated to allow access to that user (and therefore network) namespace by the specified unprivileged user.