VPN, Network isolation and policy routing for apps (like qbittorrent)

Planned to write this like 6 months ago, laziness got me
referenced Routing & Network Namespaces - WireGuard
Essentialy used to use this to run qbittorrent isolated using VPN while still having access to it's webui port

1) Make sure ip forwarding is enabled in kernel
Code:
sysctl -w net.ipv4.ip_forward=1

1.1) Make a network namespace (named vpn here)
Code:
ip netns add vpn

1.2) Add a wireguard type interface (named wg0 here) in the default/init namespace
Code:
ip link add wg0 type wireguard

1.3) Now set the wireguard interface to our own custom namespace (named vpn here), wireguard remembers the originating namespace so it can send UDP packets from there without any additional configuration
Code:
ip link set wg0 netns vpn

1.4) Setup the wg0 interface in the network namespace (note that the config file format for wg is slightly different from wg-quick, just comment out the interface address and dns from it to make it compatible)
10.65.79.144/32 is the local interface address of wg0 here
Code:
ip -n vpn addr add 10.65.79.144/32 dev wg0
ip netns exec vpn wg setconf wg0 /etc/wireguard/switzer.conf
ip -n vpn link set wg0 up

1.5) Now make a table (named 2468 here) and add default interface as wg0 for that table, then have all non-marked/fwmarked (mark 1/0x1 here) trafic use that table
Code:
ip -n vpn route add default dev wg0 table 2468
ip -n vpn rule add not fwmark 1 table 2468

Now to route and connect a port (9000 here) to our local network, essentially bypassing that port from vpn (new outbound connection inside vpn namespace will use the vpn, but established connection can escape)

2.1) Add a virtual eth pair, one in each init and vpn namespace, give then addresses (10.0.0.1/24 for init namespace here, 10.0.0.2/24 for our vpn namespace) and add default route for our main table (this table having lower preference than our 2468 table, only packets having mark/fwmark 0x1 will use this table)
Code:
ip link add veth0 type veth peer name veth1 netns vpn
ip -n vpn addr add 10.0.0.2/24 dev veth1
ip addr add 10.0.0.1/24 dev veth0
ip -n vpn link set dev veth1 up
ip link set dev veth0 up
ip -n vpn route add default via 10.0.0.1 dev veth1

2.2) DNAT desired port (9000 here) to our veth1 IP
Code:
iptables -t nat -A PREROUTING -p tcp --dport 9000 -j DNAT --to 10.0.0.2

2.3)mark desired port (9000 here) with mark/fwmar (0x1 here)
Code:
ip netns exec vpn iptables -A PREROUTING -t mangle -i veth1 -p tcp --dport 9000 -j MARK --set-mark 1
ip netns exec vpn iptables -A PREROUTING -t mangle -m mark --mark 0x1 -j CONNMARK --save-mark
ip netns exec vpn iptables -A OUTPUT -t mangle -j CONNMARK --restore-mark

2.4) run desired app using ip netns exec vpn "command", for example qbittorrent here
Code:
ip netns exec vpn sudo -u rehan qbittorrent-nox --webui-port=9000 -d

extra - to use a different dns for our network namespace, create a different resolvconf at
/etc/netns/vpn/resolv.conf
Code:
mkdir -p /etc/netns/vpn
echo 'nameserver 1.1.1.1' > /etc/netns/vpn/resolv.conf


Now, the qbittorrent will only use wg0 interface i.e. vpn to download but it's webui port (9000) can still be accessed normally, it's just an example of things that can be done, like having multiple vpn (one vpn on top of another one) etc.
reverse proxy like nginx should work normally, try using 10.0.0.2 instead of localhost
should be a much better option than the interface/IP binding option in qbittorrent

NOTE - will clean and format thread properly soon

I really have no idea if anyone finds this helpful, if it is i can have a automated script for easier use
Should i write more things like this?
 
Last edited:
One rather important thing I should have rather added Is that the mangling most probably won't work properly with some VMs (including VPS)

One rather nasty way to bypass this issue is to use static routes in our custom network namespace
 
Last edited:
Forgot to add this simple snippet for docker containers (maybe someone out there will find it useful), essentialy makes it so that docker processes can only communicate using the VPN interface (to outside)

172.16.0.2/32 is the VPN interface IP address
wg0.conf here won't have interface IP and DNS, it's configured manually
image-name is the name of the docker image

Code:
#!/bin/bash
DID=$(docker run -d  --dns="1.1.1.1" --network="none" image-name)
PID=$(docker inspect --format '{{.State.Pid}}' $DID)
ip link add wg0 type wireguard
ip link set wg0 netns $PID
nsenter -t $PID -n ip addr add 172.16.0.2/32 dev wg0
nsenter -t $PID -n wg setconf wg0 /etc/wireguard/wg0.conf
nsenter -t $PID -n ip link set wg0 up
nsenter -t $PID -n ip route add default dev wg0
echo $PID
 

Top