From fa8bed058077e203f552b808751f4a4e3755c5db Mon Sep 17 00:00:00 2001 From: Richard Lawrence Date: Thu, 16 Jul 2026 14:06:02 -0500 Subject: [PATCH] Add a --netns FD option for preexisting namespaces Implement `--netns FD` to allow bubblewrap to join preexisting network namespaces via an open file descriptor. This mimics the behavior of `--userns` and `--pidns`. Enforce mutual exclusivity between `--netns` and `--unshare-net`. Add documentation to `bwrap.xml`, update shell completions, and add a new integration test case (`test-specifying-netns.sh`). Co-authored-by: rarensu <40872193+rarensu@users.noreply.github.com> Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Signed-off-by: Richard Lawrence --- bubblewrap.c | 32 +++++++++++++++++++++ bwrap.xml | 4 +++ completions/bash/bwrap | 1 + completions/zsh/_bwrap | 1 + tests/meson.build | 1 + tests/test-specifying-netns.sh | 52 ++++++++++++++++++++++++++++++++++ 6 files changed, 91 insertions(+) create mode 100755 tests/test-specifying-netns.sh diff --git a/bubblewrap.c b/bubblewrap.c index 3b9719c4..06be1a39 100644 --- a/bubblewrap.c +++ b/bubblewrap.c @@ -90,6 +90,7 @@ static char *opt_args_data = NULL; /* owned */ static int opt_userns_fd = -1; static int opt_userns2_fd = -1; static int opt_pidns_fd = -1; +static int opt_netns_fd = -1; static int opt_tmp_overlay_count = 0; static int next_perms = -1; static size_t next_size_arg = 0; @@ -303,6 +304,7 @@ usage (int ecode, FILE *out) " --disable-userns Disable further use of user namespaces inside sandbox\n" " --assert-userns-disabled Fail unless further use of user namespace inside sandbox is disabled\n" " --pidns FD Use this pid namespace (as parent namespace if using --unshare-pid)\n" + " --netns FD Use this net namespace (cannot combine with --unshare-net)\n" " --uid UID Custom uid in the sandbox (requires --unshare-user or --userns)\n" " --gid GID Custom gid in the sandbox (requires --unshare-user or --userns)\n" " --hostname NAME Custom hostname in the sandbox (requires --unshare-uts)\n" @@ -2210,6 +2212,26 @@ parse_args_recurse (int *argcp, opt_pidns_fd = the_fd; + argv += 1; + argc -= 1; + } + else if (strcmp (arg, "--netns") == 0) + { + int the_fd; + char *endptr; + + if (argc < 2) + die ("--netns takes an argument"); + + if (opt_netns_fd != -1) + warn_only_last_option ("--netns"); + + the_fd = strtol (argv[1], &endptr, 10); + if (argv[1][0] == 0 || endptr[0] != 0 || the_fd < 0) + die ("Invalid fd: %s", argv[1]); + + opt_netns_fd = the_fd; + argv += 1; argc -= 1; } @@ -2656,6 +2678,9 @@ main (int argc, if (opt_userns_fd != -1 && opt_unshare_user_try) die ("--userns is not compatible with --unshare-user-try"); + if (opt_netns_fd != -1 && opt_unshare_net) + die ("--netns is not compatible with --unshare-net"); + if (opt_disable_userns && !opt_unshare_user) die ("--disable-userns requires --unshare-user"); @@ -2798,6 +2823,9 @@ main (int argc, die_with_error ("Joining specified user namespace failed"); } + if (opt_netns_fd != -1 && setns (opt_netns_fd, CLONE_NEWNET) != 0) + die_with_error ("Joining specified net namespace failed"); + /* Sometimes we have uninteresting intermediate pids during the setup, set up code to pass the real pid down */ if (opt_pidns_fd != -1) { @@ -2938,6 +2966,10 @@ main (int argc, */ switch_to_user_with_privs (); + /* If a pre-existing network namespace is specified via --netns, we assume + * that it already contains a loopback interface, so we do not call + * loopback_setup(). + */ if (opt_unshare_net) loopback_setup (); /* Will exit if unsuccessful */ diff --git a/bwrap.xml b/bwrap.xml index ca717abd..13ee82f9 100644 --- a/bwrap.xml +++ b/bwrap.xml @@ -197,6 +197,10 @@ Use an existing pid namespace instead of creating one. This is often used with --userns, because the pid namespace must be owned by the same user namespace that bwrap uses. Note that this can be combined with --unshare-pid, and in that case it means that the sandbox will be in its own pid namespace, which is a child of the passed in one. + + + Use an existing net namespace instead of creating one. This cannot be combined with . + Use a custom user id in the sandbox (requires ) diff --git a/completions/bash/bwrap b/completions/bash/bwrap index e7a523c2..232d4711 100644 --- a/completions/bash/bwrap +++ b/completions/bash/bwrap @@ -51,6 +51,7 @@ _bwrap() { --hostname --info-fd --lock-file + --netns --overlay --overlay-src --perms diff --git a/completions/zsh/_bwrap b/completions/zsh/_bwrap index fbddda43..20b71287 100644 --- a/completions/zsh/_bwrap +++ b/completions/zsh/_bwrap @@ -54,6 +54,7 @@ _bwrap_args=( '--lock-file[Take a lock on DEST while sandbox is running]:lock file:_files' '--mqueue[Mount new mqueue on DEST]:mount point for mqueue:_files -/' '--new-session[Create a new terminal session]' + '--netns[Use this net namespace (cannot combine with --unshare-net)]: :' '--perms[Set permissions for next action argument]: :_guard "[0-7]#" "permissions in octal": :->after_perms' '--pidns[Use this user namespace (as parent namespace if using --unshare-pid)]: :' '--proc[Mount new procfs on DEST]:mount point for procfs:_files -/' diff --git a/tests/meson.build b/tests/meson.build index 87bf709e..69db7ef3 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -18,6 +18,7 @@ executable( test_scripts = [ 'test-run.sh', 'test-seccomp.py', + 'test-specifying-netns.sh', 'test-specifying-pidns.sh', 'test-specifying-userns.sh', ] diff --git a/tests/test-specifying-netns.sh b/tests/test-specifying-netns.sh new file mode 100755 index 00000000..e8d762f0 --- /dev/null +++ b/tests/test-specifying-netns.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +set -xeuo pipefail + +srcd=$(cd $(dirname "$0") && pwd) +. "${srcd}/libtest.sh" + +echo "1..1" + +# This test needs user namespaces +if test -n "${bwrap_is_suid:-}"; then + echo "ok - # SKIP no setuid support for --unshare-user" +else + # Start the server inside a bubblewrap sandbox with a new user and net namespace + $RUN --info-fd 42 --unshare-user --unshare-net python3 -c " +import socket +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.bind(('127.0.0.1', 0)) +port = s.getsockname()[1] +with open('server-port.txt', 'w') as f: + f.write(str(port)) +s.listen(1) +conn, addr = s.accept() +conn.sendall(b'hello from netns\n') +conn.close() +" 42>info.json & + + # Wait for the server to write the port file + while ! test -f server-port.txt; do sleep 0.1; done + SANDBOX1PID=$(extract_child_pid info.json) + + # Run the client inside another bubblewrap sandbox, joining the user and net namespaces of the first + ASAN_OPTIONS=detect_leaks=0 LSAN_OPTIONS=detect_leaks=0 \ + $RUN --userns 11 --netns 12 python3 -c " +import socket +with open('server-port.txt', 'r') as f: + port = int(f.read().strip()) +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.connect(('127.0.0.1', port)) +data = s.recv(1024) +print(data.decode().strip()) +s.close() +" 11< /proc/$SANDBOX1PID/ns/user 12< /proc/$SANDBOX1PID/ns/net > client-output.txt + + # Verify the output + echo "hello from netns" > expected.txt + assert_files_equal expected.txt client-output.txt + + rm -f info.json server-port.txt client-output.txt expected.txt + + echo "ok - Test --netns" +fi