Making all network traffic for a Linux user use a specific network interface

I’ve recently been testing out a VPN service, and normally while running the VPN, all internet traffic goes over the VPN interface. This isn’t really ideal, as I only want traffic from specific applications to use the VPN. IPTables doesn’t seem to have the option to filter specific processes, but it can filter based on a specific user account.

IPTables itself doesn’t really deal with routing packets to interfaces, so we can’t use it to directly route packets. We can however mark packets from the user so they can be routed by the ip routing table. I’ve created a script to flush and apply firewall rules, which does what we need ( obviously set the variables at the beginning of the script to match your details ):

#! /bin/bash

export INTERFACE="ppp0"
export VPNUSER="vpnuser"
export LANIP="192.168.1.0/24"
export NETIF="br0"

iptables -F -t nat
iptables -F -t mangle
iptables -F -t filter

# mark packets from $VPNUSER
iptables -t mangle -A PREROUTING ! -s $LANIP  -j MARK --set-mark 0x1
iptables -t mangle -A OUTPUT ! --dest $LANIP  -m owner --uid-owner $VPNUSER -j MARK --set-mark 0x1
iptables -t mangle -A OUTPUT ! --src $LANIP -j MARK --set-mark 0x1

# allow responses
iptables -A INPUT -i $INTERFACE -m conntrack --ctstate ESTABLISHED -j ACCEPT

# allow bittorrent
iptables -A INPUT -i $INTERFACE -p tcp --dport 59560 -j ACCEPT
iptables -A INPUT -i $INTERFACE -p tcp --dport 6443 -j ACCEPT

iptables -A INPUT -i $INTERFACE -p udp --dport 8881 -j ACCEPT
iptables -A INPUT -i $INTERFACE -p udp --dport 7881 -j ACCEPT

# block everything incoming on $INTERFACE
iptables -A INPUT -i $INTERFACE -j REJECT

# send DNS to google for $VPNUSER
iptables -t nat -A OUTPUT --dest $LANIP -p udp --dport 53  -m owner --uid-owner $VPNUSER  -j DNAT --to-destination 8.8.4.4
iptables -t nat -A OUTPUT --dest $LANIP -p tcp --dport 53  -m owner --uid-owner $VPNUSER  -j DNAT --to-destination 8.8.8.8

# allow web interface
iptables -A OUTPUT -p tcp --dport 6443 -m owner --uid-owner $VPNUSER -j ACCEPT

# let $VPNUSER access lo and $INTERFACE
iptables -A OUTPUT -o lo -m owner --uid-owner $VPNUSER -j ACCEPT
iptables -A OUTPUT -o $INTERFACE -m owner --uid-owner $VPNUSER -j ACCEPT

# all packets on $INTERFACE needs to be masqueraded
iptables -t nat -A POSTROUTING -o $INTERFACE -j MASQUERADE

iptables -t mangle -A OUTPUT -p tcp --sport 6443  -m owner --uid-owner $VPNUSER  -j MARK --set-mark 0x2

# reject connections from predator ip going over $NETIF
iptables -A OUTPUT ! --src $LANIP -o $NETIF -j REJECT

Now all packets from the user will be marked for the VPN. Next we need to configure the routing rules for the marked packets:

#! /bin/bash
VPNIF="ppp0"
VPNUSER="vpnuser"
GATEWAYIP=`ifconfig $VPNIF | egrep -o '([0-9]{1,3}\.){3}[0-9]{1,3}' | egrep -v '255|(127\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})' | tail -n1`
if [[ `ip rule list | grep -c 0x1` == 0 ]]; then
ip rule add from all fwmark 0x1 lookup $VPNUSER
fi
ip route replace default via $GATEWAYIP table $VPNUSER 
ip route append default via 127.0.0.1 dev lo table $VPNUSER
ip route flush cache

Now we just need to add the routing table, by adding the table name to the rt_tables file. In Gentoo this file is in /etc/iproute2/rt_tables , other distros will have it in different places, on my machine the file looks like this, the last line being the one added:

#
# reserved values
#
255     local
254     main
253     default
0       unspec
#
# local
#
#1      inr.ruhep
200     vpnuser

Now run the two scripts ( or add the two scripts to run when the network interface starts – this is in /etc/conf.d/net on Gentoo ) , and the specific user should only be able to access traffic on the VPN, and other users on the system should access the network as normal.

This entry was posted in Linux and tagged , , , . Bookmark the permalink.

4 Responses to Making all network traffic for a Linux user use a specific network interface

  1. A says:

    Hi,

    In your script, you are trying to set the mark for all packets which don’t match the Tunnel IP. So won’t this match all packets generated by other users ?
    iptables -t mangle -A OUTPUT ! –src $NETIP -j MARK –set-mark 0×1

    I am trying to do VPN split tunneling, and trying to follow your script. I am facing problem trying to match the last ACK packet, which doesn’t have the UID.

  2. Simon says:

    Hi,

    $NETIP is the LAN ip address of the network card, so if the other users send packets from an ip address which is the VPN’s IP it is sent via the vpn.

    I’ve updated the article with the latest version of my script, hope this helps.

    Simon

  3. Rob says:

    Hi Simon,

    Thanks for this article – I’m now using your techniques to select a network interface by UID.

    I’m curious about the masquerading line. Is there a reason why you don’t restrict this to the target UID, rather then masquerading everything on that interface? Presumably there is an overhead associated with this.

    Cheers,

    Rob

  4. Simon says:

    Hi Rob,

    Glad my article has helped.

    It’s been a while since I wrote the script, but I think it was just put in as an extra safety measure to ensure everything on the interface comes from the correct IP. I don’t know of any reason why it couldn’t match the UID when it does the postrouting rule, but I haven’t tested it. Performance wise, I haven’t noticed any issues masquerading everything, but it might be a worthwhile optimisation to put in.

    Thanks

    Simon

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>