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.


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.
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
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
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