- Rust 85.6%
- Shell 7.9%
- CSS 3.6%
- JavaScript 1%
- Dockerfile 0.8%
- Other 1.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
AWG 3.1 end to end, a health check and a diagnostic bundle in every node image, an unprivileged resolver with a domain blacklist, the tunnel's own status page, and a CLI that can hand a script JSON or a file. Every image published for amd64 and arm64. |
||
| .github | ||
| containers | ||
| crates | ||
| .gitattributes | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CONTRIBUTING.md | ||
| LICENSE | ||
| README.md | ||
| RU.CONTRIBUTING.MD | ||
| RU.README.MD | ||
AmneziaWG Containers and Tools
Self-hosted AmneziaWG servers for every protocol generation — 1.0, 1.5, 2.0, 3.0 and 3.1 — as small container images, plus a Rust CLI that generates the obfuscation parameters and can install a server on a remote machine over SSH.
Русская версия: RU.README.MD
Unofficial. This is a community project. It is not affiliated with, endorsed by, or supported by AmneziaVPN. If something here breaks, open an issue on this repository — please do not send it upstream.
Why this exists
AmneziaWG 3.0 has been out for a while, and the clients support it. Self-hosting it is a different story.
The official server pipeline configures tunnels through awg-quick, which reads a .conf file and hands it to awg setconf. That parser lives in amneziawg-tools, and it still only understands the 2.0 key set. Feed it a 3.0 config and it stops at the first unknown line:
$ awg setconf awg0 awg0.conf
Line unrecognized: `HeaderProtectionKey=...'
Configuration parsing error
The daemon itself is fine — amneziawg-go v3.0.2 implements 3.0 completely. Only the config path in front of it is behind. So this project skips that path and talks to the daemon directly over its UAPI socket, which accepts every 3.0 key today.
That is the whole trick. Everything else here is packaging, parameter generation and making it pleasant to use.
How it ended up like this
It began as a small thing to make installing AmneziaWG less tedious — a wrapper around steps people were already doing by hand. Then it turned out nobody could self-host 3.0 at all, and, well, you only live once. So it grew up into the operator of its own stack: it builds the images, puts them on your server, issues the client configs and tells you when something is wrong with the result.
Official self-hosted 3.0 will land upstream sooner or later. When it does, use it. Until then, this works.
What you get
| Four server images | one per protocol generation, ~33 MB each |
| A DNS resolver image | ~20 MB, reachable only from inside the tunnel |
awg-tool |
generates parameters, exports .conf and vpn://, installs servers over SSH |
| An interactive UI | run awg-tool with no arguments |
Images are published on Docker Hub under vaiprog:
| Image | Protocol | Built on |
|---|---|---|
vaiprog/amnezia-wg-1 |
AWG 1.0 | amneziawg-go v0.2.12 |
vaiprog/amnezia-wg-15 |
AWG 1.5 | amneziawg-go v0.2.14-beta-awg-1.5-1 |
vaiprog/amnezia-wg-2 |
AWG 2.0 | amneziawg-go v0.2.19 |
vaiprog/amnezia-wg-3 |
AWG 3.0 | amneziawg-go v3.0.2 |
vaiprog/amnezia-wg-31 |
AWG 3.1 | amneziawg-go v3.1.20260814 |
vaiprog/amnezia-wg-dns |
— | unbound |
vaiprog/amnezia-wg-status |
— | the tunnel's own status page |
All five tunnel generations run side by side on one host without stepping on each other. Every image is published for amd64 and arm64 — the latter is what most boards people actually run tunnels on speak.
Quick start
Run a 3.0 server with Docker Compose
git clone https://github.com/Vadim-Khristenko/awg-containers-and-tools
cd awg-containers-and-tools/containers
Generate one parameter block and paste it into both server.conf and client.conf — the obfuscation parameters have to match on the two ends, or the peers will not recognise each other's packets:
awg-tool gen --version 3.0
Add your keys and addresses, then:
docker compose up -d
docker compose exec server awg-uapi get # handshakes, transfer counters
docker compose exec server awg-peer add laptop
awg-peer add prints a complete client config, ready to import.
Install on a remote server over SSH
awg-tool install --host 203.0.113.9 --user root --key ~/.ssh/id_ed25519
It looks at the machine before it changes anything: which distribution, whether docker is usable and whether it needs sudo, and what address clients should be pointed at. It knows the twelve most common distributions and falls back through ID_LIKE for derivatives, so Zorin is treated as Ubuntu and CachyOS as Arch.
If a package is missing it prints the exact command and asks before running it. On NixOS it stops and hands you a configuration.nix snippet instead, because installing packages imperatively there would not survive the next rebuild.
If something is already listening on the tunnel port it tells you what, and stops. It will not kill another process for you — picking a free port with --listen-port costs one flag, and killing the wrong thing costs a server.
When it finishes you get the client .conf and the vpn:// link on stdout.
The survey makes no outbound connections from your server: the endpoint address comes from the default route's source, not from an echo service. And sudo docker info is only attempted if the unprivileged call already failed — a needless sudo is how a survey trips an audit alert on someone else's machine.
Connection profiles are saved so the second run is --server NAME. Passwords only reach the disk if you ask for that.
Managing what is already there
awg-tool status --server home # what is running, peers, handshakes, traffic
awg-tool doctor # why is it not carrying traffic?
awg-tool logs --lines 200 # the log, with key material stripped out
awg-tool update # is the tool, or an image, out of date?
status finds our containers by image reference, so a node someone renamed is still found, and a container that is not ours is not touched.
doctor returns a verdict with a confidence level, the evidence behind it and the next step — not a wall of log text. It distinguishes a missing /dev/net/tun from a missing NET_ADMIN from ip_forward being off from a peer that has simply never handshaked, and when the evidence cannot separate two causes it says so and lists both instead of picking the likelier one.
logs redacts on the way out of the library, not at the print site: private keys, pre-shared keys and header-protection keys cannot cross that boundary, so the output is safe to paste somewhere.
Just generate a config
awg-tool gen --version 3.0 --profile quic --client amneziavpn
awg-tool gen --version 2.0 --client amneziawg-windows --browser chrome
awg-tool gen --version 3.0 --uapi # UAPI lines instead of .conf
awg-tool gen --version 3.1 --random-trailers # the 3.1 switches, off by default
awg-tool gen --version 3.1 --disable-cookies # breaks NAT keepalive under load — know why before using
Parameter generation
Every run produces a fresh parameter set. This matters more than it looks: if everyone deployed the same numbers, every server built with this tool would share one DPI fingerprint, and blocking all of them at once would be trivial. Randomising per install is the point.
The generator enforces the protocol's real constraints rather than emitting plausible-looking numbers — the S-value floor when header protection is on, the relationship between the rekey and reject timers, the per-version tag vocabulary, and the size ceilings that keep junk packets inside your MTU.
Mimicry profiles
The I1–I5 junk packets can be shaped to look like ordinary traffic:
quic quic0rtt tls noise dtls http3 sip tls-to-quic quic-burst dns random
awg-tool profiles # list them
awg-tool gen --profile tls --host cdn.example.com
--browser chrome|edge|firefox|safari|yandex-desktop|yandex-mobile matches the packet sizes a real browser produces, instead of picking sizes that no browser would ever emit.
Client limits
Clients do not all accept the same things, and a config that exceeds one of these limits fails at import — sometimes loudly, sometimes not. --client trims the output to what your target actually supports:
awg-tool clients
| Client | Notable limit |
|---|---|
| AmneziaVPN | full support |
| AmneziaWG Android / iOS | full support |
| AmneziaWG Windows | H-values capped at INT32_MAX |
| WG Tunnel | large S3/S4 drains battery — keep S4 modest |
| WireSock | no <c>/<rc>/<rd> tags |
| Keenetic (native) | sensitive to complex I1; prefer simple junk or DNS mimicry |
| amneziawg-go (legacy) | <c> is unimplemented — produces ErrorCode 1000 |
| OpenWRT, ASUS Merlin | full support |
The <c> tag is off by default for exactly that reason. --tag-c turns it on if you know your client handles it.
Export formats
Both formats the Amnezia ecosystem uses:
.conf— the standard WireGuard-style filevpn://— the one-line link the Amnezia client imports directly
DNS leak protection
The resolver sits on its own bridge network at 172.29.172.254, matching what AmneziaVPN does upstream. Nothing publishes its port and no other network can reach that bridge, so a client configured with DNS = 172.29.172.254 can only get an answer through the tunnel.
That is a stronger guarantee than "the resolver is preferred". A query that leaks outside the tunnel does not quietly reach your ISP's resolver instead — it reaches nothing at all, and fails visibly.
Blocking domains
Mount a file of domain names into the resolver and each one — with everything beneath it — stops resolving, answered NXDOMAIN by the resolver itself, with no upstream query ever leaving the container:
dns:
volumes:
- ./blocklist.txt:/etc/unbound/blacklist.d/list.txt:ro
One domain per line; # comments and blank lines are skipped, and hosts-file style lines (0.0.0.0 ads.example.com) work as-is, so an existing hosts file mounts unchanged. Lines that are not domains are dropped with a count rather than taking the resolver down. NXDOMAIN rather than 0.0.0.0 on purpose: a sinkholed address invites the client to retry, a name that does not exist is simply done.
The page that answers "am I under the VPN?"
The compose file stands up a small status page at http://amiunder.vpn, and it works by construction: .vpn is not a real top-level domain and never will be, so the only resolver on earth that answers the name is the tunnel's own, and the address is reachable only through the tunnel. If the page opens, you are under the VPN; if it does not, you are not — there is no third answer to render.
It speaks Russian and English, shows the address the tunnel sees for you, and measures throughput by streaming a payload from the same host — no third-party speed test, no request that could reveal your real address. The stylesheet is the Architect kit itself, served locally like everything else on the page. It runs with no capabilities and a read-only filesystem, and it is there by default; to leave it out of a particular up:
docker compose up -d --scale status=0
Server management
Once a server is up:
awg-peer add <label> [ip] # new peer; prints the client config once
awg-peer list # label, public key, allowed IP, last handshake
awg-peer rm <label|pubkey> # revoke
awg-uapi get # raw daemon state
A new peer's private key is generated inside the container, printed once and never written to disk. What is stored is the label, public key, address and pre-shared key — the last one because the server needs it again after every restart.
The node also keeps an event log at /var/log/awg/events.log, mirrored to docker logs: when the node came up, when the interface was configured, who was given access and when it was taken away. It contains no private keys, pre-shared keys or config bodies — a public key identifies a peer perfectly well. It is still sensitive, though, because it is the list of who has access, so treat it accordingly rather than pasting it into an issue. It is bounded at 256 KiB plus one rotation, so a node that flaps for a year still costs half a megabyte.
When it does not work
Two tools live inside every node image for exactly that moment:
awg-health # exit 0 and a green docker ps status when the node can carry traffic
awg-dump > dump.txt # the whole picture, made for pasting
The health check is deliberately narrow: interface up, UAPI answering. A node with no peers yet is healthy — an orchestrator restarting it into the same silence fixes nothing. The dump gathers what an operator needs side by side — interface, routes and rules, the UAPI view, NAT rules, the event log tail — and carries no private keys or pre-shared keys by construction, because its whole purpose is to leave the node.
Building from source
Rust 1.90 or newer (edition 2024):
cargo build --release
./target/release/awg-tool --help
Building the images yourself:
cd containers
./build.sh
./selftest.sh # brings up a real tunnel per generation and proves it carries traffic
selftest.sh is not a smoke test — it establishes a handshake, pushes data through and compares checksums on the far side, for each of the five generations.
Status
Working today: parameter generation for all five versions, the interactive UI, all six container images, SSH deployment, container discovery, health, diagnosis, redacted logs, update checks, and both export formats.
Planned: a web UI, WASM builds, Android builds.
This is release 0.2.2. There are 326 tests, the containers are verified against live tunnels rather than smoke tests, and install is exercised end to end against a throwaway VM — but the tool is young, so please report what breaks.
Verifying a download
Every release ships a checksum file named after its tag:
sha256sum -c checksums-v0.2.2.sha256
The binaries are not signed. The checksum tells you the file arrived intact; it does not tell you who built it.
Known limits, so they are not a surprise:
- The UI connects to saved profiles that need no password, or whose password you chose to store. Anything else is a command away, and the screen says so.
- If the tunnel port is taken, the tool tells you what holds it and stops. It will not kill it for you.
Contributing
See CONTRIBUTING.md (по-русски).
Support the project
awg-tool donate
Prints the same cards, recurring options and crypto addresses that Any Tech ARCHITECT lists, so you do not have to go and find a web page to support something you are already running. Each address is on a line of its own, so selecting one cannot pick up a label along with it.
License
MIT — see LICENSE.
Parameter-generation logic is shared with Any Tech ARCHITECT. A joint release of Any Tech ARCHITECT and VAIEXIA.
Built by Vadim Khristenko (VAI_PROG).