Get Rewarded! We will reward you with up to €50 credit on your account for every tutorial that you write and we publish!

Making Your Website Accessible from Restricted Regions

profile picture
Author
Yusuf Khasbulatov
Published
2026-02-27
Time to read
21 minutes reading time

About the author- DevOps engineer focused on self-hosted infrastructure and security hardening

The Problem

If you host a website or web application on popular hosting providers (e.g. Hetzner), users in certain regions may be unable to access it.

Some ISPs, operating under government directives, use deep packet inspection (DPI) devices that systematically throttle connections to foreign hosting providers. Rather than outright blocking, they limit TCP connections to approximately 16KB per connection, making websites effectively unusable. The HTML page might partially load, but CSS, JavaScript, images, and fonts all fail — leaving users with a broken, unstyled page.

This isn't a problem you can solve by asking your users to "just use a VPN." Your website should work transparently, without requiring any special action from visitors.

This guide walks you through a battle-tested solution for exactly this problem.

The Solution: Reverse Proxy + AmneziaWG Tunnel

The architecture that actually works:

┌──────────────┐         ┌──────────────────┐          ┌──────────────────┐
│  User in     │  HTTPS  │  In-Region VPS   │  AWG     │  Your Origin     │
│  restricted  │────────:arrow_forward:│  (Nginx reverse  │─tunnel─:arrow_forward:│  Server          │
│  region      │         │   proxy)         │  (UDP)   │  (e.g. Hetzner)  │
└──────────────┘         └──────────────────┘          └──────────────────┘
      No DPI issues            │                            │
      (domestic traffic)       │    Encrypted tunnel        │
                               │    disguised as QUIC       │
                               │    DPI can't inspect       │

Why this works:

  1. User → In-Region VPS: Domestic traffic within the same country — no throttling
  2. In-Region VPS → Your origin: Traffic flows through an AmneziaWG tunnel on UDP port 443, which looks like QUIC (HTTP/3) to DPI systems. The DPI can't identify the destination or inspect the content
  3. Completely transparent — your users don't need to install anything or change any settings

What is AmneziaWG?
AmneziaWG is a fork of WireGuard that adds DPI-resistant obfuscation. It modifies packet headers, randomizes handshake sizes, and can disguise traffic as common UDP protocols like QUIC. It was built specifically to defeat sophisticated DPI systems.

Key parameters:
  • Jc, Jmin, Jmax — Sends junk packets before the handshake to blur the traffic pattern
  • S1, S2 — Adds random data to handshake packets, changing their recognizable size
  • H1-H4 — Replaces WireGuard's well-known magic headers with random values
  • I1(Critical) Prepends a real protocol signature (e.g., a QUIC Initial packet) to the handshake. Without this, sophisticated DPI still detects and kills the tunnel within seconds

Who This Tutorial Is For

This tutorial is for developers and operators who host web applications on mainstream providers (e.g. Hetzner) and need to make them accessible to users in regions with ISP-level DPI throttling — without migrating their entire infrastructure.

Why not just host the website inside the restricted region?

Since this solution requires renting a small VPS inside the restricted region anyway, a natural question is: why not skip the complexity and host the application there directly?

Description
Your application likely needs more resources than a cheap regional VPS offers. The in-region VPS in this guide only needs 1 vCPU and 1GB RAM — it runs nothing but Nginx and a tunnel endpoint. Your actual application, with its database, background workers, build tools, and runtime, likely needs significantly more. Mainstream providers give you 4-8x the resources for the same price, along with managed databases, object storage, automated backups, monitoring, and mature deployment tooling (Docker, CI/CD, APIs) that smaller regional providers typically lack.
Hosting your data inside a restricted region creates legal exposure. If your application and database live on a server inside the restricted country, local authorities can physically access, seize, or compel disclosure of that data. With the reverse proxy architecture described here, the in-region VPS is completely stateless — it holds no application data, no database, no user information. If it were seized or inspected, there's nothing to find beyond an Nginx config and a tunnel endpoint. Your actual data remains on infrastructure in a jurisdiction you're more comfortable with.
A note on tunnel security: The AmneziaWG tunnel is configured with AllowedIPs = 10.10.0.2/32, meaning the in-region VPS can only send traffic to a single IP on a private subnet. It cannot scan your origin server's network or access other services. The tunnel is point-to-point and encrypted — it does not grant the in-region VPS any privileged access beyond what Nginx proxies through it. That said, the in-region VPS does terminate user-facing TLS, so it can observe unencrypted request/response content in transit. If this is a concern, consider end-to-end encryption at the application layer.
One in-region VPS can serve as a proxy for multiple projects. The reverse proxy architecture is not limited to a single website. You can configure Nginx on the same in-region VPS with multiple server blocks, each proxying a different domain to a different origin server — all through the same AmneziaWG tunnel (or separate tunnels to different origins). This means a single $4-10/month VPS can make dozens of your projects accessible from the restricted region, making the per-project cost negligible.

What Doesn't Work

Before diving into the solution, here's what we tried and why it failed — so you don't waste time going down these paths.

Description
CDN Services (Cloudflare, Gcore, etc.) Major CDN providers' IP ranges are often throttled by the same DPI systems. Even CDN providers with Points of Presence inside the restricted region may still route traffic through monitored infrastructure. Cloudflare's use of TLS ECH (Encrypted Client Hello) is also specifically targeted by some DPI implementations.
Simple Reverse Proxy on an In-Region VPS (Partial) ⚠️ Renting a VPS inside the restricted region and setting up Nginx as a reverse proxy solves the client → proxy path (domestic traffic isn't throttled), but the proxy → your origin server connection still travels through the DPI infrastructure. Since your origin server is on a "foreign" hosting provider, the proxy-to-origin traffic gets the same 16KB throttle.

This was a frustrating discovery — the proxy successfully receives full responses from the origin, but the DPI sitting between the in-region VPS and the outside world still throttles the connection.
Standard WireGuard VPN Tunnel WireGuard has fixed packet headers and predictable packet sizes that create an easily recognizable signature. Modern DPI systems identify and block WireGuard connections within seconds.

Prerequisites

You'll need:

  • Your origin server running your web application (this guide assumes Linux with a reverse proxy like Traefik, Caddy, or Nginx)
  • A VPS inside the restricted region from a provider that accepts international payment (see section below)
  • A domain name with DNS you can modify
  • Basic SSH and Linux command line knowledge
Finding a VPS Provider Inside the Restricted Region
This is often the hardest part. Most local hosting providers in restricted regions only accept domestic payment methods. Look for providers that accept:
  • International Visa/Mastercard (sometimes through third-party payment processors)
  • Cryptocurrency
  • PayPal
Minimum specs needed: 1 vCPU, 1GB RAM, 20GB disk. This server only runs Nginx and the tunnel endpoint — no application workload.

Step 1 - Set Up the In-Region VPS

SSH into your new VPS and install the essentials:

sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx certbot python3-certbot-nginx

Step 2 - DNS Configuration

Point your domain (or subdomain) to the in-region VPS IP address:

yourapp.example.com  →  A record  →  <IN_REGION_VPS_IP>

If you have other subdomains (API, monitoring, etc.) that don't need access from the restricted region, leave them pointing to your origin server.

Step 3 - Obtain SSL Certificate on the In-Region VPS

sudo certbot --nginx -d yourapp.example.com

This gives you a Let's Encrypt certificate for the client-facing HTTPS connection.

Step 4 - Configure Nginx Reverse Proxy

Create the Nginx configuration. We'll initially point it to the origin server's public IP — we'll switch it to the tunnel IP later.

First, define your domain and origin server IP:

DOMAIN=yourapp.example.com
ORIGIN_IP=123.123.123.123

Then create the config file:

cat > /etc/nginx/sites-available/your-site << EOF
server {
    listen 80;
    server_name ${DOMAIN};

    # Pass ACME challenges through to your origin server
    # so its own Let's Encrypt cert can still auto-renew
    location /.well-known/acme-challenge/ {
        proxy_pass http://${ORIGIN_IP};
        proxy_set_header Host ${DOMAIN};
    }

    location / {
        return 301 https://\$host\$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name ${DOMAIN};

    ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;

    # Cache fonts (30 days, immutable)
    location ~* \.(woff|woff2|ttf|otf|eot) {
        proxy_pass https://${ORIGIN_IP}:443;
        proxy_ssl_server_name on;
        proxy_ssl_name ${DOMAIN};
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # Cache images, CSS, JS (7 days)
    location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|css|js) {
        proxy_pass https://${ORIGIN_IP}:443;
        proxy_ssl_server_name on;
        proxy_ssl_name ${DOMAIN};
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
        expires 7d;
        add_header Cache-Control "public";
    }

    # Main reverse proxy
    location / {
        proxy_pass https://${ORIGIN_IP}:443;
        proxy_ssl_server_name on;
        proxy_ssl_name ${DOMAIN};
        proxy_ssl_session_reuse on;

        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;

        # WebSocket support
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        # Buffer settings (optimized for 1GB RAM VPS)
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 8k;
        proxy_busy_buffers_size 16k;
    }

    client_max_body_size 50m;
}
EOF

Enable it:

sudo ln -sf /etc/nginx/sites-available/your-site /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

At this point, your site may partially work from the restricted region — the client-to-proxy path is fine, but the proxy-to-origin path is still throttled. The next steps fix that.

Step 5 - Install AmneziaWG on Both Servers

Run this on both your origin server and the in-region VPS:

# Prerequisites
sudo apt install -y software-properties-common linux-headers-$(uname -r)

# Enable source repositories (needed for kernel module build)
sudo sed -i 's/^# deb-src/deb-src/' /etc/apt/sources.list

# Install AmneziaWG
sudo add-apt-repository -y ppa:amnezia/ppa
sudo apt update -y
sudo apt install -y amneziawg amneziawg-tools

# Load kernel module and verify
sudo modprobe amneziawg
lsmod | grep amnezia

You should see amneziawg in the module list. If not, check that your kernel headers match your running kernel.

Step 6 - Generate Keys

On your origin server:

mkdir -p /etc/amnezia/amneziawg
sudo bash -c 'awg genkey | tee /etc/amnezia/amneziawg/server_private.key | awg pubkey > /etc/amnezia/amneziawg/server_public.key'
sudo bash -c 'awg genpsk > /etc/amnezia/amneziawg/preshared.key'
sudo chmod 600 /etc/amnezia/amneziawg/server_private.key /etc/amnezia/amneziawg/preshared.key

echo "=== Copy these to the in-region VPS ==="
echo "Origin Public Key: $(sudo cat /etc/amnezia/amneziawg/server_public.key)"
echo "Preshared Key:     $(sudo cat /etc/amnezia/amneziawg/preshared.key)"

On the in-region VPS:

mkdir -p /etc/amnezia/amneziawg
sudo bash -c 'awg genkey | tee /etc/amnezia/amneziawg/client_private.key | awg pubkey > /etc/amnezia/amneziawg/client_public.key'
sudo chmod 600 /etc/amnezia/amneziawg/client_private.key

echo "=== Copy this to the origin server ==="
echo "VPS Public Key: $(sudo cat /etc/amnezia/amneziawg/client_public.key)"

You'll need to exchange these keys between the two servers.

Copy From To
/etc/amnezia/amneziawg/preshared.key origin in-region VPS
/etc/amnezia/amneziawg/server_public.key origin in-region VPS
/etc/amnezia/amneziawg/client_public.key in-region VPS origin

Copy files to other server:

# On the origin server, run:
sudo scp /etc/amnezia/amneziawg/preshared.key <user>@<IN_REGION_VPS_IP>:~/
sudo scp /etc/amnezia/amneziawg/server_public.key <user>@<IN_REGION_VPS_IP>:~/

# On the in-region VPS, run:
sudo scp /etc/amnezia/amneziawg/client_public.key <user>@<ORIGIN_SERVER_IP>:~/

Move copied files into correct directory

# On the origin server, run:
sudo mv ~/client_public.key /etc/amnezia/amneziawg/

# On the in-region VPS, run:
sudo mv ~/preshared.key /etc/amnezia/amneziawg/
sudo mv ~/server_public.key /etc/amnezia/amneziawg/

Step 7 - Configure AmneziaWG on the Origin Server

Create the config file:

sudo bash -c 'cat > /etc/amnezia/amneziawg/awg0.conf << EOF
[Interface]
Address = 10.10.0.1/24
ListenPort = 443
PrivateKey = $(cat /etc/amnezia/amneziawg/server_private.key)
MTU = 1280

# Obfuscation parameters (must match on both sides)
Jc = 4
Jmin = 50
Jmax = 1000
S1 = 55
S2 = 155
H1 = 1953034736
H2 = 752945292
H3 = 3945748733
H4 = 1666444888

# QUIC protocol signature — makes the tunnel look like QUIC (HTTP/3)
# Without this, sophisticated DPI detects and kills the tunnel in seconds
I1 = <b 0xc70000000108ce1bf31eec7d93360000449e227e4596ed7f75c4d35ce31880b4133107c822c6355b51f0d7c1bba96d5c210a48aca01885fed0871cfc37d59137d73b506dc013bb4a13c060ca5b04b7ae215af71e37d6e8ff1db235f9fe0c25cb8b492471054a7c8d0d6077d430d07f6e87a8699287f6e69f54263c7334a8e144a29851429bf2e350e519445172d36953e96085110ce1fb641e5efad42c0feb4711ece959b72cc4d6f3c1e83251adb572b921534f6ac4b10927167f41fe50040a75acef62f45bded67c0b45b9d655ce374589cad6f568b8475b2e8921ff98628f86ff2eb5bcce6f3ddb7dc89e37c5b5e78ddc8d93a58896e530b5f9f1448ab3b7a1d1f24a63bf981634f6183a21af310ffa52e9ddf5521561760288669de01a5f2f1a4f922e68d0592026bbe4329b654d4f5d6ace4f6a23b8560b720a5350691c0037b10acfac9726add44e7d3e880ee6f3b0d6429ff33655c297fee786bb5ac032e48d2062cd45e305e6d8d8b82bfbf0fdbc5ec09943d1ad02b0b5868ac4b24bb10255196be883562c35a713002014016b8cc5224768b3d330016cf8ed9300fe6bf39b4b19b3667cddc6e7c7ebe4437a58862606a2a66bd4184b09ab9d2cd3d3faed4d2ab71dd821422a9540c4c5fa2a9b2e6693d411a22854a8e541ed930796521f03a54254074bc4c5bca152a1723260e7d70a24d49720acc544b41359cfc252385bda7de7d05878ac0ea0343c77715e145160e6562161dfe2024846dfda3ce99068817a2418e66e4f37dea40a21251c8a034f83145071d93baadf050ca0f95dc9ce2338fb082d64fbc8faba905cec66e65c0e1f9b003c32c943381282d4ab09bef9b6813ff3ff5118623d2617867e25f0601df583c3ac51bc6303f79e68d8f8de4b8363ec9c7728b3ec5fcd5274edfca2a42f2727aa223c557afb33f5bea4f64aeb252c0150ed734d4d8eccb257824e8e090f65029a3a042a51e5cc8767408ae07d55da8507e4d009ae72c47ddb138df3cab6cc023df2532f88fb5a4c4bd917fafde0f3134be09231c389c70bc55cb95a779615e8e0a76a2b4d943aabfde0e394c985c0cb0376930f92c5b6998ef49ff4a13652b787503f55c4e3d8eebd6e1bc6db3a6d405d8405bd7a8db7cefc64d16e0d105a468f3d33d29e5744a24c4ac43ce0eb1bf6b559aed520b91108cda2de6e2c4f14bc4f4dc58712580e07d217c8cca1aaf7ac04bab3e7b1008b966f1ed4fba3fd93a0a9d3a27127e7aa587fbcc60d548300146bdc126982a58ff5342fc41a43f83a3d2722a26645bc961894e339b953e78ab395ff2fb854247ad06d446cc2944a1aefb90573115dc198f5c1efbc22bc6d7a74e41e666a643d5f85f57fde81b87ceff95353d22ae8bab11684180dd142642894d8dc34e402f802c2fd4a73508ca99124e428d67437c871dd96e506ffc39c0fc401f666b437adca41fd563cbcfd0fa22fbbf8112979c4e677fb533d981745cceed0fe96da6cc0593c430bbb71bcbf924f70b4547b0bb4d41c94a09a9ef1147935a5c75bb2f721fbd24ea6a9f5c9331187490ffa6d4e34e6bb30c2c54a0344724f01088fb2751a486f425362741664efb287bce66c4a544c96fa8b124d3c6b9eaca170c0b530799a6e878a57f402eb0016cf2689d55c76b2a91285e2273763f3afc5bc9398273f5338a06d>

[Peer]
PublicKey = $(cat /etc/amnezia/amneziawg/client_public.key)
PresharedKey = $(cat /etc/amnezia/amneziawg/preshared.key)
AllowedIPs = 10.10.0.2/32
EOF'

sudo chmod 600 /etc/amnezia/amneziawg/awg0.conf

Open the firewall:

sudo ufw allow 443/udp
sudo ufw allow in on awg0

Step 8 - Configure AmneziaWG on the In-Region VPS

Replace <ORIGIN_SERVER_IP> with the IP of your server.

sudo bash -c 'cat > /etc/amnezia/amneziawg/awg0.conf << EOF
[Interface]
Address = 10.10.0.2/24
PrivateKey = $(cat /etc/amnezia/amneziawg/client_private.key)
MTU = 1280

# Obfuscation parameters — must match origin server exactly
Jc = 4
Jmin = 50
Jmax = 1000
S1 = 55
S2 = 155
H1 = 1953034736
H2 = 752945292
H3 = 3945748733
H4 = 1666444888

# Same QUIC signature as origin
I1 = <b 0xc70000000108ce1bf31eec7d93360000449e227e4596ed7f75c4d35ce31880b4133107c822c6355b51f0d7c1bba96d5c210a48aca01885fed0871cfc37d59137d73b506dc013bb4a13c060ca5b04b7ae215af71e37d6e8ff1db235f9fe0c25cb8b492471054a7c8d0d6077d430d07f6e87a8699287f6e69f54263c7334a8e144a29851429bf2e350e519445172d36953e96085110ce1fb641e5efad42c0feb4711ece959b72cc4d6f3c1e83251adb572b921534f6ac4b10927167f41fe50040a75acef62f45bded67c0b45b9d655ce374589cad6f568b8475b2e8921ff98628f86ff2eb5bcce6f3ddb7dc89e37c5b5e78ddc8d93a58896e530b5f9f1448ab3b7a1d1f24a63bf981634f6183a21af310ffa52e9ddf5521561760288669de01a5f2f1a4f922e68d0592026bbe4329b654d4f5d6ace4f6a23b8560b720a5350691c0037b10acfac9726add44e7d3e880ee6f3b0d6429ff33655c297fee786bb5ac032e48d2062cd45e305e6d8d8b82bfbf0fdbc5ec09943d1ad02b0b5868ac4b24bb10255196be883562c35a713002014016b8cc5224768b3d330016cf8ed9300fe6bf39b4b19b3667cddc6e7c7ebe4437a58862606a2a66bd4184b09ab9d2cd3d3faed4d2ab71dd821422a9540c4c5fa2a9b2e6693d411a22854a8e541ed930796521f03a54254074bc4c5bca152a1723260e7d70a24d49720acc544b41359cfc252385bda7de7d05878ac0ea0343c77715e145160e6562161dfe2024846dfda3ce99068817a2418e66e4f37dea40a21251c8a034f83145071d93baadf050ca0f95dc9ce2338fb082d64fbc8faba905cec66e65c0e1f9b003c32c943381282d4ab09bef9b6813ff3ff5118623d2617867e25f0601df583c3ac51bc6303f79e68d8f8de4b8363ec9c7728b3ec5fcd5274edfca2a42f2727aa223c557afb33f5bea4f64aeb252c0150ed734d4d8eccb257824e8e090f65029a3a042a51e5cc8767408ae07d55da8507e4d009ae72c47ddb138df3cab6cc023df2532f88fb5a4c4bd917fafde0f3134be09231c389c70bc55cb95a779615e8e0a76a2b4d943aabfde0e394c985c0cb0376930f92c5b6998ef49ff4a13652b787503f55c4e3d8eebd6e1bc6db3a6d405d8405bd7a8db7cefc64d16e0d105a468f3d33d29e5744a24c4ac43ce0eb1bf6b559aed520b91108cda2de6e2c4f14bc4f4dc58712580e07d217c8cca1aaf7ac04bab3e7b1008b966f1ed4fba3fd93a0a9d3a27127e7aa587fbcc60d548300146bdc126982a58ff5342fc41a43f83a3d2722a26645bc961894e339b953e78ab395ff2fb854247ad06d446cc2944a1aefb90573115dc198f5c1efbc22bc6d7a74e41e666a643d5f85f57fde81b87ceff95353d22ae8bab11684180dd142642894d8dc34e402f802c2fd4a73508ca99124e428d67437c871dd96e506ffc39c0fc401f666b437adca41fd563cbcfd0fa22fbbf8112979c4e677fb533d981745cceed0fe96da6cc0593c430bbb71bcbf924f70b4547b0bb4d41c94a09a9ef1147935a5c75bb2f721fbd24ea6a9f5c9331187490ffa6d4e34e6bb30c2c54a0344724f01088fb2751a486f425362741664efb287bce66c4a544c96fa8b124d3c6b9eaca170c0b530799a6e878a57f402eb0016cf2689d55c76b2a91285e2273763f3afc5bc9398273f5338a06d>

[Peer]
PublicKey = $(cat /etc/amnezia/amneziawg/server_public.key)
PresharedKey = $(cat /etc/amnezia/amneziawg/preshared.key)
Endpoint = <ORIGIN_SERVER_IP>:443
AllowedIPs = 10.10.0.1/32
PersistentKeepalive = 25
EOF'

sudo chmod 600 /etc/amnezia/amneziawg/awg0.conf

Step 9 - Start the Tunnel

Start on the origin server first (it's the listener):

sudo systemctl enable --now awg-quick@awg0

Then start on the in-region VPS (it initiates the connection):

sudo systemctl enable --now awg-quick@awg0

Verify:

# On either server — should show a recent handshake
sudo awg show

# Test connectivity (from in-region VPS)
ping -c 3 10.10.0.1

# Test connectivity (from origin server)
ping -c 3 10.10.0.2

Both pings should succeed with ~40-60ms latency.

Step 9.1 - Survive Kernel Upgrades (Origin Server)

This step is critical. AmneziaWG uses a DKMS kernel module. When your kernel updates (e.g., via unattended upgrades), the module may not be rebuilt for the new kernel, and the tunnel will fail to start after reboot — silently breaking your site for restricted-region users.

Create a systemd service that auto-builds the module before the tunnel starts:

sudo bash -c 'cat > /usr/local/bin/awg-module-ensure.sh << "SCRIPT"
#!/bin/bash
KVER=$(uname -r)

if modprobe amneziawg 2>/dev/null; then
    logger -t awg-ensure "amneziawg module loaded successfully"
    exit 0
fi

logger -t awg-ensure "Module missing for kernel $KVER, attempting DKMS build"

# Install headers if missing
apt-get install -y -qq linux-headers-$KVER 2>/dev/null

# Build and install module
dkms install amneziawg/1.0.0 -k $KVER 2>&1 | logger -t awg-ensure

# Try loading again
if modprobe amneziawg; then
    logger -t awg-ensure "Module built and loaded successfully"
else
    logger -t awg-ensure "ERROR: Failed to build/load module"
    exit 1
fi
SCRIPT'

sudo chmod +x /usr/local/bin/awg-module-ensure.sh

Create the systemd service:

sudo bash -c 'cat > /etc/systemd/system/awg-module-ensure.service << "EOF"
[Unit]
Description=Ensure AmneziaWG kernel module is built and loaded
Before=awg-quick@awg0.service
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/awg-module-ensure.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF'

Make the tunnel service depend on it:

sudo mkdir -p /etc/systemd/system/awg-quick@awg0.service.d
sudo bash -c 'cat > /etc/systemd/system/awg-quick@awg0.service.d/override.conf << "EOF"
[Unit]
After=awg-module-ensure.service
Requires=awg-module-ensure.service
EOF'

sudo systemctl daemon-reload
sudo systemctl enable --now awg-module-ensure.service

Also ensure the module name is in the auto-load list:

sudo bash -c 'echo "amneziawg" >> /etc/modules-load.d/amneziawg.conf'

Now on every boot — even after a kernel upgrade — the ensure service runs first, builds the module if needed, and then the tunnel starts.

Step 9.2 - Tunnel Watchdog (In-Region VPS)

The origin server may reboot for maintenance or kernel upgrades. When it comes back, the in-region VPS needs to re-establish the tunnel automatically. The PersistentKeepalive setting helps, but won't always recover from a full reboot on the other end.

Create a watchdog script that restarts the tunnel if the handshake goes stale:

sudo tee /usr/local/bin/awg-watchdog.sh > /dev/null << 'SCRIPT'
#!/bin/bash
# Restart tunnel if handshake is older than TIMEOUT seconds
TIMEOUT=180

HANDSHAKE=$(awg show awg0 latest-handshakes 2>/dev/null | awk '{print $2}')

if [ -z "$HANDSHAKE" ] || [ "$HANDSHAKE" -eq 0 ]; then
    logger -t awg-watchdog "No handshake found, restarting tunnel"
    awg-quick down awg0 2>/dev/null; awg-quick up awg0
    exit 0
fi

NOW=$(date +%s)
AGE=$((NOW - HANDSHAKE))

if [ "$AGE" -gt "$TIMEOUT" ]; then
    logger -t awg-watchdog "Handshake stale (${AGE}s old), restarting tunnel"
    awg-quick down awg0 2>/dev/null; awg-quick up awg0
fi
SCRIPT

sudo chmod +x /usr/local/bin/awg-watchdog.sh

Run it every minute via cron:

(crontab -l 2>/dev/null | grep -v awg-watchdog; echo "* * * * * /usr/local/bin/awg-watchdog.sh") | crontab -

With this in place, if the origin server reboots, the in-region VPS will detect the stale handshake within 3 minutes and re-establish the tunnel automatically. Your users experience at most a few minutes of downtime instead of indefinite breakage.

Tip: You should also run awg-module-ensure.sh and the systemd override on the in-region VPS if it runs the DKMS kernel module. The same kernel upgrade issue applies there too.

Step 10 - Route Nginx Through the Tunnel

Update the Nginx config on the in-region VPS to use the tunnel IP (10.10.0.1) instead of the origin server's public IP:

sudo sed -i 's|<ORIGIN_SERVER_IP>|10.10.0.1|g' /etc/nginx/sites-available/your-site
sudo nginx -t
sudo systemctl reload nginx

Test the full chain from the in-region VPS:

curl -sk https://10.10.0.1 -H "Host: yourapp.example.com" | head -5

You should see your website's HTML output.

Step 11 - Verify from the Restricted Region

Use check-host.net to test your website from locations inside the restricted region, or ask a user there to confirm everything loads — including large JavaScript bundles, CSS, images, and fonts.

Important Configuration Details

Why Port 443/UDP?

We use UDP port 443 because this is the standard QUIC (HTTP/3) port. DPI systems expect to see UDP traffic on this port. Combined with AmneziaWG's QUIC protocol signature (I1 parameter), the tunnel traffic is indistinguishable from legitimate QUIC connections. A random high port would be more suspicious and more likely to be blocked.

Why MTU 1280?

AmneziaWG adds overhead to each packet (obfuscation headers, junk data prefix). Without reducing MTU from the default 1420, some packets exceed the maximum transmission size inside the tunnel and get silently dropped. This manifests as partial page loads where the TCP handshake succeeds but data transfer stalls midway. MTU 1280 (the IPv6 minimum) works reliably even with maximum obfuscation overhead.

Why the I1 Parameter is Critical

This was our most important discovery. Basic AmneziaWG obfuscation (S1/S2/H1-H4 parameters only) randomizes headers and packet sizes, but sophisticated DPI can still detect the tunnel by analyzing traffic patterns. The tunnel would connect, work for 10-30 seconds, then get killed.

Adding the I1 parameter — which prepends a real QUIC Initial packet signature to the handshake — was the difference between a tunnel that lasted seconds and one that runs indefinitely. The DPI sees what looks like a legitimate QUIC connection being established and leaves it alone.

Which Parameters Must Match?

Must be identical on both servers:

  • S1, S2 — Handshake padding sizes
  • H1, H2, H3, H4 — Magic header replacement values
  • I1 — Protocol signature

Can differ (but simplest to keep the same):

  • Jc, Jmin, Jmax — Junk packet count and size range
Firewall Rules

Origin server:

sudo ufw allow 443/udp        # AmneziaWG tunnel
sudo ufw allow in on awg0     # All traffic on tunnel interface

In-region VPS:

sudo ufw allow 80/tcp          # HTTP (for redirects and ACME)
sudo ufw allow 443/tcp         # HTTPS (client connections)
sudo ufw allow in on awg0      # Tunnel interface
sudo ufw allow out on awg0     # Outbound on tunnel

Troubleshooting

Tunnel won't establish (no handshake)
# Check service status
sudo systemctl status awg-quick@awg0

# Check interface
sudo awg show

# Verify UDP 443 is reachable from in-region VPS
nc -zvu <ORIGIN_SERVER_IP> 443

Common causes:

  • Origin firewall blocks UDP 443
  • Keys don't match
  • Kernel module not loaded (modprobe amneziawg)
Tunnel breaks after kernel upgrade / server reboot

If awg show returns nothing and the service failed, the kernel module wasn't rebuilt for the new kernel:

# Check what happened
uname -r                    # Current kernel
dkms status                 # Shows which kernels have the module

# Rebuild for current kernel
sudo apt install -y linux-headers-$(uname -r)
sudo dkms install amneziawg/1.0.0 -k $(uname -r)
modprobe amneziawg
sudo systemctl restart awg-quick@awg0

To prevent this permanently, set up the awg-module-ensure systemd service described in Step 9.1.

Tunnel connects but dies after 10-30 seconds

You're missing the I1 parameter or it doesn't match on both sides. Add the QUIC protocol signature to both configs.

Ping works through tunnel but TCP/HTTPS doesn't

MTU problem. Ensure both configs have MTU = 1280.

Also verify the origin server's firewall allows TCP traffic from the tunnel interface:

# On origin server
sudo iptables -I INPUT -i awg0 -j ACCEPT
Handshake goes stale after periods of inactivity

The PersistentKeepalive = 25 on the in-region VPS config should prevent this. If it still happens:

awg-quick down awg0 && awg-quick up awg0
504 Gateway Timeout in browser

Nginx can't reach the origin through the tunnel. Debug sequence:

  1. Is the tunnel up? awg show | grep handshake
  2. Can you curl through it? curl -sk https://10.10.0.1 -H "Host: yourapp.example.com"
  3. Does your origin web server listen on 0.0.0.0:443 (all interfaces)?

Check with ss -tlnp | grep 443 on the origin — if it shows a specific IP instead of 0.0.0.0, your web server won't respond on the tunnel interface.

"Destination Host Unreachable" when pinging

The tunnel handshake hasn't completed. The in-region VPS is the initiator (it has the Endpoint configured), so restart it:

# On the in-region VPS
awg-quick down awg0 && awg-quick up awg0

Then check immediately: awg show | grep handshake — should show a handshake within the last few seconds.

Further considerations

Security
  • The in-region VPS terminates TLS from users and re-encrypts to the origin. If you don't fully control this VPS, treat it as a potential observation point
  • Regularly update both servers' OS and AmneziaWG packages
  • Monitor tunnel health — use the watchdog script from Step 9.2 on the in-region VPS, and the module-ensure service from Step 9.1 on the origin server to handle reboots and kernel upgrades automatically
Cost

The only additional cost is the in-region VPS: typically $4-10/month for minimum specs. No CDN subscriptions, no special software licenses, no per-request fees.

Conclusion

The complete solution has three layers:

  1. DNS points to the in-region VPS
  2. Nginx on the in-region VPS handles TLS termination and reverse proxying
  3. AmneziaWG tunnel with QUIC obfuscation carries the proxy-to-origin traffic through the DPI undetected

The key insight is that neither a CDN nor a simple reverse proxy is sufficient when DPI throttles both direct and proxied connections to foreign IPs. You need an encrypted, obfuscated tunnel between the in-region proxy and your origin — and that tunnel needs protocol-level disguise (the I1 parameter) to survive against modern DPI systems.

Resources


Written based on real-world experience. DPI techniques evolve — if this stops working, check the AmneziaVPN community for the latest bypass methods.

License: MIT
Want to contribute?

Get Rewarded: Get up to €50 in credit! Be a part of the community and contribute. Do it for the money. Do it for the bragging rights. And do it to teach others!

Report Issue
Try Hetzner Cloud

Get €20/$20 free credit!

Valid until: 31 December 2026 Valid for: 3 months and only for new customers
Get started
Want to contribute?

Get Rewarded: Get up to €50 credit on your account for every tutorial you write and we publish!

Find out more