Skip to content

VPN & Tunneling - Street-Level Ops

Quick Diagnosis Commands

When the VPN is down and nobody can access internal resources:

# 1. Is the WireGuard interface up?
wg show
ip link show wg0

# 2. Is the tunnel passing traffic?
wg show wg0
# Check "latest handshake" — if > 2 minutes, something is wrong
# Check "transfer" — should show non-zero rx/tx

# 3. Can you ping across the tunnel?
ping -c 3 10.0.0.1    # ping the VPN server's tunnel IP

# 4. Check routing
ip route show
ip route get 10.0.0.1

# 5. Is the VPN port open?
ss -ulnp | grep 51820         # WireGuard
ss -ulnp | grep 1194          # OpenVPN

# 6. Check firewall
iptables -L -n | grep -i "51820\|wg0"
nft list ruleset 2>/dev/null | grep wg0

# 7. OpenVPN status
systemctl status openvpn
cat /var/log/openvpn-status.log

# 8. IPSec status
ipsec statusall
ipsec status

Pattern: WireGuard Debugging

# Full status with all peers
wg show wg0

# Example output:
# interface: wg0
#   public key: abc123...
#   private key: (hidden)
#   listening port: 51820
#
# peer: def456...
#   endpoint: 203.0.113.5:51820
#   allowed ips: 10.0.0.2/32
#   latest handshake: 32 seconds ago     <-- GOOD (< 2 min)
#   transfer: 1.24 MiB received, 3.45 MiB sent

# If "latest handshake" is missing or very old:
# 1. Key mismatch (most common)
# 2. Endpoint unreachable (firewall/NAT)
# 3. AllowedIPs mismatch between peers

# Debug connectivity
# From client:
ping -c 3 10.0.0.1               # ping server tunnel IP
traceroute 10.0.0.1

# Check if UDP port is reachable
nc -zu vpn.example.com 51820

# Check kernel module
lsmod | grep wireguard
modprobe wireguard

# Enable kernel debug logging (verbose, use briefly)
echo module wireguard +p > /sys/kernel/debug/dynamic_debug/control
dmesg -w | grep wireguard
# Disable when done:
echo module wireguard -p > /sys/kernel/debug/dynamic_debug/control

Gotcha: MTU Inside Tunnels

Tunnels add headers, reducing the effective MTU. If you do not account for this, you get mysterious connectivity issues — small packets work, large ones fail silently.

Standard Ethernet MTU:     1500 bytes
WireGuard overhead:        60 bytes  (40 IPv6 or 20 IPv4 + 8 UDP + 32 WG)
OpenVPN overhead:          ~70 bytes (varies by cipher)
IPSec overhead:            ~60-80 bytes (varies by mode)

Effective MTU through WireGuard: 1500 - 60 = 1440
Effective MTU through OpenVPN:   1500 - 70 = 1430
# Check current MTU
ip link show wg0

# Set correct MTU (WireGuard)
ip link set mtu 1420 dev wg0

# Or in wg0.conf
[Interface]
MTU = 1420

# Test with large packets
ping -c 3 -s 1400 -M do 10.0.0.1    # -M do = don't fragment
# If this fails but ping -s 1200 works, MTU is the problem

# Find the correct MTU
ping -c 1 -s 1400 -M do 10.0.0.1    # try 1400
ping -c 1 -s 1350 -M do 10.0.0.1    # try 1350
# Binary search until you find the max that works

Gotcha: DNS Leaks

You set up a full-tunnel VPN but DNS queries still go to your ISP's resolver, leaking which domains you visit.

# Check which DNS server you are using
cat /etc/resolv.conf
resolvectl status

# Test for DNS leaks
dig +short whoami.akamai.net
# If this returns your ISP's IP, DNS is leaking

# WireGuard fix: set DNS in client config
[Interface]
DNS = 10.0.0.1    # use VPN server's DNS

# OpenVPN fix: push DNS to clients
push "dhcp-option DNS 10.8.0.1"

# systemd-resolved integration (modern Linux)
resolvectl dns wg0 10.0.0.1
resolvectl domain wg0 "~."    # route ALL DNS through this interface

Pattern: OpenVPN Log Reading

# Real-time log monitoring
tail -f /var/log/openvpn.log

# Key log messages and what they mean:

# GOOD: Connection established
# "Initialization Sequence Completed"
# "Peer Connection Initiated with [AF_INET]203.0.113.5:1194"

# BAD: TLS handshake failure
# "TLS Error: TLS key negotiation failed"
# → Certificate mismatch, expired cert, or wrong CA

# BAD: Authentication failure
# "AUTH_FAILED"
# → Wrong credentials or certificate revoked

# BAD: Timeout
# "Connection reset, restarting"
# "Inactivity timeout"
# → Network issue, firewall blocking, or keepalive too aggressive

# Check connected clients (server side)
cat /var/log/openvpn-status.log

# Count connected clients
grep "^CLIENT" /var/log/openvpn-status.log | wc -l

Pattern: SSH Tunnel Quick Setups

# Scenario 1: Access a database behind a bastion
ssh -L 5432:db-server:5432 -N bastion.example.com &
psql -h localhost -p 5432 -U dbuser mydb

# Scenario 2: Access a web app behind a firewall
ssh -L 8080:internal-app:80 -N bastion.example.com &
curl http://localhost:8080

# Scenario 3: SOCKS proxy for browsing internal sites
ssh -D 1080 -N bastion.example.com &
curl --socks5 localhost:1080 http://internal-wiki.corp/

# Scenario 4: Expose local dev server to remote team
ssh -R 8080:localhost:3000 -N shared-server.example.com
# Team accesses shared-server:8080 to reach your localhost:3000

# Scenario 5: Jump through multiple bastions
ssh -J bastion1.example.com,bastion2.example.com internal-server

# Kill background SSH tunnels
ps aux | grep "ssh -[LDNR]" | grep -v grep
kill $(pgrep -f "ssh -L 5432")

Gotcha: Split-Brain Routing

You configure a full-tunnel VPN but forget to exempt the VPN server's own endpoint. The VPN traffic tries to route through the VPN, creating a routing loop. Connection drops immediately.

# Symptom: VPN connects briefly then drops
# Or: VPN connects but no traffic flows

# WireGuard: AllowedIPs = 0.0.0.0/0 captures ALL traffic,
# including the UDP packets to the VPN endpoint itself

# Fix: wg-quick handles this automatically when you set:
[Peer]
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0
# wg-quick adds a specific route for the endpoint via the default gateway

# If using manual ip/wg commands, add the exception yourself:
ip route add 203.0.113.1/32 via 192.168.1.1 dev eth0  # endpoint via real gateway
wg set wg0 peer PUBKEY allowed-ips 0.0.0.0/0           # everything else via VPN
ip route add default dev wg0                            # default route via VPN

Pattern: WireGuard Behind NAT

WireGuard uses UDP, which is NAT-friendly, but you need keepalive to maintain the NAT mapping:

# Client behind NAT: add PersistentKeepalive
[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = vpn.example.com:51820
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25    # send keepalive every 25 seconds

Without PersistentKeepalive, NAT mappings expire (typically 30-120 seconds), and the server can no longer reach the client. The client can still initiate connections, but the server cannot push traffic.


Gotcha: Keepalive Misconfiguration

# OpenVPN: keepalive 10 120
# → Send ping every 10 seconds
# → If no response in 120 seconds, restart connection
# Too aggressive: keepalive 1 5 (floods network, false restarts)
# Too passive: keepalive 60 600 (10 min to detect dead peer)

# WireGuard: PersistentKeepalive = 25
# → Only needed behind NAT
# → 25 seconds is the recommended value
# → Setting to 0 disables (fine if not behind NAT)
# → Setting too low wastes bandwidth on metered connections

Pattern: IPSec Troubleshooting

# Check IKE SA (Phase 1)
ipsec statusall | grep "IKE_SA"

# Check Child SA (Phase 2 — the actual tunnel)
ipsec statusall | grep "CHILD_SA"

# If Phase 1 fails:
# → Check pre-shared key or certificate
# → Check IKE version (ikev1 vs ikev2)
# → Check firewall (UDP 500, UDP 4500 for NAT-T)
# → Check time sync between peers

# If Phase 1 succeeds but Phase 2 fails:
# → Check traffic selectors (subnet mismatch)
# → Check ESP proposal (cipher mismatch)
# → Check firewall (ESP protocol 50)

# Restart IPSec
ipsec restart

# Debug logging
ipsec stroke loglevel ike 4
journalctl -u strongswan -f

# Verify traffic is being encrypted
tcpdump -i eth0 esp
# You should see ESP packets, not plaintext

Pattern: Quick WireGuard Peer Addition

# On the new client: generate keys
wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey

# On the server: add the peer (no restart needed)
wg set wg0 peer $(cat client-publickey) allowed-ips 10.0.0.5/32

# Save the running config to the file
wg-quick save wg0

# On the client: create config and bring up
cat > /etc/wireguard/wg0.conf << 'EOF'
[Interface]
PrivateKey = CLIENT_PRIVATE_KEY
Address = 10.0.0.5/24
DNS = 1.1.1.1

[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = vpn.example.com:51820
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25
EOF

wg-quick up wg0

Gotcha: Firewall Rules for VPN

Common ports and protocols that must be allowed:

WireGuard:   UDP 51820 (or custom port)
OpenVPN:     UDP 1194 (default) or TCP 443 (fallback)
IPSec:       UDP 500 (IKE), UDP 4500 (NAT-T), Protocol 50 (ESP)
SSH Tunnel:  TCP 22

# WireGuard firewall rules (iptables)
iptables -A INPUT -p udp --dport 51820 -j ACCEPT
iptables -A FORWARD -i wg0 -j ACCEPT
iptables -A FORWARD -o wg0 -j ACCEPT
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

# Also enable IP forwarding
sysctl net.ipv4.ip_forward=1
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.d/99-vpn.conf