TL;DR: Serious application-level network filtering on Linux is possible using netfilter NFQUEUE. However, NFQUEUE is a low-level facility for developers. Wait until firewall solutions are created that provide abstraction through a user-friendly interface.

Update 14.02.2019: systemd is using cgroupsv2 in combination with bpf to enable per systemd-unit firewalling:

Application-level network filtering using iptables

The “owner” module

The owner module allows to filter outgoing network traffic:

iptables -A OUTPUT -m owner --uid-owner 0 -j LOG
iptables -A OUTPUT -m owner --uid-owner 0 -j DROP

Use cases:

  • log all traffic caused by root user
  • prohibit or restrict network access of certain applications

More infos:

iptables -m owner --help
man iptables-extensions

⚠️ The owner module can only filter outgoing traffic originating from a specific uid or gid. It cannot filter based on other attributes, such as pid, path to executable, hash of executable and so on.

Example 1 - PoC

  • Download busybox' ping utility: curl -O -L https://www.busybox.net/downloads/binaries/1.30.0-i686/busybox_PING
  • Make it executable: chmod +x busybox_PING
  • Try to run it as user:
vagrant@lx-box ~ $ ./busybox_PING -w 3 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
ping: permission denied (are you root?) man capabilities
  • The missing kernel capability is CAP_NET_RAW.
  • Add the missing capability to the downloaded executable:
root@lx-box ~ # getcap /home/vagrant/busybox_PING
root@lx-box ~ # setcap cap_net_raw+ep /home/vagrant/busybox_PING
root@lx-box ~ # getcap /home/vagrant/busybox_PING
/home/vagrant/busybox_PING = cap_net_raw+ep
  • Try to run ping again:
vagrant@lx-box ~ $ ./busybox_PING -w 3 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=63 time=5.827 ms
^C
--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 5.827/5.827/5.827 ms
vagrant@lx-box ~ $
  • Now that everything has been prepared, let’s test the iptables owner module.
  • Make sure the iptables chains are empty and configured with ACCEPT policies:
root@lx-box ~ # iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
  • Next, deny outgoing ICMP packets system-wide:
iptables -A OUTPUT -p icmp --icmp-type 8 -m state --state NEW,ESTABLISHED,RELATED -j REJECT
iptables -A INPUT -p icmp --icmp-type 0 -m state --state ESTABLISHED,RELATED -j REJECT

… which results in:

root@lx-box ~ # iptables -L -n
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
REJECT     icmp --  0.0.0.0/0            0.0.0.0/0            icmptype 0 state RELATED,ESTABLISHED reject-with icmp-port-unreachable

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
REJECT     icmp --  0.0.0.0/0            0.0.0.0/0            icmptype 8 state NEW,RELATED,ESTABLISHED reject-with icmp-port-unreachable
  • Ping is now prohibited via iptables:
vagrant@lx-box ~ $ ./busybox_PING -w 3 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
ping: sendto: Operation not permitted
  • Now, allow ping for user vagrant using iptables:
iptables -I OUTPUT -p icmp --icmp-type 8 -m owner --uid-owner vagrant -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
iptables -I INPUT -p icmp --icmp-type 0 -m state --state ESTABLISHED,RELATED -j ACCEPT

⚠️ The owner module, of course, only works for outgoing packets, because iptables only knows the pid/gid of local applications. For incoming packets (e.g. ping answer from a foreign system), ICMP traffic must be allowed system-wide. However, only ESTABLISHED/RELATED connections are allowed, no NEW connections, which reduces exposure.

  • Check iptables rules and note that pkts count for ICMP is 0:
root@lx-box ~ # iptables -L -n -v
Chain INPUT (policy ACCEPT 284 packets, 14172 bytes)
  pkts bytes target     prot opt in     out     source               destination
    0     0 ACCEPT     icmp --  *      *       0.0.0.0/0            0.0.0.0/0            icmptype 0 state RELATED,ESTABLISHED
    0     0 REJECT     icmp --  *      *       0.0.0.0/0            0.0.0.0/0            icmptype 0 state RELATED,ESTABLISHED reject-with icmp-port-unreachable

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
  pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 210 packets, 19868 bytes)
  pkts bytes target     prot opt in     out     source               destination
    0     0 ACCEPT     icmp --  *      *       0.0.0.0/0            0.0.0.0/0            icmptype 8 owner UID match 1000 state NEW,RELATED,ESTABLISHED
    0     0 ACCEPT     icmp --  *      *       0.0.0.0/0            0.0.0.0/0            icmptype 8 owner UID match 1000 state NEW,RELATED,ESTABLISHED
    1    84 REJECT     icmp --  *      *       0.0.0.0/0            0.0.0.0/0            icmptype 8 state NEW,RELATED,ESTABLISHED reject-with icmp-port-unreachable
  • Check if ping works, by sending 2 packets:
vagrant@lx-box ~ $ ./busybox_PING -w 3 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=63 time=12.635 ms
64 bytes from 8.8.8.8: seq=1 ttl=63 time=5.352 ms
^C
--- 8.8.8.8 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 5.352/8.993/12.635 ms
vagrant@lx-box ~ $

It works 👍.

  • Again, check iptables rules and verify if our “-m owner"- rule was actually used:
root@lx-box ~ # iptables -L -n -v
Chain INPUT (policy ACCEPT 771 packets, 39604 bytes)
pkts bytes target     prot opt in     out     source               destination
  2   168 ACCEPT     icmp --  *      *       0.0.0.0/0            0.0.0.0/0            icmptype 0 state RELATED,ESTABLISHED
  0     0 REJECT     icmp --  *      *       0.0.0.0/0            0.0.0.0/0            icmptype 0 state RELATED,ESTABLISHED reject-with icmp-port-unreachable

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 543 packets, 50832 bytes)
pkts bytes target     prot opt in     out     source               destination
  2   168 ACCEPT     icmp --  *      *       0.0.0.0/0            0.0.0.0/0            icmptype 8 owner UID match 1000 state NEW,RELATED,ESTABLISHED
  0     0 ACCEPT     icmp --  *      *       0.0.0.0/0            0.0.0.0/0            icmptype 8 owner UID match 1000 state NEW,RELATED,ESTABLISHED
  1    84 REJECT     icmp --  *      *       0.0.0.0/0            0.0.0.0/0            icmptype 8 state NEW,RELATED,ESTABLISHED reject-with icmp-port-unreachable

Example 2 - Application-level network filtering based on group ids (gid)

Create user groups for specific applications and associate iptables rules with them (still easier than dealing with SELinux… 😏 ):

groupadd noinet
usermod -G noinet YOUR_USER # newgrp noinet

# place rule at top of OUTPUT chain
iptables -I OUTPUT 1 -m owner --gid-owner noinet -j REJECT

sg noinet -c "firefox"
[FF won't be able to connect to inet]

firefox
[FF will be able to connect to inet]

# remove iptables rule
iptables -L --line-numbers
iptables -D OUTPUT 1 

Filtering network access per application using SELinux is described here: http://blog.siphos.be/2015/08/filtering-network-access-per-application/. Drawbacks:

  • iptables rules & SELinux policies must be created.
  • This approach requires correct file system labeling (SELinux security attribute) of all applications, which is not the case with most Linux distributions that ship default SELinux policies.

Desktop system firewalls with application-level network filtering features

Two projects under active development, still in early stages:

They are both making use of NFQUEUE (https://home.regit.org/netfilter-en/using-nfqueue-and-libnetfilter_queue/). NFQUEUE allows to delegate packet filtering decision making to a user-space software.

Application-level network filtering using other methods

Unshare

Take away net cap from Firefox, rendering it unusable:

root@lx-box ~ # ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=63 time=47.5 ms
^C
--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 47.568/47.568/47.568/0.000 ms
root@lx-box ~ # unshare -n ping 8.8.8.8
connect: Network is unreachable