This article documents how the traffic of specific Linux processes can be subjected to a custom firewall or routing configuration, thanks to the magic of cgroups. We will use the Network classifier cgroup, which allows tagging the packets sent by specific processes.

To create the cgroup which will be used to identify the processes I added something like this to /etc/rc.local :

mkdir /sys/fs/cgroup/net_cls/unlocator /bin/echo 42 > /sys/fs/cgroup/net_cls/unlocator/net_cls.classid chown md: /sys/fs/cgroup/net_cls/unlocator/tasks

The tasks file, which controls the membership of processes in a cgroup, is made writeable by my user: this way I can add new processes without becoming root. 42 is the arbitrary class identifier that the kernel will associate with the packets generated by the member processes.

A command like systemd-cgls /sys/fs/cgroup/net_cls/ can be used to explore which processes are in which cgroup.

I use a simple shell wrapper to start a shell or a new program as members of this cgroup:

#!/bin/sh -e CGROUP_NAME=unlocator if [ ! -d /sys/fs/cgroup/net_cls/$CGROUP_NAME/ ]; then echo "The $CGROUP_NAME net_cls cgroup does not exist!" >&2 exit 1 fi /bin/echo $$ > /sys/fs/cgroup/net_cls/$CGROUP_NAME/tasks if [ $# = 0 ]; then exec ${SHELL:-/bin/sh} fi exec "$@"

My first goal is to use a special name server for the DNS queries of some processes, thanks to a second dnsmasq process which acts as a caching forwarder.

/etc/dnsmasq2.conf :

port=5354 listen-address=127.0.0.1 bind-interfaces no-dhcp-interface=* no-hosts no-resolv server=185.37.37.37 server=185.37.37.185

/etc/systemd/system/dnsmasq2.service :

[Unit] Description=dnsmasq - Second instance Requires=network.target [Service] ExecStartPre=/usr/sbin/dnsmasq --test ExecStart=/usr/sbin/dnsmasq --keep-in-foreground --conf-file=/etc/dnsmasq2.conf ExecReload=/bin/kill -HUP $MAINPID PIDFile=/run/dnsmasq/dnsmasq.pid [Install] WantedBy=multi-user.target

Do not forget to enable the new service:

systemctl enable dnsmasq2 systemctl start dnsmasq2

Since the cgroup match extension is not yet available in a released version of iptables, you will first need to build and install it manually:

git clone git://git.netfilter.org/iptables.git cd iptables ./autogen.sh ./configure make -k sudo cp extensions/libxt_cgroup.so /lib/xtables/ sudo chmod -x /lib/xtables/libxt_cgroup.so

The netfilter configuration required is very simple: all DNS traffic from the marked processes is redirected to the port of the local dnsmasq2:

iptables -t nat -A OUTPUT -m cgroup --cgroup 42 -p udp --dport 53 -j REDIRECT --to-ports 5354 iptables -t nat -A OUTPUT -m cgroup --cgroup 42 -p tcp --dport 53 -j REDIRECT --to-ports 5354

For related reasons, I also need to disable IPv6 for these processes:

ip6tables -A OUTPUT -m cgroup --cgroup 42 -j REJECT

I use a different cgroup to force some programs to use my office VPN by first setting a netfilter packet mark on their traffic:

iptables -t mangle -A OUTPUT -m cgroup --cgroup 43 -j MARK --set-mark 43

The packet mark is then used to policy-route this traffic using a dedicate VRF, i.e. routing table 43:

ip rule add fwmark 43 table 43

This VPN VRF just contains a default route for the VPN interface:

ip route add default dev tun0 table 43

Depending on your local configuration it may be a good idea to also add to the VPN VRF the routes of your local interfaces:

ip route show scope link proto kernel \ | xargs -I ROUTE ip route add ROUTE table 43

Since the source address selection happens before the traffic is diverted to the VPN, we also need to source-NAT to the VPN address the marked packets:

iptables -t nat -A POSTROUTING -m mark --mark 43 --out-interface tun0 -j MASQUERADE