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="tun0"
export VPNUSER="vpnuser"
export LANIP=""
export NETIF="br0"

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

# mark packets from $VPNUSER
iptables -t mangle -A OUTPUT ! --dest $LANIP  -m owner --uid-owner $VPNUSER -j MARK --set-mark 0x1
iptables -t mangle -A OUTPUT --dest $LANIP -p udp --dport 53 -m owner --uid-owner $VPNUSER -j MARK --set-mark 0x1
iptables -t mangle -A OUTPUT --dest $LANIP -p tcp --dport 53 -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

# 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
iptables -t nat -A OUTPUT --dest $LANIP -p tcp --dport 53  -m owner --uid-owner $VPNUSER  -j DNAT --to-destination

# 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

# 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. We also need to add a 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

Next we need a script to configure the routing rules for the marked packets:

#! /bin/bash
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
ip route replace default via $GATEWAYIP table $VPNUSER 
ip route append default via dev lo table $VPNUSER
ip route flush cache

If you are using OpenVPN, you will need to ensure this line is in your config file, to prevent all traffic from sending over the VPN by default:


You may also need to add these lines into /etc/sysctl.d/9999-vpn.conf to ensure the kernel lets the traffic get routed correctly ( this disables reverse path filtering ):

net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.br0.rp_filter = 0

Then run:

sysctl -p

To apply the new sysctl rules. You may also need to restart your VPN if you are already connected.

Now run the two scripts ( the second script needs to run when the network interface starts – this is in /etc/conf.d/net on Gentoo, or the ‘up’ command in OpenVPN’s config file ) , 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.

57 thoughts on “Making all network traffic for a Linux user use a specific network interface”

  1. I tryed to setup your script. It works with ip adresses. But if i want to connect to hostnames it times out ?

    Merry Christmas 🙂

  2. This is going to be the most noobish question asked yet..

    If I have a system right now running Ubuntu desktop with everything already set up and my goal is to route my rtorrent traffic through the vpnuser account how do I go about that after using these scripts ?

    For example, I have this system setup and login under user: main Now I want to add these scripts withe the flags to make user: vpnuser be the only user that has their traffic routed through the VPN. What do I have to change in my system to make rtorrent be flagged and run as: vpnuser ?

    I realize this must be really obvious, but I have no idea how to do this.

  3. Hello,
    Please can someone explain me on details this line:
    iptables -t mangle -A OUTPUT ! –src $LANIP -j MARK –set-mark 0x1

    Why do you specify a source address for an output packet ?
    What is this source address exactly?


  4. Following the guide everything works as should but I can’t access Transmisison webui on local network (port 9091). Is there anybody who got this working and can access port 9091? I’m not speaking about the WAN side access, but from local network.

  5. Didn’t work for me on Ubuntu 14.04.4. My vpnuser cant route any traffic. Other user can still route fine. Not sure where / how to start looking.

  6. Is there any way to get this working without setting rp_filter off ? As suggested in this post ( ) it may not be the best idea to turn off reverse path filtering. This guy suggest the same can be achieved with saving and restoring CONNMARK values but I don’t know how to implement here:

    # keep that rule
    OUTPUT -m owner …. -j MARK 0x3
    # add this one after the previous one: it saves the current mark into connmark
    OUTPUT -j CONNMARK –save-mark

    # and add this one (in mangle), which sets the mark to the connmark
    # if conntrack determines that it is from the same connection.
    PREROUTING -j CONNMARK –restore-mark

Leave a Reply

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