Linux Networking – How to Unshare Network for Current Process

linuxnetworkingunshare

It's possible to run a new command without network access as non-root using unshare -r -n, for example:

$ unshare -r -n ls
a.txt  b.txt

A command that does require network access will fail predictably.

$ unshare -r -n curl unix.stackexchange.com
curl: (6) Could not resolve host: unix.stackexchange.com

I'm wondering whether it's possible to remove network access for the current process, potentially by writing to a magical file in /sys or something similar.

I'd like to be able to do something like

$ /bin/sh -c 'echo 1 > /sys/unsharethis; curl unix.stackexchange.com'

An excerpt from strace-ing unshare -r -n ls shows the unshare system call

open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=4759040, ...}) = 0
mmap(NULL, 4759040, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f7ec6968000
close(3)                                = 0
unshare(CLONE_NEWUSER|CLONE_NEWNET)     = 0
open("/proc/self/setgroups", O_WRONLY)  = 3
write(3, "deny", 4)                     = 4

Which suggests to me that unsharing the network access from the current process is in fact the only way of achieving unsharing (i.e. it can't be passed as an argument to spawn or some equivalent thereof). It also suggests that unsharing from a shell script wouldn't work unless the shell had been specifically extended to expose a wrapper around unshare.

Best Answer

This can be done, sort of, with the gdb debugger, and if the running process can be attached to (programs that alter their dumpable state, or are setgid etc. can't be attached to, unless from root).

Some optional files can help to use gdb like debug symbols for libc6, and a few Linux related include files to get the actual values of a few symbols later (eg on Debian: (possibly) libc6-dbg, libc6-dev and linux-libc-dev packages), but actually once the "recipe" is made, they're probably not needed anymore.

First what more than unshare() unshare -r is doing? Without this, the new user stays at nobody and can't even write as its initial user:

$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$ strace unshare -r -n /bin/sleep 1 2>&1 |sed -n '/^unshare/,/^execve/p'
unshare(CLONE_NEWNET|CLONE_NEWUSER)     = 0
open("/proc/self/setgroups", O_WRONLY)  = 3
write(3, "deny", 4)                     = 4
close(3)                                = 0
open("/proc/self/uid_map", O_WRONLY)    = 3
write(3, "0 1000 1", 8)                 = 8
close(3)                                = 0
open("/proc/self/gid_map", O_WRONLY)    = 3
write(3, "0 1000 1", 8)                 = 8
close(3)                                = 0
execve("/bin/sleep", ["/bin/sleep", "1"], [/* 18 vars */]) = 0

That will be used later.

$ ip -4 -br a
lo               UNKNOWN        127.0.0.1/8 
eth0@if19        UP             10.0.3.66/24 
$ ping -c1 10.0.3.1
PING 10.0.3.1 (10.0.3.1) 56(84) bytes of data.
64 bytes from 10.0.3.1: icmp_seq=1 ttl=64 time=0.167 ms

--- 10.0.3.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms

rtt min/avg/max/mdev = 0.167/0.167/0.167/0.000 ms
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$ echo $$
338
$

On an other terminal:

$ gdb --pid=338
Reading symbols from /bin/bash...(no debugging symbols found)...done.
Reading symbols from /lib/x86_64-linux-gnu/libtinfo.so.5...(no debugging symbols found)...done.
Reading symbols from /lib/x86_64-linux-gnu/libdl.so.2...Reading symbols from /usr/lib/debug/.build-id/b8/95f0831f623c5f23603401d4069f9f94c24761.debug...done.
done.
Reading symbols from /lib/x86_64-linux-gnu/libc.so.6...Reading symbols from /usr/lib/debug/.build-id/aa/889e26a70f98fa8d230d088f7cc5bf43573163.debug...done.
done.

[...]

(gdb)

Now let's call the first function:

(gdb) call unshare(CLONE_NEWNET|CLONE_NEWUSER)
No symbol "CLONE_NEWNET" in current context.

Ok, there might be a method to have gdb know it, but I'm not a guru:

(gdb) !
$ grep CLONE_NEW /usr/include/linux/sched.h # man 2 unshare
#define CLONE_NEWNS 0x00020000  /* New mount namespace group */
#define CLONE_NEWCGROUP     0x02000000  /* New cgroup namespace */
#define CLONE_NEWUTS        0x04000000  /* New utsname namespace */
#define CLONE_NEWIPC        0x08000000  /* New ipc namespace */
#define CLONE_NEWUSER       0x10000000  /* New user namespace */
#define CLONE_NEWPID        0x20000000  /* New pid namespace */
#define CLONE_NEWNET        0x40000000  /* New network namespace */
$ find /usr/include/ -name fcntl.h |xargs grep O_WRONLY # man 2 open
/usr/include/asm-generic/fcntl.h:#define O_WRONLY   00000001
$ exit
exit
(gdb) call unshare(0x50000000)
$1 = 0
(gdb) call open("/proc/self/setgroups", 1)
$2 = 3
(gdb) call write($2,"deny",4)
$3 = 4
(gdb) call close($2)
$4 = 0
(gdb) call open("/proc/self/uid_map", 1)
$5 = 3
(gdb) call write($5, "0 1000 1", 8)
$6 = 8
(gdb) call close($5)
$7 = 0
(gdb) call open("/proc/self/gid_map", 1)
$8 = 3
(gdb) call write($8, "0 1000 1", 8)
$9 = 8
(gdb) call close($8)
$10 = 0
(gdb) quit
A debugging session is active.

    Inferior 1 [process 338] will be detached.

Quit anyway? (y or n) y
Detaching from program: /bin/bash, process 338

On the altered process, one can verify the eth0 interface disappeared:

$ ip -br a
lo               DOWN           127.0.0.1/8 
$ echo $$
338
$ id
uid=0(root) gid=0(root) groupes=0(root)
$ touch /
touch: setting times of '/': Permission denied
$ touch ~/test1
$ ls ~/test1
/home/user/test1
$ ping 10.0.3.1
connect: Network is unreachable

There's no going back: the new user namespace can't change back to its initial namespace. If the process is running with enough privileges (eg root without lost capabilities nor SELinux) then it would be possible (using only unshare(CLONE_NEWNET) / setns(savedopenedfd)).

Of course it's possible to script it in a file, and alter any allowed running process, or have the shell alter itself from a gdb subprocess. Contents of removenetwork.gdb, valid here only to alter a process with pid:gid == 1000:1000 :

UPDATE: added the (approximate) return type for the syscalls below, this should avoid some versions of gdb to complain in non-dev environments:

call (int)unshare(0x50000000)
call (int)open("/proc/self/setgroups", 1)
call (long)write($2,"deny",4)
call (int)close($2)
call (int)open("/proc/self/uid_map", 1)
call (long)write($5, "0 1000 1", 8)
call (int)close($5)
call (int)open("/proc/self/gid_map", 1)
call (long)write($8, "0 1000 1", 8)
call (int)close($8)
quit

Example:

$ sh -c 'id; gdb --pid=$$ < removenetwork.gdb >/dev/null 2>&1; id; curl unix.stackexchange.com'
uid=1000(user) gid=1000(user) groups=1000(user)
uid=0(root) gid=0(root) groups=0(root)
curl: (6) Could not resolve host: unix.stackexchange.com

UPDATE: if root is not needed at all, as it appears for this Question, then there's no need to map to root at all. Simply replace occurences of write($XX, "0 1000 1", 8) with write($XX, "1000 1000 1", 11) (for the uid:gid == 1000:1000 case). Supplementary groups are still unavoidably lost, but the uid/gid doesn't change (is mapped to itself).

Related Question