Selective routing on Linux

Sometime it’s needed to selectively route specified IPs or networks via different interface – i.e. if you want to route private addresses over VPN (a.k.a split tunnel routing) or to route some public IPs over VPN to unblock some nationally restricted sites (Netflix). Here are simple scripts to achieve this.

PPP up script

This is PPP up script, it routes and NATs whole traffic of user polipo over PPP tunnel to public IP. Simply put it it /etc/ppp/ip-up.d and change config values.
To forward additional owners/ips/networks over tunnel simply create a appropriate rule in iptables mangle table that will mark packets with given $TABLE_MARK.

#!/bin/bash
# ppp ip-up.d script

# This script is called with the following arguments:
# Arg Name
# $1 Interface name
# $2 The tty
# $3 The link speed
# $4 Local IP number
# $5 Peer IP number
# $6 Optional ``ipparam'' value foo

# config
COMMENT="polipo"; # used for identifying our rules
TABLE="Tproxy";   # iproute2 table as define in /etc/iproute2/rt_tables
TABLE_MARK="102"; # unique mark, using table id
TAG="$6";         # tag for syslog entry
MATCH_OWNER="polipo"; # restricted user

ARG_DEV="$1";
ARG_LOCAL_IP="$4";
ARG_REMOTE_IP="$5";

logger -t "$TAG" "Setting up $COMMENT tunnel over $ARG_DEV";

# disable rp_filter
sysctl -w net.ipv4.conf.$ARG_DEV.rp_filter=0;

ip route flush table "$TABLE"
ip route add default via "$ARG_REMOTE_IP" dev "$ARG_DEV" table "$TABLE"

# delete existing entries
iptables -t mangle -L OUTPUT --line-numbers | grep "$COMMENT" | awk '{print $1}' | sort -n -r | xargs -n 1 /sbin/iptables -t mangle -D OUTPUT;

# forward only specified owner
if [ -n "$MATCH_OWNER" ];
then
      iptables -t mangle -A OUTPUT -m owner --uid "$MATCH_OWNER" -j MARK --set-mark "$TABLE_MARK" -m comment --comment "$COMMENT";
fi

# forward also this this SUBNET over tunnel
#iptables -t mangle -A OUTPUT -d 173.194.0.0/16 -j MARK --set-mark "$TABLE_MARK" -m comment --comment "$COMMENT";

# NAT
iptables -t nat -L POSTROUTING --line-numbers | grep "$COMMENT" | awk '{print $1}' | sort -n -r | xargs -r -n 1 /sbin/iptables -t nat -D POSTROUTING;
iptables -t nat -A POSTROUTING -o "$ARG_DEV" -j SNAT --to-source "$ARG_LOCAL_IP" -m comment --comment "$COMMENT";

# flush rules and add new
ip rule | grep "$TABLE" | sed -e 's/.*:/ip rule del/g' | bash;
ip rule add fwmark "$TABLE_MARK" pri 100 lookup "$TABLE";
ip rule add from "$ARG_LOCAL_IP" pri 200 table "$TABLE";
ip route flush cache;

OpenVPN up script

This is OpenVPN up script, it routes and NATs traffic with tunnel source ip via VPN.

#!/bin/bash
# openvpn up script, hook with 'up' directive in openvpn config

# This script is called with the following arguments:
# Arg Name
## $1 tun_dev
## $2 tun_mtu
## $3 link_mtu
## $4 ifconfig_local_ip
## $5 ifconfig_remote_ip
## $6 [ init | restart ].

COMMENT="vpn1";   # used for identifying our rules
TABLE="Tvpn1";    # iproute2 table as define in /etc/iproute2/rt_tables
TABLE_MARK="101"; # unique mark, using table id here
TAG="$TABLE";     # tag for syslog entry

ARG_DEV="$1";
ARG_LOCAL_IP="$4";
ARG_REMOTE_IP="$5";

logger -t "$TAG" "Setting up $COMMENT tunnel over $ARG_DEV";

# disable rp_filter
sysctl -w net.ipv4.conf.$ARG_DEV.rp_filter=0;

ip route flush table "$TABLE"
ip route add default via "$ARG_REMOTE_IP" dev "$ARG_DEV" table "$TABLE";

# NAT
iptables -t nat -L POSTROUTING --line-numbers | grep "$COMMENT" | awk '{print $1}' | sort -n -r | xargs -r -n 1 /sbin/iptables -t nat -D POSTROUTING;
iptables -t nat -A POSTROUTING -o "$ARG_DEV" -j SNAT --to-source "$ARG_LOCAL_IP" -m comment --comment "$COMMENT";

# flush rules and add new
ip rule | grep "$TABLE" | sed -e 's/.*:/ip rule del/g' | bash;
ip rule add fwmark "$TABLE_MARK" pri 100 lookup "$TABLE";
ip rule add from "$ARG_LOCAL_IP" pri 200 table "$TABLE";
ip route flush cache;