⚙️ WireGuard Split-Tunnel VPN System

Complete Technical Reference - Unified Documentation

Line-by-line code documentation, configuration details, and internal mechanics

📂 Project File Overview

This WireGuard VPN system consists of exactly 4 files that work together to create a complete split-tunnel routing solution:

File Type Purpose Executes When
internal.conf Configuration dnsmasq DNS server configuration Loaded when dnsmasq starts
wg0.conf Configuration WireGuard VPN interface configuration Loaded when WireGuard interface brought up
wg0-up.sh Bash Script Post-UP hook - initializes all networking Automatically after WireGuard interface comes up
wg0-down.sh Bash Script Post-DOWN hook - cleans up all networking Automatically before WireGuard interface goes down

🌐 internal.conf - DNS Configuration File

File: internal.conf

Purpose: Configure dnsmasq DNS server with local domain overrides and upstream DNS forwarding

address=/vault.raff.local/10.4.0.7 address=/vault.raff.local/10.4.0.7

Maps "vault.raff.local" to 10.4.0.7. Split-horizon DNS for internal services.

address=/rafflab.internal/10.4.0.3 address=/rafflab.internal/10.4.0.3

Maps "rafflab.internal" to 10.4.0.3. Second internal service hostname.

Listen Addresses (3 lines) listen-address=127.0.0.1 listen-address=10.4.0.1 listen-address=10.200.1.1

127.0.0.1: Localhost interface for local processes
10.4.0.1: WireGuard interface (main DNS server for clients)
10.200.1.1: Namespace bridge for DNS privacy routing

no-resolv & strict-order no-resolv strict-order

no-resolv: Don't read /etc/resolv.conf; use only explicit server= lines
strict-order: Use upstream servers in order, don't randomize

Upstream DNS Servers server=1.1.1.2 server=1.0.0.2 server=8.8.8.8 server=8.8.4.4

Cloudflare (1.1.1.2, 1.0.0.2) primary, Google (8.8.8.8, 8.8.4.4) backup. Privacy-respecting DNS configuration.

🔐 wg0.conf - WireGuard Configuration

File: wg0.conf

[Interface]

Interface section: Contains server-side WireGuard configuration

PrivateKey

Server's private key: Cryptographic basis for all WireGuard connections. Must be kept secret. Generated with: wg genkey

Address = 10.4.0.1/24

Interface IP: Server's own IP on wg0 interface. /24 creates local route for 10.4.0.0/24 subnet. Also serves as DNS server for clients.

ListenPort = 45822

UDP listen port: Non-standard port for WireGuard. Must be open in firewall. Clients must know this exact port.

MTU = 1420

Maximum transmission unit: Reduced from standard 1500 to account for encryption overhead. Prevents fragmentation in VPN tunnels.

Table = off

Disable automatic routing: WireGuard won't create automatic routes. Custom split-tunnel routing handled manually via iptables and policy routing.

PostUp = /etc/wireguard/wg0-up.sh

Startup hook: Automatically executes wg0-up.sh after interface is up. This is where the complex networking setup happens.

PostDown = /etc/wireguard/wg0-down.sh

Shutdown hook: Automatically executes wg0-down.sh before interface is destroyed. Performs cleanup and restoration.

[Peer] ... <my peers>

Peer configurations: Each client is a peer with public key and allowed IP range. These determine IP allocation and routing.

↓ Beginning of wg0-up.sh Documentation ↓

🚀 wg0-up.sh - Setup Script Part 1: Initialization

File: wg0-up.sh - Lines 1-41

Line 1-2 #!/bin/bash set -e

Bash shebang and error handling: Script interpreted as Bash. set -e exits on first error (fail-fast mode).

Global Variables (Lines 4-19)

Lines 4-19: Configuration Variables NS="vpn_ns" TABLE="200" WG_IF="wg0" WG_NET="10.4.0.0/24" VPN_RANGE="10.4.0.128-10.4.0.255" NON_VPN_SUBNET="10.4.0.0/25" VETH_HOST="veth-wg" VETH_NS="veth-vpn" NS_NET="10.200.1.0/24" HOST_IP="10.200.1.1/24" NS_IP="10.200.1.2/24" EGRESS_IF="ens5" OPENVPN_CONFIG="/home/ec2-user/<vpn-config>.ovpn" OPENVPN_CREDS="/home/ec2-user/<vpn-creds>.txt" OPENVPN_LOG="/var/log/openvpn-ns.log" OPENVPN_PID="/run/openvpn-ns.pid"

All critical configuration in one place. These variables define IP ranges, interface names, and VPN paths. Changing these reconfigures the entire system.

Line 21 sysctl -w net.ipv4.ip_forward=1 >/dev/null

Enable IP forwarding: Critical for routing. Allows kernel to forward packets between interfaces.

Line 23 ip netns list | grep -q "^$NS " || ip netns add "$NS"

Idempotent namespace creation: Only creates namespace if it doesn't exist. Safe to run multiple times.

Lines 25-26 if ! ip link show "$VETH_HOST" &>/dev/null; then ip link add "$VETH_HOST" type veth peer name "$VETH_NS" fi

Idempotent veth creation: Creates virtual ethernet pair connecting host to namespace. Only if not already created.

Lines 28-30 if ! ip netns exec "$NS" ip link show "$VETH_NS" &>/dev/null 2>&1; then ip link set "$VETH_NS" netns "$NS" 2>/dev/null || true fi

Move namespace end of veth: Moves veth-vpn interface into the namespace. Can only be done once.

Lines 32-33 ip addr add "$HOST_IP" dev "$VETH_HOST" 2>/dev/null || true ip link set "$VETH_HOST" up

Configure and bring up host veth end: Assigns 10.200.1.1/24 to veth-wg and activates it.

Lines 35-37 ip netns exec "$NS" ip addr add "$NS_IP" dev "$VETH_NS" 2>/dev/null || true ip netns exec "$NS" ip link set "$VETH_NS" up ip netns exec "$NS" ip link set lo up

Configure namespace interfaces: Assigns 10.200.1.2/24 to veth-vpn, brings it up, and activates loopback.

Line 39 ip netns exec "$NS" sysctl -w net.ipv4.ip_forward=1 >/dev/null

Enable forwarding inside namespace: Allows namespace to route packets.

Line 41 ip netns exec "$NS" ip route replace default via 10.200.1.1

Temporary namespace default route: Temporary before OpenVPN starts. Will be overridden when tun0 is created.

↓ Continuing wg0-up.sh Documentation ↓

🛣️ Namespace Static Routes (Lines 42-47)

Line 42 ip netns exec "$NS" ip route add via 10.200.1.1 dev "$VETH_NS"

Route to OpenVPN provider server: Inside namespace, explicitly routes VPN server IP through veth back to host. Prevents routing loop when OpenVPN connects.

Line 44 ip netns exec "$NS" ip route add "$WG_NET" via 10.200.1.1 dev "$VETH_NS"

Route to WireGuard subnet: Allows response packets destined for WireGuard clients (10.4.0.0/24) to find their way back through veth.

🌐 Namespace DNS Configuration (Lines 49-52)

Line 49 mkdir -p /etc/netns/"$NS"

Create namespace config directory: Linux allows per-namespace config files in /etc/netns/[namespace-name]/

Lines 50-53 cat > /etc/netns/"$NS"/resolv.conf <

Namespace DNS configuration: Processes inside namespace read this file for DNS resolution. Points to Google DNS.

🚀 OpenVPN Startup (Lines 54-58)

Lines 55-62 ip netns exec "$NS" openvpn \ --config "$OPENVPN_CONFIG" \ --auth-user-pass "$OPENVPN_CREDS" \ --daemon \ --log "$OPENVPN_LOG" \ --writepid "$OPENVPN_PID" 2>/dev/null || true

Launch OpenVPN in namespace: Starts VPN connection inside isolated namespace. Creates tun0 interface and connects to OpenVPN provider server.

  • --daemon: Run in background
  • --writepid: Save process ID for later termination
  • 2>/dev/null || true: Suppress errors, continue anyway
↓ Policy Routing and Packet Marking ↓

🛣️ Policy-Based Routing Setup (Lines 66-80)

Lines 80-81 ip rule add from "$WG_NET" to "$WG_NET" table main priority 100

WireGuard peer-to-peer rule: Traffic between WireGuard clients (both source and destination in 10.4.0.0/24) always uses main table. Ensures all client-to-client traffic stays local.

Lines 82-92 ip rule add from all to 10.4.0.0/24 table main priority 101 ip rule add from all to 10.0.0.0/16 table main priority 102 ip rule add from all to 10.0.1.0/24 table main priority 103 ip rule add from all to 10.0.2.0/24 table main priority 104

Exclude local networks from VPN: Any traffic destined for local networks always uses direct path, never VPN. Prevents internal traffic from being routed through OpenVPN provider.

Lines 94-98 iptables -t mangle -A PREROUTING -i "$WG_IF" -m iprange --src-range "$VPN_RANGE" -j MARK --set-mark 0x200

Mark VPN client packets: When packet arrives on wg0 from VPN client range (10.4.0.128-255), mark it with fwmark 0x200. This mark determines routing.

Lines 100-101 ip rule add fwmark 0x200 table "$TABLE" priority 200

Route marked packets through table 200: Any packet with fwmark 0x200 uses routing table 200, which has default route through namespace.

🔖 DNS Query Marking (Lines 82-102)

Lines 104-112 iptables -t mangle -A OUTPUT -p udp -m udp --dport 53 -d 1.1.1.2 -j MARK --set-mark 0x100 iptables -t mangle -A OUTPUT -p udp -m udp --dport 53 -d 1.0.0.2 -j MARK --set-mark 0x100 iptables -t mangle -A OUTPUT -p udp -m udp --dport 53 -d 8.8.8.8 -j MARK --set-mark 0x100 iptables -t mangle -A OUTPUT -p udp -m udp --dport 53 -d 8.8.4.4 -j MARK --set-mark 0x100

Mark dnsmasq upstream queries: When dnsmasq sends DNS queries to upstream servers, mark them with fwmark 0x100. Different mark than VPN client traffic.

Lines 117-118 ip rule add fwmark 0x100 table "$TABLE" priority 201

Route DNS queries through table 200: Marked DNS queries also use table 200, routing them into the namespace/VPN for privacy.

Lines 120-121 ip route add default via 10.200.1.2 dev "$VETH_HOST" table "$TABLE"

Default route in table 200: The actual gateway to the namespace. All traffic in table 200 routes to 10.200.1.2 (namespace end of veth).

Lines 123-127 iptables -t nat -A POSTROUTING -s 10.0.1.191 -d 1.1.1.2 -p udp --dport 53 -j SNAT --to-source 10.200.1.1

NAT for dnsmasq queries: When dnsmasq (10.0.1.191) sends DNS queries through namespace, rewrite source to 10.200.1.1. Necessary for proper return routing.

🔄 NAT Configuration (Lines 128-145)

Lines 128-129 iptables -t nat -A POSTROUTING -s "$NS_NET" -o "$EGRESS_IF" -j MASQUERADE

NAT for namespace→internet: Traffic from namespace (10.200.1.0/24) exiting to internet masked behind AWS IP (10.0.1.191).

Lines 131-132 iptables -t nat -A POSTROUTING -s "$NON_VPN_SUBNET" -o "$EGRESS_IF" -j MASQUERADE

NAT for direct-internet clients: Non-VPN clients (10.4.0.0/25) appear to come from AWS IP when accessing internet.

🔀 Forwarding Rules (Lines 147-165)

Lines 147-148 iptables -A FORWARD -i "$WG_IF" -o "$WG_IF" -s "$WG_NET" -d "$WG_NET" -j ACCEPT

Allow WireGuard peer-to-peer: Traffic between WireGuard clients forwarded directly between them.

Lines 150-151 iptables -A FORWARD -i "$WG_IF" -m iprange --src-range "$VPN_RANGE" -o "$VETH_HOST" -j ACCEPT

Allow VPN clients→namespace: VPN client traffic (10.4.0.128-255) can be forwarded to namespace.

Lines 153-154 iptables -A FORWARD -i "$VETH_HOST" -o "$WG_IF" -m iprange --dst-range "$VPN_RANGE" -m state --state RELATED,ESTABLISHED -j ACCEPT

Allow namespace→VPN clients (replies): Response packets from namespace destined for VPN clients can be forwarded. Stateful filtering ensures only legitimate replies.

Lines 156-157 iptables -A FORWARD -i "$VETH_HOST" -o "$EGRESS_IF" -j ACCEPT

Allow namespace→internet: Namespace can forward traffic to internet.

Lines 159-160 iptables -A FORWARD -i "$EGRESS_IF" -o "$VETH_HOST" -m state --state RELATED,ESTABLISHED -j ACCEPT

Allow internet→namespace (replies): Responses from internet to namespace (stateful).

Lines 162-165 iptables -A FORWARD -i "$WG_IF" -s "$NON_VPN_SUBNET" -o "$EGRESS_IF" -j ACCEPT iptables -A FORWARD -i "$EGRESS_IF" -o "$WG_IF" -d "$NON_VPN_SUBNET" -m state --state RELATED,ESTABLISHED -j ACCEPT

Allow direct-internet clients: Non-VPN clients can access internet directly, and receive responses.

🔗 MSS Clamping (Lines 167-171)

Lines 167-171 iptables -t mangle -A FORWARD -i "$WG_IF" -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu ip netns exec "$NS" iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

MSS clamping: Adjusts TCP Maximum Segment Size to match interface MTU (1420). Prevents TCP stalls due to fragmentation when packets are larger than interface allows.

📊 Startup Status Message (Lines 173-185)

Displays formatted status message showing system is configured, IP allocation scheme, and excluded networks. Provides operators with quick overview of configuration.

↓ Beginning of wg0-down.sh Documentation ↓

🛑 wg0-down.sh - Teardown Script (Complete)

File: wg0-down.sh - Full Documentation

Purpose: Completely dismantle all networking created by wg0-up.sh, leaving system in clean state

Initialization (Lines 1-15)

Lines 1-2 #!/bin/bash set -e

Same as wg0-up.sh: Bash shebang and error exit mode.

Lines 4-15

Variable redefinition: Same variables as wg0-up.sh. MUST match exactly or cleanup fails.

OpenVPN Shutdown (Lines 17-21)

Lines 17-21 if [ -f "$OPENVPN_PID" ]; then echo "Stopping OpenVPN..." kill $(cat "$OPENVPN_PID") 2>/dev/null || true rm -f "$OPENVPN_PID" sleep 2 fi

Stop OpenVPN: If PID file exists, kill the process and wait for graceful shutdown. MUST be done before deleting namespace.

Namespace and Host Cleanup

Lines 23-66

Remove all iptables rules: Deletes rules in reverse order of creation (though order doesn't matter for deletion):

  • Namespace NAT rules
  • Host mangle rules (marking)
  • Host mangle rules (MSS)
  • Host NAT rules
  • Host FORWARD rules
Lines 30-37

Remove policy routing rules: Deletes all rules that determined which traffic went through which routing table.

Lines 66-70 ip link del "$VETH_HOST" 2>/dev/null || true ip netns del "$NS" 2>/dev/null || true rm -rf /etc/netns/"$NS" 2>/dev/null || true

Delete veth and namespace: Deleting veth automatically deletes both ends. Deleting namespace terminates all processes inside. Cleanup complete.

📋 Technical Summary & Dependencies

Critical Dependencies

Component Required Why
Linux kernel 4.15+ Essential Network namespaces, policy routing, iptables required
WireGuard Essential Creates and manages wg0 interface
iproute2 (ip command) Essential Namespace and routing management
iptables Essential NAT, packet marking, forwarding rules
OpenVPN client Essential VPN tunnel (tun0) creation
dnsmasq Essential DNS server for clients

Traffic Flow Summary

Traffic Type Source IP Path Exit Point
Direct client data 10.4.0.0-127 wg0 → host → NAT → ens5 AWS Elastic IP
VPN client data 10.4.0.128-255 wg0 → marked → veth → namespace → NAT → tun0 OpenVPN provider exit
Peer-to-peer 10.4.0.0-255 wg0 → FORWARD → wg0 N/A (internal)
Local network any Excluded by policy rule → direct Local gateway

Key Design Decisions

  1. Namespace Isolation: OpenVPN in isolated namespace prevents routing conflicts
  2. Packet Marking: fwmark enables routing based on packet properties
  3. Policy Routing: RPDB rules enable source-based routing decisions
  4. IP Range Split: IP assignment determines routing automatically
  5. Stateful Filtering: Connection tracking prevents spoofed replies
  6. DNS Privacy: Routing DNS queries through VPN provides privacy
  7. Local Exclusions: Policy rules ensure internal traffic never routes through VPN
  8. Idempotent Setup: Using -C flag prevents duplicate rules

🎓 Conclusion

This WireGuard split-tunnel VPN system demonstrates advanced Linux networking in production. By combining namespaces, policy routing, packet marking, and stateful NAT, it achieves selective traffic routing without client-side configuration.

Core Concept: IP address allocation is destiny - assigning an IP to one half of the subnet versus the other automatically determines the entire network path, transparently and without client awareness.