Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save rohan-molloy/35d5ccf03e4e6cbd03c3c45528775ab3 to your computer and use it in GitHub Desktop.
Save rohan-molloy/35d5ccf03e4e6cbd03c3c45528775ab3 to your computer and use it in GitHub Desktop.
This tutorial will look at how network namespaces can be defined in systemd service unit definitions

Network Isolation of Services with Systemd

Network namespaces are an important component of containerization in Linux. A network namespace (netns) allows a running process to see different network interfaces, routes, and firewall rules from the rest of the system. There are a number of use cases for network namespaces, such as running multiple servers, testing/staging environments and providing isolation of services.

Creating a Network Namepsace

We begin by creating a generic systemd service for creating a named network namespace. I add the following to /usr/lib/systemd/system/netns@.service. In systemd, the @ mean the service takes a parameter which is passed to the unit via %i. E.g, we can run sudo systemctl start netns@webserver.service.

[Unit]
Description=Named network namespace %i
StopWhenUnneeded=true

[Service]
Type=oneshot
RemainAfterExit=yes

# Ask systemd to create a network namespace
PrivateNetwork=yes

# Ask ip netns to create a named network namespace
# (This ensures that things like /var/run/netns are properly setup)
ExecStart=/sbin/ip netns add %i

# Drop the network namespace that ip netns just created
ExecStart=/bin/umount /var/run/netns/%i

# Re-use the same name for the network namespace that systemd put us in
ExecStart=/bin/mount --bind /proc/self/ns/net /var/run/netns/%i

# Clean up the name when we are done with the network namespace
ExecStop=/sbin/ip netns delete %i

Unit to set up the interface

Next, we create a definition in /usr/lib/systemd/system/attach-enp3s1f0@.service. This service associates the enp3s1f0 with a specified network namespace. It also sets up addresses within the network namespace. In iproute2, the command to run a process within a specified network namespace is ip netns exec $namespace $command. Note that ADDRESS=192.168.0.80, BROACAST=192.168.0.255 and GATEWAY=192.168.0.1 of the interface are set in an /etc/enp3s1f0.conf file.

[Unit]
Description=Attach enp3s1f0 to Named network namespace %i
Requires=netns@%i.service
After=netns@%i.service

[Service]
Type=oneshot
RemainAfterExit=yes

# Environment File containing address and gateway definitions
EnvironmentFile=/etc/enp3s1f0.conf

# Associate the enp3s1f0 NIC with the network namespace
ExecStart=/usr/sbin/ip link set enp3s1f0 netns %i

# Run iproute2 (inside the netns) to bring the NIC up
ExecStart=/usr/sbin/ip netns exec %i ip l set up dev enp3s1f0

# Run iproute2 (inside the netns) to add the address/gateway
ExecStart=/usr/sbin/ip netns exec %i ip a add $ADDRESS broadcast $BROADCAST dev enp3s1f0
ExecStart=/usr/sbin/ip netns exec %i ip r add default via $GATEWAY dev enp3s1f0

# Run iproute2 (inside the netns) to bring the NIC down (on stop)
ExecStop=/usr/sbin/ip netns exec %i ip l set down dev enp3s1f0

Running a process inside a network namespace

As a simple test, I define /usr/lib/systemd/system/webserver.service which runs a simple TCP server over netcat inside the netns. Notice the JoinsNamespaceOf=netns@webserver.service option, stating that the service join the network namespace of an already running service. I added some extra privilege constraint such as ProtectSystem=true and CapabilityBoundingSet= which are unrelated to network namespaces - these are for another post :)

[Unit]
Description=Example Systemd Service running in Netns

# Require the network namespace is set up
Requires=netns@webserver.service
After=netns@webserver.service
JoinsNamespaceOf=netns@webserver.service

# Require the interface is set up
Requires=attach-enp3s1f0@webserver.service
After=attach-enp3s1f0@webserver.service

[Service]
Type=simple
RemainAfterStart=true

# Run the process in a non-default netns
PrivateNetwork=true

# Add additional limitation on privileges
# (this is unrelated to network namespaces)
ProtectSystem=true
CapabilityBoundingSet=
User=nobody
Group=nobody
PrivateTmp=true

# Start the netcat health check service on port 8080
ExecStart=/usr/bin/nc --send-only --exec "/usr/bin/echo OK" -lkp 8080

[Install]
WantedBy=multi-user.target

Trying it out

After setting the above files, I run the following commands

sudo systemctl start \
netns@webserver.service \
attach-enp3s1f0@webserver.service \
webserver.service

Now the enp3s1f0 interface has disappeared from the system, as it's no longer in the default namespace

$ sudo ip link show dev enp3s1f0
Device "enp3s1f0" does not exist.

Let's see if its reachable

$ curl http://192.168.0.80:8080
OK

When a service is running in an alternate network namespace, it is possible to use the service's port on the host system, over all interfaces.

$ /usr/bin/nc --send-only --exec "/usr/bin/echo Foo" -lkp 8080 &
[1] 8556
$ curl http://0.0.0.0:8080
Foo
$ curl http://192.168.0.80:8080
OK

Services running in an alternate network namespace are unaffected by local firewall rules on the host system.

$ sudo iptables -t raw -I PREROUTING -p tcp --dport 8080 -j DROP
$ curl --max-time=3 http://0.0.0.0:8080
curl: (28) Connection timed out after 3001 milliseconds
$ curl http://192.168.0.80:8080

Associating the nginx service with the network namespace

I add PrivateNetworking=true to the [Service] section and the following lines to the [Unit] section of /usr/lib/systemd/system/nginx.service

Requires=webserver.service
After=webserver.service
JoinsNamespaceOf=netns@webserver.service

I then run sudo systemctl daemon-reload; sudo systemctl start nginx.service.

Let's test it out

$ curl http://192.168.0.80
<!doctype html>
<html>
 <head>
  <title>Nginx Server</title>
 </head>
 <body>
  <h1>It Works!</h1>
 </body>
</html>

Conclusion

A couple of final things to note. Network namespaces are a form of specific isolation - they only concern networking, not filesystems, user rights, etc. These partial isolation systems can be joined to form general isolation, this is how Docker and containerd operate. Systemd and the related technologies on a modern Linux system are extremely powerful and there's a lot of isolation that can be done within systemd service definitions, such as privilege and capability dropping, which will be covered in more detail in a later post.

Standalone use of network namespaces can be useful for situations when the only form of isolation required is on the networking side. For example, running a service (such as the transmission-daemon bittorrent) client over a VPN, without requiring a VPN on the rest of the system.

I hope you found this post informative.

[Unit]
Description=Attach enp3s1f0 to Named network namespace %i
Requires=netns@%i.service
After=netns@%i.service
[Service]
Type=oneshot
RemainAfterExit=yes
# Environment File containing address and gateway definitions
EnvironmentFile=/etc/enp3s1f0.conf
# Associate the enp3s1f0 NIC with the network namespace
ExecStart=/usr/sbin/ip link set enp3s1f0 netns %i
# Run iproute2 (inside the netns) to bring the NIC up
ExecStart=/usr/sbin/ip netns exec %i ip l set up dev enp3s1f0
# Run iproute2 (inside the netns) to add the address/gateway
ExecStart=/usr/sbin/ip netns exec %i ip a add $ADDRESS broadcast $BROADCAST dev enp3s1f0
ExecStart=/usr/sbin/ip netns exec %i ip r add default via $GATEWAY dev enp3s1f0
# Run iproute2 (inside the netns) to bring the NIC down (on stop)
ExecStop=/usr/sbin/ip netns exec %i ip l set down dev enp3s1f0
[Unit]
Description=Named network namespace %i
StopWhenUnneeded=true
[Service]
Type=oneshot
RemainAfterExit=yes
# Ask systemd to create a network namespace
PrivateNetwork=yes
# Ask ip netns to create a named network namespace
# (This ensures that things like /var/run/netns are properly setup)
ExecStart=/sbin/ip netns add %i
# Drop the network namespace that ip netns just created
ExecStart=/bin/umount /var/run/netns/%i
# Re-use the same name for the network namespace that systemd put us in
ExecStart=/bin/mount --bind /proc/self/ns/net /var/run/netns/%i
# Clean up the name when we are done with the network namespace
ExecStop=/sbin/ip netns delete %i
[Unit]
Description=Example Systemd Service running in Netns
# Require the network namespace is set up
Requires=netns@webserver.service
After=netns@webserver.service
JoinsNamespaceOf=netns@webserver.service
# Require the interface is set up
Requires=attach-enp3s1f0@webserver.service
After=attach-enp3s1f0@webserver.service
[Service]
Type=simple
RemainAfterStart=true
# Run the process in a non-default netns
PrivateNetwork=true
# Add additional limitation on privileges
# (this is unrelated to network namespaces)
ProtectSystem=true
CapabilityBoundingSet=
User=nobody
Group=nobody
PrivateTmp=true
# Start the netcat health check service on port 8080
ExecStart=/usr/bin/nc --send-only --exec "/usr/bin/echo OK" -lkp 8080
[Install]
WantedBy=multi-user.target
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment