Howto: run a single application behind a VPN on linux

VPNs have lots of benefits like privacy and geo-diverse egress, but also can have worse latency, throughput and be blocked by some sites. So it’s natural that you might want a VPN for some of your internet usage, but not all. I found I wanted to run one application behind a VPN but not others, and my OS didn’t provide an easy way to do this. This post documents my solution.

Prerequisites

This guide is for Debian 12 (Bookworm) using NetworkManager. It will likely work on any NetworkManager system, and the core concepts can be used on any Linux system.

This guide assumes you already have a VPN set up in NetworkManager with name us-sea-wg-101 and interface wg1.

How it works

Linux routing and firewalls don’t allow you to control traffic based on the application, just networking constructs packet information and interfaces. To make traffic from the target application distinguished from other traffic, we put it in a network namespace, with it’s own IP address, and bridge it with a virtual device. At this point traffic from the application appears as inbound traffic and we can route and NAT it outbound however we choose.

%%{init: {'theme':'neutral'}}%%
graph LR
  subgraph Computer
    subgraph Namespace
      VpnApp[Application]
    end
    subgraph routing
      special_route[from 10.42.0.1/24]
      default_route[Default Route]
    end
    VpnApp--virtual ethernet-->special_route--NAT-->wg1
    NormalApp[Normal Applications]-->default_route-->eth0
  end
  wg1-->VPN
  eth0-->Internet

Step by step guide

Step 1: Create network startup script

NetworkManager doesn’t natively have options to manage network namespaces or NAT, so we do this with a script that runs when the relevant device comes up.

#!/bin/sh
# Save in /etc/NetworkManager/dispatcher.d/10-netns

# Only interested in virt0 up
if [ "$1" != "virt0" ]; then
    exit 0;
fi

if [ "$2" != "up" ]; then
    exit 0;
fi

# Move virt1 to ns1 Bring it up and configure an IP address
ip netns add ns1 || true
ip link set virt1 netns ns1
ip netns exec ns1 ip link set virt1 up
ip netns exec ns1 ip addr add fd00::42:2/24 dev virt1
ip netns exec ns1 ip addr add 10.42.0.2/24 dev virt1

# Route all traffic from the netns to the host system via
ip netns exec ns1 ip route add default via 10.42.0.1 dev virt1
ip netns exec ns1 ip -6 route add default via fd00::42:1 dev virt1

# Set up NAT on our host
sysctl net.ipv4.ip_forward=1
sysctl net.ipv6.conf.all.forwarding=1
ip6tables -t nat -A POSTROUTING -o wg1 -j MASQUERADE
iptables -t nat -A POSTROUTING -o wg1 -j MASQUERADE

ip -6 route add fd00::42:0/64 dev virt0

Step 2: Create Network Manager connections

We instruct network manager to make a virtual ethernet device. This automatically creates the corresponding virt1 interface. The routing commands configures the host to only use this virtual ethernet device for the custom address space we gave are using in the network namespace.

nmcli connection add veth conn-name veth-virt0 if-name virt0 ip4 10.42.0.1 ip6 fd00::42:1 ipv4.method manual ipv4.never-default true ipv4.routing-rule1 "priority 10 from 10.42.0.1/24 table 69" ipv6.method manual ipv6.never-default true ipv6.route-table 69 ipv6.routing-rule1 "priority 10 from fd00::42:1 table 69"

Step 3: Start your application in the netns

Entering a namespace requires root permission, but we really want the application to run as the same user as usual, so we enter the namespace as root and su back inside it before running our application.

sudo nsenter --net=/var/run/netns/ns1 su $USER YOUR_APPLICATION

Posted

in

by

Tags:

Comments

Leave a Reply

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