HTTP API server for managing AmneziaWG 2.0 VPN clients. Uses the AmneziaWG kernel module on the host with the awg CLI tool — kernel-level performance with DPI obfuscation via CPS (Custom Protocol Signature).
Supports per-client obfuscation profiles — each unique set of CPS parameters gets its own AWG interface, created on demand.
One-liner that installs AmneziaWG 2.0, downloads the latest awg-server binary, and gets you ready to run:
# 1. Install AmneziaWG 2.0 kernel module (DKMS, from source)
apt update && apt install -y build-essential git dkms linux-headers-$(uname -r)
git clone --depth 1 https://github.com/amnezia-vpn/amneziawg-linux-kernel-module.git /tmp/amneziawg-module
cd /tmp/amneziawg-module/src
make dkms-install
dkms add -m amneziawg -v 1.0.0
dkms build -m amneziawg -v 1.0.0
dkms install -m amneziawg -v 1.0.0
modprobe amneziawg
cd ~ && rm -rf /tmp/amneziawg-module
# 2. Install AmneziaWG 2.0 tools (awg CLI)
git clone --depth 1 https://github.com/amnezia-vpn/amneziawg-tools.git /tmp/amneziawg-tools
make -C /tmp/amneziawg-tools/src && make -C /tmp/amneziawg-tools/src install
rm -rf /tmp/amneziawg-tools
# 3. Enable IP forwarding
sysctl -w net.ipv4.ip_forward=1
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
# 4. Download latest awg-server
curl -fsSL https://github.com/stealthsurf-vpn/awg-server/releases/latest/download/awg-server-linux-$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/') -o /usr/local/bin/awg-server
chmod +x /usr/local/bin/awg-server
# 5. Create data directory
mkdir -p /data
# 6. Create systemd service
cat > /etc/systemd/system/awg-server.service <<EOF
[Unit]
Description=AmneziaWG Server
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/awg-server
Restart=always
RestartSec=5
Environment=AWG_API_TOKEN=your-secret-token
Environment=AWG_ADDRESS=10.0.0.1/24
Environment=AWG_ENDPOINT=your.server.ip
# Optional (shown values are defaults):
# Environment=AWG_JC=5
# Environment=AWG_JMIN=50
# Environment=AWG_JMAX=1000
# Environment=AWG_LISTEN_PORT=51820
# Environment=AWG_HTTP_PORT=7777
# Environment=AWG_DNS=1.1.1.1
# Environment=AWG_MTU=1420
# Environment=AWG_MAX_INTERFACES=0
[Install]
WantedBy=multi-user.target
EOF
# 7. Start and enable on boot
systemctl daemon-reload
systemctl enable --now awg-serverCheck status:
systemctl status awg-server
journalctl -u awg-server -f- amneziawg-linux-kernel-module installed on host
- amneziawg-tools (
awgCLI) installed on host iptables,iproute2(usually already present)net.ipv4.ip_forward=1sysctl enabled
# Check current version
awg-server version
# Self-update to latest GitHub release
awg-server update
# Start the server (default, no arguments)
awg-serverAfter awg-server update, restart the service: systemctl restart awg-server.
# Build for current platform (with version)
make build VERSION=1.0.0
# Build for all platforms (linux, darwin, windows × amd64, arm64)
make build-all
# Static analysis
make vet
# Clean build artifacts
make cleanRequires Go 1.24+. Binaries are output to dist/.
Copy awg-server binary to the VPN server and run:
AWG_API_TOKEN=your-secret-token \
AWG_ADDRESS=10.0.0.1/24 \
AWG_ENDPOINT=your.server.ip \
./awg-serverAll endpoints require Authorization: Bearer <AWG_API_TOKEN>.
# Health check (no auth)
curl http://localhost:7777/health
# List clients
curl http://localhost:7777/api/clients -H "Authorization: Bearer $TOKEN"
# Create client (default obfuscation params from env)
curl -X POST http://localhost:7777/api/clients \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"id":"my-client-uuid"}'
# Create client with custom obfuscation params and port
curl -X POST http://localhost:7777/api/clients \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"id":"my-client-uuid","awg_params":{"port":51825,"jc":5,"jmin":50,"jmax":1000,"s1":40,"s3":20,"h1":"100000-800000"}}'
# Update client obfuscation params
curl -X PATCH http://localhost:7777/api/clients/my-client-uuid \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"awg_params":{"port":51825,"jc":10,"jmin":100,"jmax":2000}}'
# Get client config (.conf)
curl http://localhost:7777/api/clients/my-client-uuid/configuration \
-H "Authorization: Bearer $TOKEN"
# Get client usage stats (accumulated rx/tx, last handshake)
curl http://localhost:7777/api/clients/my-client-uuid/stats \
-H "Authorization: Bearer $TOKEN"
# → {"rx_bytes":1073741824,"tx_bytes":5368709120,"last_handshake":"2026-04-01T12:00:00Z"}
# Delete client
curl -X DELETE http://localhost:7777/api/clients/my-client-uuid \
-H "Authorization: Bearer $TOKEN"Environment variables:
| Variable | Required | Default | Description |
|---|---|---|---|
AWG_API_TOKEN |
yes | — | Bearer token for API auth |
AWG_ADDRESS |
yes | — | Server VPN address (CIDR), e.g. 10.0.0.1/24 |
AWG_ENDPOINT |
yes | — | Public IP/hostname for client configs |
AWG_LISTEN_PORT |
no | 51820 |
Base WireGuard UDP port (auto-assigned sequentially; can be overridden per-client via port in awg_params) |
AWG_HTTP_PORT |
no | 7777 |
HTTP API port |
AWG_MTU |
no | 1420 |
MTU value |
AWG_DNS |
no | 1.1.1.1 |
DNS for client configs |
AWG_DATA_DIR |
no | /data |
Persistence directory |
AWG_INTERFACE |
no | auto-detect | Override outbound network interface for NAT |
AWG_MAX_INTERFACES |
no | 0 |
Max AWG interfaces (0 = unlimited) |
On first start, the server generates and persists unique obfuscation values in /data/clients.json:
| Parameter | Generation | Purpose |
|---|---|---|
| H1-H4 | Random non-overlapping ranges (format min-max) |
Replace WireGuard message type headers. Zero performance impact. |
| S1 | Random 15-150 | Init handshake packet padding |
| S2 | Random 15-150, S1 + 56 ≠ S2 |
Response handshake packet padding |
These are reused across restarts. No env vars needed.
| Variable | Default | What it does | Impact |
|---|---|---|---|
AWG_JC |
5 |
Junk packets sent during handshake | 0 = off, 3-8 = good. No effect after connect. |
AWG_JMIN |
50 |
Junk packet minimum size (bytes) | Wider range = harder to fingerprint. |
AWG_JMAX |
1000 |
Junk packet maximum size (bytes) | 500-1000 = good. |
AWG_S3 |
0 |
Extra bytes added to cookie reply packets | 0-32 = good. Only under load. |
AWG_S4 |
0 |
Extra bytes added to every data packet | 0 = recommended. Adds overhead to every packet. |
AWG_I1-AWG_I5 |
empty | CPS signature packets (client config only) | Decoy UDP packets mimicking another protocol. Uses CPS tag format. |
Rule of thumb: H1-H4 and S1/S2 are auto-generated.
Jc/Jmin/Jmaxonly affect connection time.S4affects every packet — use with care.
Default (no extra env vars needed) — headers + light junk at handshake:
H1-H4, S1, S2 auto-generated. Jc=5, Jmin=50, Jmax=1000 (defaults).
Ping: same as plain WireGuard after connect. Protection: blocks signature + size-based DPI.
Minimum latency — disable junk packets:
AWG_JC=0 AWG_JMIN=0 AWG_JMAX=0Auto-generated H1-H4 still provide header masking (zero overhead).
Maximum stealth — for regions with aggressive DPI (China, Iran, Turkmenistan):
AWG_JC=8 AWG_JMIN=50 AWG_JMAX=1000
AWG_I1='<b 0xc0><r 32><c><t>'Ping: same after connect (~100ms extra at handshake). Protection: maximum without per-packet overhead.
AmneziaWG sets CPS obfuscation parameters at the interface level, not per-peer. To support per-client custom parameters, the server manages a pool of interfaces:
- Each unique set of CPS parameters gets its own
awgNinterface (awg0, awg1, awg2, ...) - Clients with identical CPS parameters share an interface
- Each interface listens on its own UDP port (explicit
portfromawg_params, or auto-assigned sequentially from base port) - Interfaces are created on demand and destroyed when their last peer is removed
- All interfaces share the same server private key
main.go → config → awg (pool, params, keygen) → clients (manager, storage) → api (server, handlers)
- Kernel module —
amneziawg-linux-kernel-moduleon host,awgCLI for management - Static binary —
CGO_ENABLED=0, no external Go dependencies beyondgolang.org/x/crypto - Persistence via JSON file with atomic writes
- IP allocation sequential from .2, freed IPs reusable
- Auth Bearer token on all endpoints