📑 Complete Table of Contents
- Project File Overview
- internal.conf - DNS Configuration
- wg0.conf - WireGuard Configuration
- wg0-up.sh - Initialization (Lines 1-41)
- Namespace Routes (Lines 42-47)
- DNS Configuration (Lines 49-52)
- OpenVPN Startup (Lines 54-58)
- Policy Routing (Lines 66-80)
- DNS Marking (Lines 82-102)
- NAT Configuration (Lines 104-145)
- Forwarding Rules (Lines 147-165)
- MSS Clamping (Lines 167-171)
- Startup Messages (Lines 173-185)
- wg0-down.sh - Teardown Script
- Technical Summary
📂 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
Maps "vault.raff.local" to 10.4.0.7. Split-horizon DNS for internal services.
Maps "rafflab.internal" to 10.4.0.3. Second internal service hostname.
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: Don't read /etc/resolv.conf; use only explicit server= lines
strict-order: Use upstream servers in order, don't randomize
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 section: Contains server-side WireGuard configuration
Server's private key: Cryptographic basis for all WireGuard connections. Must be kept secret. Generated with: wg genkey
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.
UDP listen port: Non-standard port for WireGuard. Must be open in firewall. Clients must know this exact port.
Maximum transmission unit: Reduced from standard 1500 to account for encryption overhead. Prevents fragmentation in VPN tunnels.
Disable automatic routing: WireGuard won't create automatic routes. Custom split-tunnel routing handled manually via iptables and policy routing.
Startup hook: Automatically executes wg0-up.sh after interface is up. This is where the complex networking setup happens.
Shutdown hook: Automatically executes wg0-down.sh before interface is destroyed. Performs cleanup and restoration.
Peer configurations: Each client is a peer with public key and allowed IP range. These determine IP allocation and routing.
🚀 wg0-up.sh - Setup Script Part 1: Initialization
File: wg0-up.sh - Lines 1-41
Bash shebang and error handling: Script interpreted as Bash. set -e exits on first error (fail-fast mode).
Global Variables (Lines 4-19)
All critical configuration in one place. These variables define IP ranges, interface names, and VPN paths. Changing these reconfigures the entire system.
Enable IP forwarding: Critical for routing. Allows kernel to forward packets between interfaces.
Idempotent namespace creation: Only creates namespace if it doesn't exist. Safe to run multiple times.
Idempotent veth creation: Creates virtual ethernet pair connecting host to namespace. Only if not already created.
Move namespace end of veth: Moves veth-vpn interface into the namespace. Can only be done once.
Configure and bring up host veth end: Assigns 10.200.1.1/24 to veth-wg and activates it.
Configure namespace interfaces: Assigns 10.200.1.2/24 to veth-vpn, brings it up, and activates loopback.
Enable forwarding inside namespace: Allows namespace to route packets.
Temporary namespace default route: Temporary before OpenVPN starts. Will be overridden when tun0 is created.
🛣️ Namespace Static Routes (Lines 42-47)
Route to OpenVPN provider server: Inside namespace, explicitly routes VPN server IP through veth back to host. Prevents routing loop when OpenVPN connects.
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)
Create namespace config directory: Linux allows per-namespace config files in /etc/netns/[namespace-name]/
Namespace DNS configuration: Processes inside namespace read this file for DNS resolution. Points to Google DNS.
🚀 OpenVPN Startup (Lines 54-58)
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 termination2>/dev/null || true: Suppress errors, continue anyway
🛣️ Policy-Based Routing Setup (Lines 66-80)
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.
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.
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.
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)
Mark dnsmasq upstream queries: When dnsmasq sends DNS queries to upstream servers, mark them with fwmark 0x100. Different mark than VPN client traffic.
Route DNS queries through table 200: Marked DNS queries also use table 200, routing them into the namespace/VPN for privacy.
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).
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)
NAT for namespace→internet: Traffic from namespace (10.200.1.0/24) exiting to internet masked behind AWS IP (10.0.1.191).
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)
Allow WireGuard peer-to-peer: Traffic between WireGuard clients forwarded directly between them.
Allow VPN clients→namespace: VPN client traffic (10.4.0.128-255) can be forwarded to namespace.
Allow namespace→VPN clients (replies): Response packets from namespace destined for VPN clients can be forwarded. Stateful filtering ensures only legitimate replies.
Allow namespace→internet: Namespace can forward traffic to internet.
Allow internet→namespace (replies): Responses from internet to namespace (stateful).
Allow direct-internet clients: Non-VPN clients can access internet directly, and receive responses.
🔗 MSS Clamping (Lines 167-171)
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.
🛑 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)
Same as wg0-up.sh: Bash shebang and error exit mode.
Variable redefinition: Same variables as wg0-up.sh. MUST match exactly or cleanup fails.
OpenVPN Shutdown (Lines 17-21)
Stop OpenVPN: If PID file exists, kill the process and wait for graceful shutdown. MUST be done before deleting namespace.
Namespace and Host Cleanup
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
Remove policy routing rules: Deletes all rules that determined which traffic went through which routing table.
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
- Namespace Isolation: OpenVPN in isolated namespace prevents routing conflicts
- Packet Marking: fwmark enables routing based on packet properties
- Policy Routing: RPDB rules enable source-based routing decisions
- IP Range Split: IP assignment determines routing automatically
- Stateful Filtering: Connection tracking prevents spoofed replies
- DNS Privacy: Routing DNS queries through VPN provides privacy
- Local Exclusions: Policy rules ensure internal traffic never routes through VPN
- 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.