Files
vpn/IMPLEMENTATION.md
mguschin f14d4f8f33 Migrate to pure nftables routing (remove iptables/ipset)
- Replace hybrid iptables/ipset/nftables approach with pure nftables
- Add nftables native set for Russian IP ranges (populated from RIPE)
- Create update-direct-routes.sh script to load IP ranges from RIPE database
- Remove ipset and iptables dependencies from postup.sh/postdown.sh
- Add automatic weekly cron job for IP range updates
- Update all documentation to reflect the new approach

Benefits:
- More reliable: no iptables/nftables conflicts
- Simpler debugging: single tool for all rules (nft list ruleset)
- Atomic rule loading: prevents partial failures
- IP-based routing is more predictable than DNS-based

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-19 18:02:28 +03:00

13 KiB

Implementation Plan

Prerequisites

  • SSH access to both VDS servers (RU: 176.124.216.197, DE: 194.31.173.178)
  • Root or sudo privileges on both servers
  • Basic firewall rules allowing SSH access

Phase 1: DE VDS Setup (Exit Node)

The simpler node - just accepts traffic from RU VDS and NATs it to the internet.

Step 1.1: Install packages

apt update && apt install -y wireguard nftables

Step 1.2: Enable IP forwarding

echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.d/99-vpn.conf
sysctl -p /etc/sysctl.d/99-vpn.conf

Step 1.3: Generate WireGuard keys

mkdir -p /etc/wireguard/keys
chmod 700 /etc/wireguard/keys
wg genkey | tee /etc/wireguard/keys/server.key | wg pubkey > /etc/wireguard/keys/server.pub
chmod 600 /etc/wireguard/keys/*

Step 1.4: Create WireGuard config

Create /etc/wireguard/wg0.conf:

[Interface]
Address = 10.20.0.2/30
ListenPort = 51821
PrivateKey = <DE_SERVER_PRIVATE_KEY>
PostUp = nft -f /etc/nftables.conf
PostDown = nft flush ruleset

[Peer]
# RU VDS
PublicKey = <RU_SERVER_PUBLIC_KEY>
AllowedIPs = 10.20.0.1/32, 10.10.0.0/24

Step 1.5: Configure nftables

Create /etc/nftables.conf:

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        # Allow established connections
        ct state established,related accept

        # Allow loopback
        iif lo accept

        # Allow SSH (adjust port if needed)
        tcp dport 22 accept

        # Allow WireGuard from RU VDS only
        ip saddr 176.124.216.197 udp dport 51821 accept

        # Allow ICMP
        icmp type echo-request accept
    }

    chain forward {
        type filter hook forward priority 0; policy drop;

        # Allow forwarding from VPN
        iifname "wg0" accept

        # Allow established connections back
        ct state established,related accept
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}

table inet nat {
    chain postrouting {
        type nat hook postrouting priority 100;

        # NAT traffic from VPN to internet
        oifname != "wg0" ip saddr { 10.10.0.0/24, 10.20.0.0/30 } masquerade
    }
}

Step 1.6: Enable and start services

systemctl enable --now nftables
systemctl enable --now wg-quick@wg0

Step 1.7: Verify

wg show
ip addr show wg0

Phase 2: RU VDS Setup (Gateway)

The main node - handles user connections, IP-based routing decisions.

Step 2.1: Install packages

apt update && apt install -y wireguard dnsmasq nftables qrencode curl bc

Step 2.2: Enable IP forwarding

echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.d/99-vpn.conf
sysctl -p /etc/sysctl.d/99-vpn.conf

Step 2.3: Generate WireGuard keys

mkdir -p /etc/wireguard/keys
chmod 700 /etc/wireguard/keys

# Server key for user-facing interface
wg genkey | tee /etc/wireguard/keys/server.key | wg pubkey > /etc/wireguard/keys/server.pub

# Key for DE tunnel
wg genkey | tee /etc/wireguard/keys/de-tunnel.key | wg pubkey > /etc/wireguard/keys/de-tunnel.pub

chmod 600 /etc/wireguard/keys/*

Step 2.4: Create routing tables

Add to /etc/iproute2/rt_tables:

200 proxy

Step 2.5: Create WireGuard configs

Create /etc/wireguard/wg0.conf (user-facing):

[Interface]
Address = 10.10.0.1/24
ListenPort = 51820
PrivateKey = <RU_SERVER_PRIVATE_KEY>
PostUp = /etc/wireguard/postup.sh
PostDown = /etc/wireguard/postdown.sh

# Users will be added here as [Peer] sections

Create /etc/wireguard/wg1.conf (DE tunnel):

[Interface]
Address = 10.20.0.1/30
PrivateKey = <RU_DE_TUNNEL_PRIVATE_KEY>

[Peer]
# DE VDS
PublicKey = <DE_SERVER_PUBLIC_KEY>
Endpoint = 194.31.173.178:51821
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25

Step 2.6: Create PostUp script

Create /etc/wireguard/postup.sh:

#!/bin/bash
set -e

# Load nftables rules (includes the 'direct' set and packet marking)
nft -f /etc/nftables.conf

# Add default route via DE tunnel for 'proxy' table
ip route add default via 10.20.0.2 dev wg1 table proxy 2>/dev/null || true

# Policy routing: packets with fwmark 0x1 use 'proxy' table
ip rule add from 10.10.0.0/24 fwmark 0x1 table proxy priority 100 2>/dev/null || true

echo "PostUp script completed successfully"

Make executable:

chmod +x /etc/wireguard/postup.sh

Step 2.7: Create PostDown script

Create /etc/wireguard/postdown.sh:

#!/bin/bash

# Remove policy routing rule
ip rule del from 10.10.0.0/24 fwmark 0x1 table proxy priority 100 2>/dev/null || true

# Flush routing table
ip route flush table proxy 2>/dev/null || true

# Flush nftables vpn-routing table
nft flush table ip vpn-routing 2>/dev/null || true

echo "PostDown script completed"

Make executable:

chmod +x /etc/wireguard/postdown.sh

Step 2.8: Configure nftables

Create /etc/nftables.conf:

#!/usr/sbin/nft -f
#
# RU VDS nftables configuration
# Pure nftables - no iptables/ipset dependencies
#

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        ct state established,related accept
        iif lo accept
        tcp dport 22 accept
        udp dport 51820 accept
        iifname "wg0" udp dport 53 accept
        iifname "wg0" tcp dport 53 accept
        icmp type echo-request accept
    }

    chain forward {
        type filter hook forward priority 0; policy drop;

        iifname "wg0" accept
        iifname "wg1" accept
        ct state established,related accept
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}

table ip vpn-routing {
    # Set for Russian IPs (direct routing, no proxy)
    # Populated by update-direct-routes.sh script
    set direct {
        type ipv4_addr
        flags interval, timeout
        timeout 6h
    }

    # Packet marking for policy routing
    chain prerouting {
        type filter hook prerouting priority mangle; policy accept;

        # Only process traffic from VPN clients
        ip saddr != 10.10.0.0/24 return

        # Destinations in 'direct' set: no mark (direct routing)
        ip daddr @direct return

        # Everything else: mark for proxy routing
        meta mark set 0x1
    }
}

table inet nat {
    chain postrouting {
        type nat hook postrouting priority 100;

        oifname != "wg0" oifname != "wg1" ip saddr 10.10.0.0/24 masquerade
    }
}

Step 2.9: Configure dnsmasq

Disable systemd-resolved if running:

systemctl disable --now systemd-resolved
rm /etc/resolv.conf
echo "nameserver 8.8.8.8" > /etc/resolv.conf

Create /etc/dnsmasq.d/vpn-routing.conf:

# dnsmasq configuration for VPN
# Routing is handled by nftables based on IP ranges, not DNS

# Listen only on VPN interface
interface=wg0
bind-interfaces

# Upstream DNS
server=8.8.8.8
server=8.8.4.4
server=1.1.1.1

# Don't read /etc/resolv.conf
no-resolv

# Cache size
cache-size=10000

Step 2.10: Create Russian IP ranges update script

Create /etc/wireguard/update-direct-routes.sh:

#!/bin/bash
#
# Downloads Russian IP ranges from RIPE and loads them into nftables
# Run after services are started, and weekly via cron
#

set -e

RIPE_URL="https://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-extended-latest"
TEMP_FILE="/tmp/ripe-delegated.txt"

echo "Downloading RIPE delegation data..."
curl -s "$RIPE_URL" -o "$TEMP_FILE"

echo "Parsing Russian IP allocations..."
RU_RANGES=$(grep '|RU|ipv4|' "$TEMP_FILE" | while IFS='|' read -r _ _ _ start count _ _ _; do
    if [[ "$count" =~ ^[0-9]+$ ]] && [ "$count" -gt 0 ]; then
        prefix=$(echo "32 - l($count)/l(2)" | bc -l | cut -d. -f1)
        echo "$start/$prefix"
    fi
done)

echo "Flushing existing 'direct' set..."
nft flush set ip vpn-routing direct 2>/dev/null || true

echo "Adding ranges to nftables..."
echo "$RU_RANGES" | while read -r cidr; do
    [[ -n "$cidr" ]] && nft add element ip vpn-routing direct { "$cidr" } 2>/dev/null || true
done

rm -f "$TEMP_FILE"
echo "Done. Russian IP ranges loaded."

Make executable and set up cron:

chmod +x /etc/wireguard/update-direct-routes.sh

# Weekly updates
cat > /etc/cron.weekly/update-vpn-routes << 'EOF'
#!/bin/bash
/etc/wireguard/update-direct-routes.sh >> /var/log/vpn-routes-update.log 2>&1
EOF
chmod +x /etc/cron.weekly/update-vpn-routes

Step 2.11: Enable and start services

systemctl enable nftables dnsmasq wg-quick@wg0 wg-quick@wg1
systemctl start dnsmasq
systemctl start wg-quick@wg1
systemctl start wg-quick@wg0

Step 2.12: Load Russian IP ranges

After the tunnel is up:

/etc/wireguard/update-direct-routes.sh

Step 2.13: Verify

wg show
ip route show table proxy
ip rule show
nft list set ip vpn-routing direct | head -20

Phase 3: Key Exchange

After generating keys on both servers, exchange public keys:

On DE VDS:

cat /etc/wireguard/keys/server.pub
# Copy this value

On RU VDS:

cat /etc/wireguard/keys/server.pub
cat /etc/wireguard/keys/de-tunnel.pub
# Copy these values

Update configs:

  1. DE VDS /etc/wireguard/wg0.conf: Replace <RU_SERVER_PUBLIC_KEY> with RU's de-tunnel.pub
  2. RU VDS /etc/wireguard/wg1.conf: Replace <DE_SERVER_PUBLIC_KEY> with DE's server.pub

Restart WireGuard:

# On DE VDS
systemctl restart wg-quick@wg0

# On RU VDS
systemctl restart wg-quick@wg1

Verify tunnel:

# On RU VDS
ping 10.20.0.2
wg show wg1

Phase 4: Add First Client

Step 4.1: Generate client keys (on RU VDS)

CLIENT_NAME="phone"
wg genkey | tee /etc/wireguard/keys/client_${CLIENT_NAME}.key | wg pubkey > /etc/wireguard/keys/client_${CLIENT_NAME}.pub
chmod 600 /etc/wireguard/keys/client_${CLIENT_NAME}.*

Step 4.2: Add peer to server

CLIENT_IP="10.10.0.2"
CLIENT_PUBKEY=$(cat /etc/wireguard/keys/client_${CLIENT_NAME}.pub)

wg set wg0 peer ${CLIENT_PUBKEY} allowed-ips ${CLIENT_IP}/32
wg-quick save wg0

Step 4.3: Create client config

CLIENT_NAME="phone"
CLIENT_IP="10.10.0.2"
SERVER_PUBKEY=$(cat /etc/wireguard/keys/server.pub)
CLIENT_PRIVKEY=$(cat /etc/wireguard/keys/client_${CLIENT_NAME}.key)

cat > /etc/wireguard/clients/${CLIENT_NAME}.conf << EOF
[Interface]
PrivateKey = ${CLIENT_PRIVKEY}
Address = ${CLIENT_IP}/32
DNS = 10.10.0.1

[Peer]
PublicKey = ${SERVER_PUBKEY}
Endpoint = 176.124.216.197:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
EOF

mkdir -p /etc/wireguard/clients
chmod 600 /etc/wireguard/clients/${CLIENT_NAME}.conf

Step 4.4: Transfer to client

Display as QR code (for mobile):

apt install -y qrencode
qrencode -t ansiutf8 < /etc/wireguard/clients/${CLIENT_NAME}.conf

Or copy the file contents manually.


Phase 5: Testing

Test 1: Basic connectivity

# From client
ping 10.10.0.1  # Should work - RU VDS
ping 10.20.0.2  # Should work - DE VDS

Test 2: DNS resolution

# From client
nslookup google.com 10.10.0.1
nslookup yandex.ru 10.10.0.1

Test 3: Routing verification

# On RU VDS - check ipset after client visits some .ru sites
ipset list direct

# From client - check external IP
curl ifconfig.me        # Should show DE VDS IP (194.31.173.178)
curl ifconfig.me --resolve ifconfig.me:80:$(dig +short yandex.ru | head -1)  # Won't work, but...

# Better test - check where traffic goes
curl https://2ip.ru    # Russian service, should go direct, show RU VDS IP
curl https://ifconfig.me  # Should show DE VDS IP

Test 4: Check that .ru domains go direct

# From client
traceroute yandex.ru   # Should not go through DE
traceroute google.com  # Should go through DE (you'll see 10.20.0.x hop)

Troubleshooting

WireGuard not connecting

# Check if service is running
systemctl status wg-quick@wg0

# Check for errors
journalctl -u wg-quick@wg0 -e

# Verify port is open
ss -ulnp | grep 51820

DNS not working

# Check dnsmasq
systemctl status dnsmasq
journalctl -u dnsmasq -e

# Test locally
dig @127.0.0.1 google.com

Routing not working

# Check nftables set
nft list set ip vpn-routing direct

# Check routing table
ip route show table proxy
ip rule show

# Check nftables rules
nft list chain ip vpn-routing prerouting

# Test packet marking (watch counters)
nft list ruleset | grep -A5 "chain prerouting"

Traffic not NATed

# Check nftables
nft list ruleset

# Check forwarding
cat /proc/sys/net/ipv4/ip_forward

Summary Checklist

  • DE VDS

    • WireGuard installed
    • IP forwarding enabled
    • Keys generated
    • wg0.conf configured
    • nftables configured
    • Services enabled and started
  • RU VDS

    • WireGuard installed
    • dnsmasq installed
    • nftables installed
    • IP forwarding enabled
    • Keys generated
    • Routing table 'proxy' added
    • wg0.conf configured (users)
    • wg1.conf configured (DE tunnel)
    • postup.sh / postdown.sh created
    • nftables configured (with vpn-routing table)
    • dnsmasq configured
    • Services enabled and started
    • Russian IP ranges loaded (update-direct-routes.sh)
  • Key Exchange

    • DE public key → RU wg1.conf
    • RU de-tunnel public key → DE wg0.conf
    • Tunnel verified (ping 10.20.0.2 from RU)
  • First Client

    • Keys generated
    • Peer added to wg0
    • Client config created
    • Connection tested
    • Routing verified