Routing specific destinations via vpn

Recently I stumbled upon a curious case where traffic for a big video content provider should be routed via a VPN. The requirement was that only traffic from a certain executable, for specific domains (used by that content provider) should be routed via the VPN, and the rest of the traffic should not be affected.

I have done similar things before with cgroups earlier and together with chatgpt I came up with a solution which I think is worth taking note of for similar upcoming tasks.

In this example we are using OpenVPN and taking advantage of the “up” and “down” hooks from OpenVPN:

script-security 2
up /etc/openvpn/client/videoprovider-up.sh
down /etc/openvpn/client/videoprovider-down.sh

ls -l /etc/openvpn/client/
-rw-r----- 1 jonas jonas 20 Sep 30 09:41 auth.txt
lrwxrwxrwx 1 root root 28 Sep 30 17:42 vpn.conf -> /etc/openvpn/client/vpn.ovpn
-rw-r--r-- 1 jonas jonas 4685 Oct 1 19:50 vpn.ovpn
-rwxr-xr-x 1 root root 756 Sep 30 17:32 videoprovider-down.sh
-rwxr-xr-x 1 root root 1421 Sep 30 17:37 videoprovider-refresh.sh
-rwxr-xr-x 1 root root 126 Sep 30 17:01 videoprovider-up.sh

For completeness I list the content of the scripts.

videoprovider-up.sh 
#!/bin/bash
# Start the VideoProvider refresher script in background
REFRESHER="/etc/openvpn/client/videoprovider-refresh.sh"
LOGFILE="/var/log/videoprovider-refresh.log"

# Launch in background, disown so it survives OpenVPN script environment
nohup "$REFRESHER" >> "$LOGFILE" 2>&1 &
disown

videoprovider-refresh.sh
#!/bin/bash
# VideoProvider VPN dynamic route refresher using systemd slice

VPN_IFACE="tun0"
VIDEOPROVIDER_TABLE="videoprovidervpn"
KODI_CGROUP="/system.slice/kodi.service"
TABLE_ID=10
LOCKFILE="/run/videoprovider-refresh.lock"
DOMAINS=(
    "videoprovider.com"
    "www.videoprovider.com"
    "api.videoprovider.com"
)
INTERVAL=300

# --- Prevent multiple loops ---
if [ -e "$LOCKFILE" ] && kill -0 "$(cat "$LOCKFILE")" 2>/dev/null; then
    echo "Refresher already running (PID $(cat $LOCKFILE))"
    exit 0
fi

echo $$ > "$LOCKFILE"
trap "rm -f $LOCKFILE" EXIT

# --- Ensure routing table exists ---
grep -q "$VIDEOPROVIDER_TABLE" /etc/iproute2/rt_tables || echo "10 $VIDEOPROVIDER_TABLE" >> /etc/iproute2/rt_tables

# --- Add iptables mark for Kodi cgroup ---
iptables -t mangle -C OUTPUT -m cgroup --path "/sys/fs/cgroup$KODI_CGROUP" -j MARK --set-mark $TABLE_ID 2>/dev/null || \
    iptables -t mangle -A OUTPUT -m cgroup --path "/sys/fs/cgroup$KODI_CGROUP" -j MARK --set-mark $TABLE_ID

# --- Main loop ---
while true; do
    # Flush old rules
    ip rule show | grep "$VIDEOPROVIDER_TABLE" | while read -r rule; do
        ip rule del ${rule#0: }
    done

    ip route flush table "$VIDEOPROVIDER_TABLE" 2>/dev/null
    ip route add default dev "$VPN_IFACE" table "$VIDEOPROVIDER_TABLE"

    # Resolve VideoProvider domains and add rules
    for domain in "${DOMAINS[@]}"; do
        for ip in $(getent ahosts "$domain" | awk '{print $1}' | sort -u); do
            ip rule add to "$ip" table "$VIDEOPROVIDER_TABLE" 2>/dev/null || true
        done
    done

    # Apply fwmark
    ip rule add fwmark $TABLE_ID table "$VIDEOPROVIDER_TABLE" 2>/dev/null || true

    sleep "$INTERVAL"
done

videoprovider-down.sh
#!/bin/bash
# Stop VideoProvider VPN routing and refresher loop

VIDEOPROVIDER_TABLE="videoprovidervpn"
KODI_CGROUP="/system.slice/kodi.service"
TABLE_ID=10
LOCKFILE="/run/videoprovider-refresh.lock"

# --- Kill refresher if running ---
if [ -e "$LOCKFILE" ]; then
    PID=$(cat "$LOCKFILE")
    if kill -0 "$PID" 2>/dev/null; then
        kill "$PID"
        sleep 1
        kill -9 "$PID" 2>/dev/null || true
    fi
    rm -f "$LOCKFILE"
fi

# --- Flush ip rules ---
for prio in $(ip rule show | awk -v t="$VIDEOPROVIDER_TABLE" '$0 ~ t {print $1}'); do
    ip rule del pref "$prio"
done

# --- Flush routing table ---
ip route flush table "$VIDEOPROVIDER_TABLE" 2>/dev/null

# --- Remove iptables rule ---
iptables -t mangle -D OUTPUT -m cgroup --path "/sys/fs/cgroup$KODI_CGROUP" -j MARK --set-mark $TABLE_ID 2>/dev/null || true
 
This entry was posted in datorer, linux and tagged , , . Bookmark the permalink.

Leave a Reply

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