systemd-nspawn and Private Networking

Currently there’s two things I want to do with my PC at the same time, one is watching streaming services like ABC iView (which won’t run from non-Australian IP addresses) and another is torrenting over a VPN. I had considered doing something ugly with iptables to try and get routing done on a per-UID basis but that seemed to difficult. At the time I wasn’t aware of the ip rule add uidrange [1] option. So setting up a private networking namespace with a systemd-nspawn container seemed like a good idea.

Chroot Setup

For the chroot (which I use as a slang term for a copy of a Linux installation in a subdirectory) I used a btrfs subvol that’s a snapshot of the root subvol. The idea is that when I upgrade the root system I can just recreate the chroot with a new snapshot.

To get this working I created files in the root subvol which are used for the container.

I created a script like the following named /usr/local/sbin/container-sshd to launch the container. It sets up the networking and executes sshd. The systemd-nspawn program is designed to launch init but that’s not required, I prefer to just launch sshd so there’s only one running process in a container that’s not being actively used.


# restorecon commands only needed for SE Linux
/sbin/restorecon -R /dev
/bin/mount none -t tmpfs /run
/bin/mkdir -p /run/sshd
/sbin/restorecon -R /run /tmp
/sbin/ifconfig host0 netmask
/sbin/route add default gw
exec /usr/sbin/sshd -D -f /etc/ssh/sshd_torrent_config

How to Launch It

To setup the container I used a command like “/usr/bin/systemd-nspawn -D /subvols/torrent -M torrent –bind=/home -n /usr/local/sbin/container-sshd“.

First I had tried the --network-ipvlan option which creates a new IP address on the same MAC address. That gave me an interface iv-br0 on the container that I could use normally (br0 being the bridge used in my workstation as it’s primary network interface). The IP address I assigned to that was in the same subnet as br0, but for some reason that’s unknown to me (maybe an interaction between bridging and network namespaces) I couldn’t access it from the host, I could only access it from other hosts on the network. I then tried the --network-macvlan option (to create a new MAC address for virtual networking), but that had the same problem with accessing the IP address from the local host outside the container as well as problems with MAC redirection to the primary MAC of the host (again maybe an interaction with bridging).

Then I tried just the “-n” option which gave it a private network interface. That created an interface named ve-torrent on the host side and one named host0 in the container. Using ifconfig and route to configure the interface in the container before launching sshd is easy. I haven’t yet determined a good way of configuring the host side of the private network interface automatically.

I had to use a bind for /home because /home is a subvol and therefore doesn’t get included in the container by default.

How it Works

Now when it’s running I can just “ssh -X” to the container and then run graphical programs that use the VPN while at the same time running graphical programs on the main host that don’t use the VPN.

Things To Do

Find out why --network-ipvlan and --network-macvlan don’t work with communication from the same host.

Find out why --network-macvlan gives errors about MAC redirection when pinging.

Determine a good way of setting up the host side after the systemd-nspawn program has run.

Find out if there are better ways of solving this problem, this way works but might not be ideal. Comments welcome.

4 comments to systemd-nspawn and Private Networking

  • kapouer

    Instead of doing a btrfs subtree of root volume, one can simply use
    `–volatile=true –directory=/`
    and then `–bind-ro` various additional directories (/etc, /var/lib/xxx in specific cases).

    It is also a real cool idea to use `–user=`.

    You can even go a step further and add two files:
    /var/lib/machines/yourapp (a symlink of /)
    /etc/systemd/nspawn/yourapp.nspawn (with:)
    Boot = false
    User = xxx
    WorkingDirectory = /home/xxx/Downloads
    Parameters = /usr/bin/something
    Volatile = true
    PrivateUsersChown = false
    BindReadOnly = /etc
    BindReadOnly = /home/xxx/.config
    Bind = /home/xxx/something
    # your config here
    And then start it with
    `machinectl start myapp`

    However this needs to be root.
    You can put xxx in a “machinectl” group and add this polkit rules (in /etc/polkit-1/rules.d/)
    polkit.addRule(function(action, subject) {
    if ( == “org.freedesktop.machine1.manage-machines” && subject.isInGroup(“machinectl”)) {
    return polkit.Result.YES;

    polkit.addRule(function(action, subject) {
    if (( == “org.freedesktop.systemd1.manage-units” || == “org.freedesktop.systemd1.manage-unit-files”) &&
    /^systemd-nspawn\@.*\.service$/.test(action.lookup(“unit”)) &&
    subject.isInGroup(“machinectl”)) {
    return polkit.Result.YES;

  • kapouer: Excellent suggestions, it will take me some time to work through all that.

    Also you are one of the few people reading my blog with IPv6! ;)

  • Anonymous

    French ISP “free” enabled IPv6 by default on their DSLAMS for some time now. That’s so simpler to be aware of IPv6 issues !

  • tonton

    About ipvlan / macvlan / macvtap (almost the same thing) : yes, you can’t reach the host or base namespace, that’s by design.
    I have read somewhere that these interfaces are too low level, they bypass the IP stack.
    Also, the “bridge” mode is misleading: it’s a bridge between the sub-interfaces, but the master interface is excluded …

    I think what you want is the –network-zone= which creates an autoconfigured bridge