As VPN use becomes more sophisticated, many Linux users want selective tunneling: run one app through a VPN while the rest of the system uses the normal connection — and ensure that app cannot leak if the VPN drops. This guide (September 2026) walks through a practical, reproducible method using network namespaces, WireGuard, and nftables to create a per‑app split tunnel with a tested kill‑switch and DNS isolation.

What you'll achieve

  • Run a single application inside an isolated network namespace that routes all traffic through a WireGuard tunnel.
  • Enforce a kill‑switch inside the namespace so traffic is dropped unless routed via the VPN peer.
  • Isolate DNS for the namespace to prevent host resolver leaks.
  • Automate startup with a systemd service to make the setup persistent and reliable.

Prerequisites and assumptions

  • Linux host (tested on Debian/Ubuntu, Fedora, and Arch derivatives) with root access.
  • Installed packages: iproute2, wireguard-tools (wg/wg-quick), nftables, and commonly available utilities (curl, iptables may exist but we use nftables).
  • A WireGuard endpoint you control or a commercial provider that allows custom clients. You should know the server's public key and endpoint IP:PORT.
  • Familiarity with editing configuration files and basic systemd unit files.

High‑level design

We create a network namespace (vpnns) and a veth pair connecting the namespace to the host for initial connectivity. WireGuard runs inside the namespace and becomes that namespace's default route. A namespace‑local nftables policy drops all outgoing packets except:

  • UDP traffic to the WireGuard endpoint (to allow the tunnel to form), and
  • Traffic that is already on the wg0 interface (i.e., encapsulated traffic).

This prevents the application from falling back to the host interface if the VPN fails. DNS for the namespace uses a resolver file in /etc/netns/vpnns to avoid host resolver leaks.

Step 1 — Create the network namespace and veth pair

  1. Create the namespace: ip netns add vpnns
  2. Create a veth pair: ip link add veth-host type veth peer name veth-ns
  3. Move the peer into the namespace: ip link set veth-ns netns vpnns
  4. Assign addresses and bring links up:
    • ip addr add 10.200.200.1/24 dev veth-host
    • ip link set veth-host up
    • ip netns exec vpnns ip addr add 10.200.200.2/24 dev veth-ns
    • ip netns exec vpnns ip link set veth-ns up
    • ip netns exec vpnns ip link set lo up
  5. Enable IPv4 forwarding on the host temporarily: sysctl -w net.ipv4.ip_forward=1

Notes: replace the 10.200.200.0/24 range if it collides with your LAN. The veth pair provides bootstrap connectivity so the namespace can reach the WireGuard server while the tunnel is brought up.

Step 2 — Configure DNS isolation

Create a resolver file for the namespace so processes there use a dedicated nameserver rather than the host's /etc/resolv.conf. This prevents the host resolver from being queried by apps in the namespace.

  1. mkdir -p /etc/netns/vpnns
  2. Write a conservative resolver: echo "nameserver 1.1.1.1" > /etc/netns/vpnns/resolv.conf

Later you can switch this to the private DNS advertised by the WireGuard server (if any) after the tunnel is up.

Step 3 — WireGuard client inside the namespace

Create a WireGuard configuration that is used only inside vpnns. Save it as /etc/wireguard/wg0-vpnns.conf. Minimal example (replace keys and endpoint):

  • [Interface] PrivateKey = <client-private-key> Address = 10.10.10.2/32
  • [Peer] PublicKey = <server-public-key> Endpoint = wg.example.net:51820 AllowedIPs = 0.0.0.0/0, ::/0 PersistentKeepalive = 25

Bring it up inside the namespace: ip netns exec vpnns wg-quick up /etc/wireguard/wg0-vpnns.conf

If the initial WireGuard handshake fails, troubleshoot with: ip netns exec vpnns wg show and ip netns exec vpnns ping -c 1 <server-ip>. If you need to debug lower-level packets, use the host-side tcpdump on the uplink: tcpdump -n -i <uplink> udp and host <server-ip>.

Step 4 — Implement the namespace kill‑switch using nftables

Implement the kill‑switch inside the namespace so no packet can leave the namespace unless it's destined for the WireGuard endpoint (to allow tunnel formation) or is already encapsulated on wg0.

  1. Determine the WireGuard server IP (use DNS lookup on the host if you only have a hostname).
  2. Create namespace‑local nftables rules. Example sequence (replace <WG_SERVER_IP> and <WG_SERVER_PORT>):

ip netns exec vpnns nft add table ip nsfilter

ip netns exec vpnns nft 'add chain ip nsfilter output { type filter hook output priority 0; policy drop; }'

Allow loopback and localhost: ip netns exec vpnns nft add rule ip nsfilter output ip daddr 127.0.0.0/8 accept

Allow traffic destined to the WireGuard endpoint so the tunnel can be established: ip netns exec vpnns nft add rule ip nsfilter output ip daddr <WG_SERVER_IP> udp dport <WG_SERVER_PORT> accept

Allow traffic that goes out the wg0 device (already encapsulated): ip netns exec vpnns nft add rule ip nsfilter output oifname "wg0" accept

Optional: allow UDP DNS to the server, if you need to reach external resolvers before the tunnel: ip netns exec vpnns nft add rule ip nsfilter output udp dport 53 accept

Because the chain policy is drop, any traffic that doesn't match an explicit accept will be dropped — fulfilling the kill‑switch behavior.

Step 5 — Configure host routing/NAT for bootstrap (if needed)

To allow the namespace to reach the WireGuard endpoint before wg0 exists, configure NAT on the host for the veth subnet. Example nftables NAT (replace <UPLINK> with your host's internet interface):

nft add table ip nat

nft 'add chain ip nat postrouting { type nat hook postrouting priority 100; }'

nft add rule ip nat postrouting ip saddr 10.200.200.0/24 oifname <UPLINK> masquerade

Alternatively, use an explicit iptables MASQUERADE rule if you prefer that tool. Keep a note of these rules for cleanup.

Step 6 — Run your app in the namespace

Launch the target application inside the namespace so its network use follows the namespace policy and WireGuard tunnel. Example for a browser sandbox or command-line app:

  • ip netns exec vpnns firefox --profile /tmp/vpn-profile
  • or for a single command: ip netns exec vpnns curl https://ifconfig.io

Because the namespace enforces routing and nftables drops, only traffic allowed by the kill‑switch (i.e., via wg0 or to the WG endpoint) will exit.

Step 7 — Verify and test for leaks

  1. Confirm external IP from inside namespace: ip netns exec vpnns curl -s https://ifconfig.io
  2. Ensure DNS queries are not leaving the namespace to the host: run tcpdump on the host uplink and check for port 53 traffic originating from 10.200.200.2 (there should be none once the tunnel is up).
  3. Simulate a VPN failure: ip netns exec vpnns wg-quick down /etc/wireguard/wg0-vpnns.conf then try curl; requests should fail (killed) rather than use the host's link.
  4. Check nftables counters inside the namespace to see drops: ip netns exec vpnns nft list chain ip nsfilter output

Step 8 — Automate with systemd

Make the namespace persistent by creating a systemd unit that sets up veth, DNS file, brings up WireGuard inside the namespace and applies nft rules. Use Type=oneshot units for setup and enable After=network-online.target. For service management, keep separate units for setup and for managing wg-quick inside the namespace (use ExecStartPre/ExecStartPost as necessary). This lets you bring the isolated environment up on boot and manage it with systemctl.

Operational tips and caveats

  • IPv6: If the WireGuard peer advertises IPv6 routes, make sure ip netns exec vpnns sysctl -w net.ipv6.conf.all.forwarding=1 and mirror nft rules for ip6 if you need IPv6. If you don't use IPv6, explicitly disable it in the namespace to avoid leaks.
  • DoH/DoT: Modern apps (browsers) may use DoH or DoT. If they do DoH over HTTPS, ensure the HTTPS requests are routed via the tunnel; DNS isolation alone won't stop encrypted DoH queries that use the host resolver path if the app chooses it.
  • Sandboxed apps: Flatpak/snap apps or apps that bundle their own resolver behavior may not respect /etc/netns resolv.conf; test each target application.
  • Trust model: You still need to trust your WireGuard endpoint. This guide isolates traffic to the endpoint but does not remove provider trust.
  • Complexity & maintenance: Kernel upgrades, changes to systemd, or distribution updates can affect network namespace behaviour. Keep the systemd automation and nft rules under version control and test after upgrades.

Why this method in 2026?

Network namespaces provide the cleanest kernel‑level isolation for per‑app networking. Running WireGuard inside the namespace avoids complex routing table workarounds on the host and keeps the kill‑switch enforcement local to the confined environment. In 2026, as VPN providers and client stacks mature, this approach remains robust because it relies only on kernel primitives (namespaces, veth, nftables, WireGuard) that distributions continue to ship and maintain.

Next steps and variations

  • Multiple apps or containers: Run a lightweight container (systemd‑nspawn or podman) attached to the namespace to group multiple apps.
  • Split DNS inside namespace: Run dnsmasq inside the namespace to forward to the provider's resolvers after WG is up.
  • Use policy routing instead of NAT for more advanced per‑dest routing.

This guide gives a reproducible pattern: network namespace for isolation, WireGuard inside the namespace for encryption, and namespace‑local nftables as a robust kill‑switch. It is appropriate for advanced VPN enthusiasts who require per‑app privacy without changing the host's global network configuration.