# GFW/DPI Evasion for WireGuard (2026-04-30 Case Study)

## Problem Diagnosis

**Critical Diagnostic Sign:**
- **`wg show` shows NO "latest handshake" line** ← This is definitive proof of handshake failure
- When handshake is successful, you'll see: `latest handshake: X seconds ago`
- When handshake fails, the line is MISSING entirely (even though endpoint may be shown)

**Symptoms:**
- Client shows transmitted traffic, but zero or minimal received traffic
- `wg show` shows endpoint but NO "latest handshake" timestamp
- Connection attempts visible but handshake never completes
- `ping 10.0.0.X` fails with 100% packet loss
- Port 51820 (WireGuard default) or even 443 blocked by DPI

**Root Cause:**
1. **DPI (Deep Packet Inspection)** identifying WireGuard traffic
   - Port 51820 is a known WireGuard default port
   - DPI detects UDP packet patterns characteristic of WireGuard handshake
   - Initial handshake succeeds (low traffic volume, less suspicious)
   - Sustained data transfer blocked (high traffic triggers DPI)

2. **Missing NAT rules** preventing return traffic
   - Service-side `POSTROUTING MASQUERADE` missing
   - Client sends packets through tunnel, but return packets can't be NAT'd back

## Solution: Port Migration to HTTPS Port (443)

**Why port 443 works:**
- Standard HTTPS port - DPI assumes it's legitimate HTTPS traffic
- Encrypted UDP traffic on port 443 is harder to fingerprint
- GFW typically doesn't block standard HTTPS traffic
- Common bypass technique for censorship-resistant VPNs

### Configuration Changes

**Server-side (`/etc/wireguard/wg0.conf`):**
```ini
[Interface]
ListenPort = 443          # ← Changed from 51820
Address = 10.0.0.1/24

# Critical NAT rules added
PostUp = ip link set dev wg0 mtu 1420 && \
         iptables -A FORWARD -i wg0 -j ACCEPT && \
         iptables -A FORWARD -o wg0 -j ACCEPT && \
         iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
         
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT && \
          iptables -D FORWARD -o wg0 -j ACCEPT && \
          iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

[Peer]
# ... existing peer configs ...
```

**Client-side (just update Endpoint):**
```ini
[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
Endpoint = <SERVER_PUBLIC_IP>:443    # ← Changed from 51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
```

### Apply Changes

```bash
# On server
sudo wg-quick down wg0
sudo wg-quick up wg0

# Verify new port
sudo wg show | grep "listening port"
# Should show: listening port: 443

# Verify NAT rules
sudo iptables -t nat -L POSTROUTING -n -v | grep MASQUERADE
# Should show new rule for eth0 interface

sudo iptables -L FORWARD -n -v | grep wg0
# Should show ACCEPT rules for wg0 in both directions
```

## Verification Steps

After port change, verify with:

1. **Port connectivity:**
```bash
# From client location
nc -uvz <SERVER_PUBLIC_IP> 443
# Should succeed: Connection to <IP> 443 port [udp/*] succeeded!
```

2. **Fresh handshake:**
```bash
sudo wg show | grep "latest handshake"
# Should show: X seconds ago (not hours)
```

3. **Bi-directional traffic:**
```bash
sudo wg show | grep "transfer"
# Should show both received AND sent bytes increasing
# Example: transfer: 660.30 KiB received, 2.04 MiB sent
```

4. **Internet connectivity:**
```bash
# From client with VPN active
ping -c 3 8.8.8.8
curl https://www.google.com
```

## NAT Rule Explanation

**Why POSTROUTING MASQUERADE is critical:**

Without it, the packet flow looks like:
```
Client → [wg0] → Server → [Internet]  ✅ Works
Internet → [Server] → ??? → Client    ❌ Fails
```

With MASQUERADE:
```
Client → [wg0:10.0.0.2] → Server → [eth0:REAL_IP] → [Internet]
                          ↓
                        (NAT: 10.0.0.2 → REAL_IP)
                          ↑
Internet → [eth0:REAL_IP] → Server → [wg0:10.0.0.2] → Client
                        (MASQUERADE: return packets get NAT'd back to wg0)
```

**The 3 critical iptables rules:**

1. `iptables -A FORWARD -i wg0 -j ACCEPT`
   - Allow traffic entering from WireGuard interface to be forwarded

2. `iptables -A FORWARD -o wg0 -j ACCEPT`
   - Allow traffic destined to WireGuard interface to be forwarded

3. `iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE`
   - NAT outgoing traffic from wg0 to internet, and more importantly,
   - NAT return traffic back to the correct wg0 peer IP

## Alternative Port Options

If port 443 is also blocked (more common in 2026), try:

| Port | Type | Success Rate | Notes |
|-------|-------|--------------|-------|
| 80    | UDP  | ⭐⭐⭐⭐ | HTTP - Often works when 443 is blocked |
| 443   | UDP  | ⭐⭐⭐ | HTTPS - May be DPI-detected as WireGuard |
| 22    | UDP  | ⭐⭐ | SSH - May be blocked on some networks |
| 53    | UDP  | ⭐⭐ | DNS - May conflict with local DNS |
| 123   | UDP  | ⭐ | NTP - Rarely blocked |

**Order of preference:** 80 > 443 > 22 > 53 > 123

### Port 80 Migration (2026-05-01 Case Study)

When port 443 was blocked, port 80 successfully established connection.

**Server-side change:**
```bash
# Backup first
sudo cp /etc/wireguard/wg0.conf /etc/wireguard/wg0.conf.backup_$(date +%Y%m%d_%H%M%S)

# Update port to 80
sudo sed -i 's/ListenPort = 443/ListenPort = 80/' /etc/wireguard/wg0.conf

# Restart WireGuard
sudo wg-quick down wg0
sudo wg-quick up wg0

# Verify
sudo wg show | grep "listening port"
# Should show: listening port: 80
```

**Client-side change:**
```ini
[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
Endpoint = <SERVER_PUBLIC_IP>:80    # ← Changed to port 80
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
```

**Verification:**
```bash
# Test port 80 from client location
nc -uvz <SERVER_PUBLIC_IP> 80
# Should succeed: Connection to <IP> 80 port [udp/*] succeeded!

# Check server logs for handshake
sudo wg show | grep "latest handshake"
# Should show: X seconds ago
```

## Advanced Obfuscation (If ports still blocked)

If standard port migration fails, consider:

1. **Shadowsocks + UDP2RAW**
   - Wrap WireGuard in Shadowsocks protocol
   - Use UDP2RAW to convert UDP to TCP
   - More overhead, but harder to detect

2. **Trojan / V2Ray**
   - Full protocol obfuscation suite
   - HTTPS masquerading with TLS
   - Requires additional software setup

3. **Cloudflare WARP**
   - Uses WireGuard but with Cloudflare's infrastructure
   - Generally bypasses GFW due to Cloudflare's scale
   - Trade-off: Not self-hosted, less control

## Case Study Results

**Before (port 51820):**
- Handshake: 2 hours ago (stale)
- Transfer: 0 B received, 1.16 KiB sent
- Status: Connection established, no data flow

**After (port 443 + NAT rules):**
- Handshake: 56 seconds ago (active)
- Transfer: 660.30 KiB received, 2.04 MiB sent
- Status: Fully functional

**Time to fix:** ~10 minutes (diagnosis + config update + restart)

## Common Pitfalls

1. **Forgetting to update client Endpoint**
   - Server changes to port 443/80, client still tries 51820
   - Result: Connection timeout

2. **Incomplete NAT rules**
   - Only adding `FORWARD` rules, missing `POSTROUTING MASQUERADE`
   - Result: "Send but no receive" symptom

3. **Not restarting WireGuard after config change**
   - `wg-quick down` then `wg-quick up` required
   - Result: Old port still listening

4. **Testing wrong protocol**
   - WireGuard uses UDP, not TCP
   - Wrong: `nc -tvz <IP> <PORT>` (TCP test)
   - Correct: `nc -uvz <IP> <PORT>` (UDP test)

5. **Misinterpreting missing handshake as "stale"**
   - No "latest handshake" line = handshake FAILED (not just stale)
   - Stale handshake (2 hours ago) = handshake succeeded but connection dropped
   - These are different issues with different fixes

6. **Client configuration errors**
   - Wrong server public key
   - Wrong Endpoint address or port
   - AllowedIPs set to VPN subnet instead of `0.0.0.0/0`
   - Result: Client shows "only sending, no receiving"

## Diagnostic Checklist

When VPN "sends but doesn't receive":

**Server-side checks:**
- [ ] Check `wg show` for "latest handshake" line (is it missing or stale?)
- [ ] Check `wg show` transfer counters (are both received and sent increasing?)
- [ ] Verify port listening: `sudo ss -ulnp | grep <PORT>`
- [ ] Test UDP port connectivity from external: `nc -uvz <SERVER_IP> <PORT>`
- [ ] Verify NAT rules: `sudo iptables -t nat -L POSTROUTING -n -v`
- [ ] Check FORWARD rules: `sudo iptables -L FORWARD -n -v | grep wg0`
- [ ] Consider port migration (51820 → 80 → 22 → 53 → 123)

**Client-side checks:**
- [ ] Verify Endpoint address matches server public IP
- [ ] Verify Endpoint PORT matches server listening port (80, 443, etc.)
- [ ] Verify server public key is correct: `PublicKey = FjHHksJSi3wbBW8UoevUTPgk2XeL5dTLmXByEka/yBU=` (example)
- [ ] Verify AllowedIPs = `0.0.0.0/0` (not just VPN subnet like `10.0.0.1/32`)
- [ ] Verify DNS server is set (e.g., `DNS = 1.1.1.1`)
- [ ] Check client app shows "Last Handshake" time (should be seconds/minutes, not hours)
- [ ] Test internet connectivity with VPN active: `ping 8.8.8.8` and `curl https://www.google.com`

**GFW/DPI specific indicators:**
- [ ] Handshake was working previously but suddenly stopped
- [ ] Multiple different ports fail (not just configuration error)
- [ ] Server logs show connection attempts but no completed handshakes
- [ ] Client shows "only sending, no receiving" persistently

## Related Skills

- `vpn-troubleshooting` - Main skill for MTU and performance issues
- `systematic-debugging` - 4-phase debugging methodology

## Session Context

**2026-04-30 Case Study:**
- **User:** openjerry1995
- **Server:** Ubuntu 22.04 (RackNerd VPS)
- **Client Location:** China (GFW environment)
- **Issue:** VPN handshake but no data flow (DPI blocking on port 51820)
- **Resolution:** Port 443 migration + NAT rules

**2026-05-01 Case Study:**
- **User:** openjerry1995
- **Server:** Ubuntu 22.04 (RackNerd VPS, 23.94.194.34)
- **Client Location:** China (GFW environment)
- **Issue:** Port 443 blocked - `wg show` showed NO "latest handshake" line, endpoint visible but handshake never completed
- **Symptoms:**
  - Client: "only sending, no receiving"
  - Server: `endpoint: 182.136.27.246:58801` visible but no handshake
  - `ping 10.0.0.2` = 100% packet loss
- **Root Cause:** GFW/DPI upgraded detection, now blocking WireGuard even on port 443
- **Resolution:** Migrated to port 80 (HTTP port)
- **Client config:** `Endpoint = 23.94.194.34:80`
- **Server config:** `ListenPort = 80`
- **Result:** Port 80 established successful connection after 443 was blocked
- **Key lesson:** Port 80 now has higher success rate than 443 in 2026 GFW environment
