Bypass OpenVPN for a specific Unix user

It's sometimes useful to allow a specific Unix user account to bypass an active OpenVPN tunnel. You can achieve this through a combination of firewall rules (using iptables) and routing policy.

Note

These instructions assume you’re using TUN-based routing to connect to the OpenVPN server, and that you’re using the redirect-gateway OpenVPN client option.

Tor usually runs as a non-root user (eg, toranon on Fedora, debian-tor on Debian) and is a good candidate for bypassing the tunnel. Tor will be the subject of this example.

You don’t need to have read the previous article (Force all network traffic through OpenVPN using iptables). However, if you’re using the iptables rules from the previous article, add this rule to the filter section before you continue.

*filter
... snipped ...
# Allow packets originating from the Tor user to travel into the wild abyss,
# the womb of nature! Or (more likely) travel outside of the OpenVPN tunnel.
-A OUTPUT -m owner --uid-owner toranon -j ACCEPT

Create a new routing table

When the OpenVPN tunnel is active, the default gateway is the TUN interface.

# ip route show table main | grep ^default
default via 10.8.0.5 dev tun0

Before activating the tunnel, use ip route to determine the IP address of the current (non-VPN) default gateway. In this example, the default gateway is a local wireless router with the IP address 192.168.0.1, and the default interface for outbound traffic is wlp2s0 (a wireless card).

# ip route show table main | grep ^default
default via 192.168.0.1 dev wlp2s0

# ip route show table main | awk '$1=="default" {print $3}'
192.168.0.1

By default, the kernel only uses the main table when calculating routes. Create a new routing table 10 with a default route that points to your local wireless router. The table name can be any integer in the range 1 to 2^32 (except the reserved integers 253, 254, and 255).

# ip route add default via 192.168.0.1 table 10
# ip route flush cache

Check that the default route in table 10 matches the existing default route in the main table.

# ip route show table main | grep ^default
default via 192.168.0.1 dev wlp2s0

# ip route show table 10
default via 192.168.0.1 dev wlp2s0

Add a Netfilter mark value to user packets

Routing tables have no concept of Unix user accounts; they cannot know which network packets came from the Tor user unless the packets are marked. In the iptables mangle section, mark the Tor user’s network packets with 0xa. The mark can be any base 16 value.

*mangle
... snipped ...
-A OUTPUT -m owner --uid-owner toranon -j MARK --set-mark 0xa
COMMIT

The command to append this rule:

# iptables -t mangle -A OUTPUT -m owner --uid-owner toranon \
      -j MARK --set-mark 0xa

Add a routing policy rule

Currently, routing table 10 isn’t used for anything. Let's change that! Create a routing policy rule that selects packets marked 0xa and points them to routing table 10.

# ip rule add fwmark 0xa table 10

Packets that originate from the Tor user are now re-routed according to the route in table 10. In this example, the packets start off routed towards tun0 (due to the default route in the main table), but are marked and then re-routed towards wlp2s0 (due to the default route in table 10).

Fix the source address

Packets that originate from the Tor user start off routed towards tun0 and so their source address is the IP address assigned to the TUN interface (usually 10.8.X.X). When the Tor user initiates a remote connection, the return packet comes back on the correct interface (wlp2s0) but with the wrong destination (10.8.X.X). The Tor user never receives these packets.

The solution is to fix the source address of packets that originate from the Tor user. Use ip addr to determine the IP address assigned to the default interface (eg, wlp2s0).

# ip addr show dev wlp2s0
wlp2s0:
 inet 192.168.0.6/24 brd 192.168.0.255 scope global dynamic wlp2s0

# ip addr show dev wlp2s0 | awk '$1=="inet" {match($2,/[0-9.]+/,i); print i[0]}'
192.168.0.6

In the iptables nat section, match packets marked with 0xa and change the source address (eg, to 192.168.0.6).

*nat
... snipped ...
-A POSTROUTING -o wlp2s0 -m mark --mark 0xa -j SNAT --to-source 192.168.0.6
COMMIT

The command to append this rule:

# iptables -t nat -A POSTROUTING -o wlp2s0 -m mark --mark 0xa \
      -j SNAT --to-source 192.168.0.6

Unicast reverse path forwarding

We’re not quite out of the woods yet!

Most Linux distributions enable unicast reverse path forwarding (uRPF) in strict mode (see rp_filter=1 in the sysctl documentation). uRPF is used to protect servers against IP spoofing, which is commonly used in denial-of-service attacks. (For a desktop or laptop that isn’t accepting arbitrary incoming network connections, it isn’t as beneficial.)

# sysctl -n net.ipv4.conf.wlp2s0.rp_filter
1

This is problematic. In strict mode, an incoming packet must be received on the same network interface that would be used to forward the return packet. An incoming packet that doesn’t satisfy this check is dropped. Importantly, this check happens before any packet re-routing.

To demonstrate why this is problematic, consider this situation: OpenVPN is active and an incoming packet intended for the Tor user is received by wlp2s0. Before passing this packet to the Tor user, uRPF determines what interface would be used to forward the return packet in this connection; unfortunately, the default route states that this would be tun0. Although the return packet would actually be re-routed to wlp2s0 before it leaves, uRPF doesn't care and drops the incoming packet.

To solve this, switch uRPF to loose mode (rp_filter=2) on the interface that Tor needs to communicate through (eg, wlp2s0). Loose mode permits incoming packets whose source address is reachable on a path through any interface, even if the interfaces don’t match.

# sysctl -w net.ipv4.conf.wlp2s0.rp_filter=2

Tor no longer cares whether your OpenVPN tunnel is present or not.