← Back to Home

Add Routes on Linux via .sh Script

NetRoute Pro generates a plain Bash script with ip route add commands. Apply it with one command for instant routes. Persistence requires one extra step depending on your distro.

Command Syntax

Syntax (idempotent — use this in scripts):

ip route replace <CIDR> via <GATEWAY> dev <VPN_IF>

Example:

ip route replace 1.1.1.0/24 via 10.0.0.1 dev wg0

Run as root or with sudo. NetRoute Pro generates these as a .sh script — apply with bash routes.sh. replace creates the route if missing and updates it if present, so re-running is safe; add would fail with RTNETLINK answers: File exists on a duplicate and (combined with set -e) abort the script.

Prerequisites

Step 1. Generate .sh in NetRoute Pro

  1. Open the target website in Chrome
  2. Click the NetRoute Pro icon in your extensions
  3. Select the Linux platform
  4. Set the gateway to your VPN interface IP (for example 10.8.0.1)
  5. Choose aggregation mask (recommended /24)
  6. Click Analyze Website
  7. Download the result as routes.sh
Tip: enable RIPE BGP optimization to get announced BGP prefixes instead of individual IPs — more stable when CDNs rotate addresses. Caveat: RIPE BGP returns all prefixes announced by the destination AS — for multi-tenant CDNs (Cloudflare AS13335, AWS AS16509, DigitalOcean AS14061) that’s tens of thousands of IPs covering unrelated sites. Use BGP optimization for single-tenant ASes; keep plain /24 CIDR aggregation for shared CDNs.

Step 2. Apply the script

chmod +x routes.sh
sudo bash routes.sh

Routes are applied immediately — but only until the next reboot or until your VPN interface goes down. For permanence, see Step 3.

Step 3. Make routes persistent

Pick one of the three options below, depending on how your system manages networking.

systemd-networkd

If your distro uses systemd-networkd (default on many minimal installs, Arch, server distros), create a drop-in file:

sudo mkdir -p /etc/systemd/network/10-vpn.network.d
sudo nano /etc/systemd/network/10-vpn.network.d/routes.conf

Add one [Route] block per subnet:

[Route]
Destination=104.21.32.0/24
Gateway=10.8.0.1

[Route]
Destination=172.67.182.0/24
Gateway=10.8.0.1

Apply:

sudo systemctl restart systemd-networkd

NetworkManager dispatcher

If you use NetworkManager (Ubuntu desktop, Fedora Workstation, most laptops), save a dispatcher hook:

sudo nano /etc/NetworkManager/dispatcher.d/99-vpn-routes

Example content — checks that the triggering interface is your VPN device and re-runs the ip route add commands:

#!/bin/bash
# $1 = interface name, $2 = action
if [ "$1" = "<your-vpn-interface>" ] && [ "$2" = "up" ]; then
    ip route add 104.21.32.0/24 via 10.8.0.1
    ip route add 172.67.182.0/24 via 10.8.0.1
fi

Make it executable:

sudo chmod +x /etc/NetworkManager/dispatcher.d/99-vpn-routes

systemd oneshot service (recommended)

Works on any distro with systemd (Ubuntu, Debian, Fedora, Arch, RHEL, openSUSE — virtually all modern distros). Properly waits for the VPN tunnel to be up before applying routes — no race conditions, no sleep hacks.

sudo mv routes.sh /usr/local/bin/routes.sh
sudo chmod +x /usr/local/bin/routes.sh

sudo tee /etc/systemd/system/vpn-routes.service > /dev/null <<'EOF'
[Unit]
Description=Apply VPN split-tunnel routes
After=network-online.target wg-quick@wg0.service
Wants=network-online.target
Requisite=wg-quick@wg0.service
BindsTo=wg-quick@wg0.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/routes.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now vpn-routes.service

Replace wg-quick@wg0.service with your actual VPN unit:

Verify the service:

systemctl status vpn-routes.service
journalctl -u vpn-routes.service
Why not @reboot cron? The cron daemon usually starts before the VPN tunnel is up — ip route add runs against an interface that doesn’t exist yet, fails silently, and you discover broken routes only after a reboot. systemd’s After= + Requisite= waits for the VPN unit and eliminates the race. BindsTo= also stops the routes when the VPN goes down.

DNS leak — required reading

This guide routes traffic by IP. It does not route DNS. Your browser still asks the system resolver (usually your ISP’s, learned via DHCP) for example.com first — only the resulting IP traffic is encrypted through the VPN. The ISP sees which sites you visit even though the data is encrypted.

Three options, by threat model:

  1. Hide DNS from ISP fully (split-DNS). On systemd-resolved, route DNS for tunneled domains through the VPN’s resolver only:
    sudo resolvectl dns wg0 10.0.0.1
    sudo resolvectl domain wg0 ~example.com ~another-site.com
    Now example.com queries go through the VPN, everything else uses the default resolver. Replace 10.0.0.1 with your VPN provider’s internal DNS.
  2. Reduce ISP visibility (public DoH/DoT). Point the system resolver at a public encrypted resolver:
    sudo resolvectl dns wg0 1.1.1.1#cloudflare-dns.com
    sudo resolvectl dnsovertls wg0 yes
    ISP no longer sees domain queries; the public resolver does.
  3. Accept the leak. If your goal is content access, not surveillance avoidance, this is fine — the data path is still encrypted, only the lookup metadata leaks.

Verify the current state with dnsleaktest.com or browserleaks.com/dns — the resolver shown should belong to your VPN or chosen public DoH provider, not your ISP. Locally, resolvectl status shows the per-link DNS configuration.

IPv6 dual-stack bypass

If the destination has an AAAA record (most popular sites do — Cloudflare, Google, AWS are all dual-stack), the OS prefers IPv6 over IPv4 (RFC 6724). Routes added via this guide are IPv4-only — IPv6 traffic skips the VPN entirely and exits via your ISP’s default v6 route. Result: VPN looks active but does nothing for dual-stack destinations.

Quick check from the same machine after the VPN is up:

curl -6 -s -o /dev/null -w "remote=%{remote_ip}\n" https://example.com
ip -6 route get $(dig +short AAAA example.com | head -1)

If the IPv6 trace doesn’t go through your VPN interface, you’re bypassing the tunnel. Two fixes:

Fail-closed (kill switch)

When the VPN tunnel goes down (network change, provider hiccup, sleep/wake), the kernel removes any routes bound to the dead interface — and traffic to previously-tunneled CIDRs falls back to your ISP’s default route. Result: silent leak with no error visible to the user. To enforce fail-closed (block instead of leak):

# nftables (modern Linux)
sudo nft add table inet vpnkill
sudo nft add chain inet vpnkill output { type filter hook output priority 0 \; }
sudo nft add rule inet vpnkill output ip daddr 1.1.1.0/24 oif != wg0 drop
sudo nft add rule inet vpnkill output ip daddr 8.8.8.0/24 oif != wg0 drop
# Repeat for each tunneled CIDR.

iptables equivalent:

sudo iptables -I OUTPUT ! -o wg0 -d 1.1.1.0/24 -j REJECT
sudo iptables -I OUTPUT ! -o wg0 -d 8.8.8.0/24 -j REJECT

Now traffic to those prefixes via any interface other than wg0 is dropped — VPN down = blocked, not leaked. The user sees a connection error (loud signal that VPN is down) instead of silently exiting via the ISP. This is the safer failure mode.

Verify

Check that the routes are in the kernel table:

ip route show | grep 10.8.0.1

All added subnets should appear, bound to your VPN gateway.

Rollback

The script saves a snapshot of your route table to /tmp/route-table.<timestamp>.txt before applying changes — useful for diagnosing what changed. To remove the routes added by NetRoute Pro, delete each prefix:

for cidr in 1.1.1.0/24 8.8.8.0/24 162.159.0.0/16; do
    sudo ip route del "$cidr" 2>/dev/null || true
done

Or, more conservatively, generate a reverse list from your routes.sh — replacing replace (or add) with del:

awk '/ip route /{ sub(/^ip route (replace|add)/, "ip route del"); print }' routes.sh | sudo bash

If something went catastrophically wrong, the snapshot at /tmp/route-table.<timestamp>.txt shows the pre-change state — you can manually re-add anything you needed.

Common issues

Network is unreachable

Your VPN interface is down. Check with:

ip link show

Bring the interface up before running the script.

Permission denied

ip route replace needs root. Always invoke with sudo.

Example Configuration File

Ready-to-edit template with inline comments. Replace the example routes with output from NetRoute Pro for your target sites.


#!/bin/bash
# Example Linux routing script for split tunneling.
# Generated by NetRoute Pro: https://alexander2k.github.io/netroute-site/
#
# Run as root or with sudo. Routes added here last until reboot —
# for persistence see the Linux guide (systemd-networkd / NetworkManager).
#
# Idempotent: uses `ip route replace` so re-running the script is safe.
# `add` would fail with "RTNETLINK answers: File exists" on a duplicate;
# combined with `set -e`, the first existing route would abort the script
# and skip the rest. `replace` creates if missing, updates if present.

set -euo pipefail

DEV="wg0"          # VPN interface name (wg0, tun0, ppp0, ...)
GW="10.0.0.1"      # VPN gateway IP (or use only "dev $DEV" if VPN is point-to-point)

# Snapshot current route table — useful for rollback if something breaks.
SNAPSHOT="/tmp/route-table.$(date +%s).txt"
ip route show > "$SNAPSHOT"
echo "Route table snapshot saved to $SNAPSHOT"

ip route replace 1.1.1.0/24    via "$GW" dev "$DEV"
ip route replace 8.8.8.0/24    via "$GW" dev "$DEV"
ip route replace 162.159.0.0/16 via "$GW" dev "$DEV"

# Verify with: ip route show
# Remove a single route: ip route del <CIDR>
# Full rollback (delete all routes added by this script):
#   for cidr in 1.1.1.0/24 8.8.8.0/24 162.159.0.0/16; do
#     ip route del "$cidr" 2>/dev/null || true
#   done

Tip: Need a config without these comment lines? In NetRoute Pro options, uncheck “Include comments in exported files” — the extension will export only the route commands. Useful for routers that don’t tolerate comment lines.

View all example configs on GitHub →

Official Documentation

Ready to try?

NetRoute Pro — a free Chrome extension to generate routes from any website.

Install Extension