Skip to content

WireGuard Exit Node Deployment

Multi-worker WireGuard tunnel infrastructure with VPN exit routing through OpenWRT workers.

Overview

This system creates WireGuard tunnels from a central host to 15 OpenWRT workers, each providing a unique exit IP through their VPN connections. Applications can bind to specific tunnel interfaces to route traffic through different geographic exit points.

Architecture

flowchart TB
    subgraph Host["Host Server (Ubuntu)"]
        direction TB
        APP[Application]
        WG01[wg01<br/>10.200.1.1]
        WG02[wg02<br/>10.200.2.1]
        WG03[wg03<br/>10.200.3.1]
        WGDOTS[...]
        WG15[wg15<br/>10.200.15.1]
    end

    subgraph Workers["OpenWRT Workers (17.0.0.x)"]
        direction TB
        W01[Worker 01<br/>wg_exit 10.200.1.2]
        W02[Worker 02<br/>wg_exit 10.200.2.2]
        W03[Worker 03<br/>wg_exit 10.200.3.2]
        WDOTS[...]
        W15[Worker 15<br/>wg_exit 10.200.15.2]
    end

    subgraph VPN["VPN Tunnels (tun0)"]
        direction TB
        V01[Exit: 146.70.x.x]
        V02[Exit: 151.243.x.x]
        V03[Exit: 149.22.x.x]
        VDOTS[...]
        V15[Exit: 154.47.x.x]
    end

    APP --> WG01 & WG02 & WG03 & WG15
    WG01 <-->|WireGuard| W01
    WG02 <-->|WireGuard| W02
    WG03 <-->|WireGuard| W03
    WG15 <-->|WireGuard| W15
    W01 -->|table 100| V01
    W02 -->|table 100| V02
    W03 -->|table 100| V03
    W15 -->|table 100| V15

Traffic Flow

sequenceDiagram
    participant App as Application
    participant Host as Host (wgXX)
    participant Worker as Worker (wg_exit)
    participant Table as Policy Route<br/>Table 100
    participant VPN as OpenVPN (tun0)
    participant Net as Internet

    App->>Host: curl --interface wg03
    Note over Host: Source: 10.200.3.1<br/>Route via table 203
    Host->>Worker: WireGuard encrypted
    Worker->>Table: src 10.200.3.0/24
    Table->>VPN: default via 10.96.0.1
    VPN->>Net: Exit via VPN IP
    Net-->>VPN: Response
    VPN-->>Worker: Decrypt
    Note over Worker: Masquerade (srcnat)
    Worker-->>Host: WireGuard encrypted
    Host-->>App: Response

Network Configuration

Addressing Scheme

Worker Host Interface Host IP Worker IP Host Port Worker Port Routing Table
01 wg01 10.200.1.1/24 10.200.1.2/24 51801 54501 201
02 wg02 10.200.2.1/24 10.200.2.2/24 51802 54502 202
03 wg03 10.200.3.1/24 10.200.3.2/24 51803 54503 203
... ... ... ... ... ... ...
15 wg15 10.200.15.1/24 10.200.15.2/24 51815 54515 215

Worker List

Workers are defined in scripts/wireguard/workers.txt:

# Format: worker_num,hostname,ip
01,lazarus-worker-01,17.0.0.10
02,lazarus-worker-02,17.0.0.11
03,lazarus-worker-03,17.0.0.12
...
15,lazarus-worker-15,17.0.0.25

Deployment

Batch Deployment Script

flowchart TD
    subgraph Deploy["wg-batch-deploy.sh deploy"]
        D1[Parse workers.txt] --> D2[For each worker]
        D2 --> D3[Generate host keys]
        D3 --> D4[SSH to worker]
        D4 --> D5[Install WireGuard if needed]
        D5 --> D6[Generate worker keys]
        D6 --> D7[Configure UCI network]
        D7 --> D8[Configure UCI firewall]
        D8 --> D9[Add policy routes]
        D9 --> D10[Restart network/firewall]
        D10 --> D11[Return worker pubkey]
        D11 --> D12[Create host wgXX.conf]
        D12 --> D13[wg-quick up wgXX]
        D13 --> D14[Test tunnel]
        D14 --> D2
    end

Commands

# Deploy to all workers
./scripts/wireguard/wg-batch-deploy.sh deploy

# Deploy to a single worker
./scripts/wireguard/wg-batch-deploy.sh single 03

# Test all tunnels
./scripts/wireguard/wg-batch-deploy.sh test

# Show status
./scripts/wireguard/wg-batch-deploy.sh status

# Cleanup all tunnels
./scripts/wireguard/wg-batch-deploy.sh cleanup

Script Files

Script Purpose
wg-batch-deploy.sh Main batch deployment orchestrator
wg-exit-deploy.sh Single worker deployment (legacy)
fix-routing.sh Fix routing table 100 on workers
workers.txt Worker definitions

Configuration Details

Host Configuration

Each tunnel creates a host config at /etc/wireguard/wgXX.conf:

# /etc/wireguard/wg03.conf
[Interface]
PrivateKey = <host_private_key>
Address = 10.200.3.1/24
ListenPort = 51803
Table = 203
PostUp = ip rule add from 10.200.3.1 table 203 priority 100
PostDown = ip rule del from 10.200.3.1 table 203 priority 100

[Peer]
# lazarus-worker-03 at 17.0.0.12
PublicKey = <worker_public_key>
AllowedIPs = 0.0.0.0/0
Endpoint = 17.0.0.12:54503
PersistentKeepalive = 25

Worker UCI Configuration

flowchart TD
    subgraph Network["network config"]
        N1[wg_exit interface<br/>proto: wireguard<br/>listen_port: 545XX<br/>addresses: 10.200.X.2/24]
        N2[wg_exit_peer<br/>public_key: host_key<br/>endpoint: 17.0.0.240:518XX<br/>allowed_ips: 10.200.X.1/32]
        N3[wg_rule_XX<br/>src: 10.200.X.0/24<br/>lookup: 100]
        N4[wg_tunnel_route_XX<br/>interface: wg_exit<br/>target: 10.200.X.0/24<br/>table: 100]
        N5[wg_default_route_XX<br/>interface: vpn<br/>target: 0.0.0.0<br/>gateway: 10.96.0.1<br/>table: 100]
    end

    subgraph Firewall["firewall config"]
        F1[wg_zone_XX<br/>name: wireguard_XX<br/>input/output/forward: ACCEPT<br/>network: wg_exit<br/>masq: 1]
        F2[wg_fwd_XX<br/>src: wireguard_XX<br/>dest: wan]
        F3[wg_fwd_vpn_XX<br/>src: wireguard_XX<br/>dest: vpn]
    end

    N1 --> N2
    N3 --> N4 --> N5
    F1 --> F2 & F3

Policy Routing

Traffic from WireGuard is routed through the worker's VPN (tun0):

flowchart LR
    subgraph Worker["OpenWRT Worker"]
        WG[wg_exit<br/>10.200.X.2] --> RULE[ip rule:<br/>from 10.200.X.0/24<br/>lookup 100]
        RULE --> T100[Table 100:<br/>default via 10.96.0.1<br/>dev tun0]
        T100 --> TUN[tun0<br/>VPN Tunnel]
        TUN --> MASQ[Masquerade<br/>srcnat_vpn]
    end
    MASQ --> EXIT[VPN Exit IP]

Usage

Bind Traffic to Specific Tunnel

# Using curl
curl --interface wg03 https://example.com
curl --interface wg03 https://ipinfo.io/ip

# Using wget
wget --bind-address=10.200.3.1 https://example.com

# In Python (requests)
import requests
session = requests.Session()
session.get('https://example.com',
            headers={'Host': 'example.com'},
            timeout=30,
            stream=True)
# Note: Use socket binding for interface selection

Verify Tunnel

# Check WireGuard status
sudo wg show wg03

# Ping tunnel endpoint
ping 10.200.3.2

# Check exit IP
curl --interface wg03 -sSL https://ipinfo.io/ip

Test All Tunnels

./scripts/wireguard/wg-batch-deploy.sh test

Expected output:

=== Testing all tunnels ===
wg01 (lazarus-worker-01): OK - Exit IP: 146.70.195.107
wg02 (lazarus-worker-02): OK - Exit IP: 151.243.141.134
wg03 (lazarus-worker-03): OK - Exit IP: 149.22.80.96
...

Troubleshooting

Check Handshake

sudo wg show wg03
# Look for "latest handshake" - should be within last 2 minutes

No Handshake

flowchart TD
    A[No handshake] --> B{Can ping worker LAN IP?}
    B -->|No| C[Check network connectivity<br/>ping 17.0.0.12]
    B -->|Yes| D{Worker WG listening?}
    D -->|No| E[Check worker:<br/>ssh root@17.0.0.12<br/>wg show wg_exit]
    D -->|Yes| F{Firewall blocking?}
    F -->|Yes| G[Check worker firewall:<br/>nft list chain inet fw4 input]
    F -->|No| H[Check keys match]

Tunnel Ping Works, HTTP Fails

  1. Check policy routing on worker:

    ssh root@17.0.0.12 "ip route show table 100"
    # Should show: default via 10.96.0.1 dev tun0
    

  2. Check firewall forwarding:

    ssh root@17.0.0.12 "nft list chain inet fw4 forward_wireguard_03"
    # Should show: jump accept_to_vpn
    

  3. Fix routing if needed:

    ssh root@17.0.0.12 "ip route replace default via 10.96.0.1 dev tun0 table 100"
    

Exit IP Shows Main Host IP (104.11.127.142)

Traffic is not routing through VPN. Check:

  1. Table 100 routes through tun0 (not phy0-sta0):

    ssh root@17.0.0.12 "ip route show table 100"
    

  2. Fix if routing through WAN:

    ssh root@17.0.0.12 "ip route replace default via 10.96.0.1 dev tun0 table 100"
    

Worker VPN Down

# Check if VPN is connected
ssh root@17.0.0.12 "ping -c 1 10.96.0.1"

# Check VPN interface
ssh root@17.0.0.12 "ip link show tun0"

# Restart VPN
ssh root@17.0.0.12 "/etc/init.d/openvpn restart"

Current Deployment Status

Interface Worker LAN IP Exit IP Status
wg01 lazarus-worker-01 17.0.0.10 146.70.195.107 Active
wg02 lazarus-worker-02 17.0.0.11 151.243.141.134 Active
wg03 lazarus-worker-03 17.0.0.12 149.22.80.96 Active
wg04 lazarus-worker-04 17.0.0.13 149.102.228.68 Active
wg05 lazarus-worker-05 17.0.0.14 149.22.94.166 Active
wg06 lazarus-worker-06 17.0.0.15 89.187.170.182 Active
wg07 lazarus-worker-07 17.0.0.16 149.88.30.82 Active
wg08 lazarus-worker-08 17.0.0.17 149.22.94.58 Active
wg09 lazarus-worker-09 17.0.0.19 185.244.215.14 Active
wg10 lazarus-worker-10 17.0.0.20 149.22.84.8 Active
wg11 lazarus-worker-11 17.0.0.21 89.187.180.44 Active
wg12 lazarus-worker-12 17.0.0.22 149.88.105.226 Active
wg13 lazarus-worker-13 17.0.0.23 95.173.221.53 Active
wg14 lazarus-worker-14 17.0.0.24 185.98.170.12 Active
wg15 lazarus-worker-15 17.0.0.25 154.47.25.244 Active

References