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