RU Русский | EN English

# AmneziaWG 3.x Installer: Advanced Documentation This is a supplement to the main [README.en.md](README.en.md), containing deeper technical details, explanations, and advanced options for the AmneziaWG 3.x (3.1 by default) and 2.0 installation and management scripts. For a step-by-step VPS deployment guide (VPS choice, OS choice, install flow, first client, update, uninstall, troubleshooting), see [INSTALL_VPS.md](INSTALL_VPS.md). ## Table of Contents - [✨ Features (Detailed)](#features-detailed-adv) - [🔐 Obfuscation Parameters (3.1 and 2.0)](#awg2-params-adv) - [Presets](#presets-adv) - [🆕 The third AmneziaWG line for self-hosted servers](#awg3-adv) - [What 3.1 added](#awg3-31-adv) - [Which module line you get](#awg3-lines-adv) - [What 3.0 changes on the wire](#awg3-wire-adv) - [The new 3.0 parameters](#awg3-params-adv) - [What has to match and what does not](#awg3-must-match-adv) - [Three things that can cost you an evening](#awg3-gotchas-adv) - [The 3.1 profile: what the installer turns on and what it leaves out](#awg3-not-yet-adv) - [Where 2.0 is installed instead of 3.1](#awg3-fallback-adv) - [⚙️ Client Configuration Details](#config-details-adv) - [AllowedIPs](#allowedips-adv) - [Device IPv6, `::/0` and the local network](#client-ipv6-adv) - [What a site can see when traffic is split by destination](#split-detect-adv) - [Client Isolation](#client-isolation-adv) - [IPv6 Dual-Stack Tunnel (v5.15.0+)](#ipv6-tunnel-adv) - [PersistentKeepalive](#persistentkeepalive-adv) - [DNS](#dns-adv) - [Your Own DNS Server for Clients](#own-dns-adv) - [Changing Default Settings](#change-defaults-adv) - [🔒 Server Security Settings](#security-adv) - [UFW Firewall](#ufw-adv) - [Kernel Parameters (Sysctl)](#sysctl-adv) - [The kernel module and user namespaces](#userns-module-adv) - [Fail2Ban (Automatic Setup)](#fail2ban-adv) - [Rotating the server key](#server-key-rotation-adv) - [🧹 Server Optimization](#optimization-adv) - [📋 Configuration Examples](#config-examples-adv) - [🖥️ CLI Parameters](#cli-params-adv) - [install_amneziawg.sh](#install-cli-adv) - [manage_amneziawg.sh](#manage-cli-adv) - [🧑‍💻 Full List of Management Commands](#manage-commands-adv) - [🤖 JSON interface for automation (--json)](#json-api-adv) - [🛠️ Technical Details](#tech-details-adv) - [Script Architecture](#architecture-adv) - [DKMS](#dkms-adv) - [Key and Config Generation](#keygen-adv) - [🔄 How to Update Scripts](#update-scripts-adv) - [❓ FAQ (Additional Questions)](#faq-advanced-adv) - [🩺 Diagnostics and Uninstall](#diag-uninstall-adv) - [Diagnostic Report Contents](#diagnostic-report-adv) - [🔧 Troubleshooting (Detailed)](#troubleshooting-adv) - [📊 Traffic Statistics (stats)](#stats-adv) - [⏳ Temporary Clients (--expires)](#expires-adv) - [📱 vpn:// URI Import](#vpnuri-adv) - [📱 MTU and Mobile Clients](#mtu-mobile-adv) - [🚧 Host Unreachable from Russia (Hetzner): AS-based Blocking](#as-blocking-adv) - [🛡️ Active Probing and Obfuscation Without a Proxy](#active-probing-adv) - [📋 Client Compatibility](#client-compat-adv) - [Which AmneziaWG generation a client speaks](#client-gen-adv) - [🐧 Debian Support](#debian-support-adv) - [🔧 Raspberry Pi and ARM64 Support](#arm-support-adv) - [🐧 Connecting a Linux machine as a client](#linux-client-adv) - [A split route list and the server address](#linux-client-loop-adv) - [📦 LXC / Docker via amneziawg-go (userspace)](#lxc-userspace-adv) - [🗄️ AmneziaWG on a Synology NAS (DSM)](#synology-adv) - [⚠️ Known Limitations](#limitations-adv) - [🤝 Contributing](#contributing-adv) - [💖 Acknowledgements](#thanks-adv) --- > For the full version history, see [CHANGELOG.en.md](CHANGELOG.en.md). --- ## ✨ Features (Detailed) * **AmneziaWG 3.1 by default:** header encryption (`HeaderProtectionKey`, a key of its own per install) and extra padding (`ContentPaddingAddition`) on top of junk packets, `S1`-`S4` padding and the `I1` concealment packet. AmneziaWG 2.0 is installed with the `--protocol=2.0` flag (for clients without 3.1) or automatically where the installer does not issue 3.1 (ARM, kernel older than 6.7): [details](#awg3-fallback-adv). * **Native key generation:** Keys are generated via `awg genkey/pubkey`, configs via Bash templates, QR codes via `qrencode`. The external Python/awgcfg.py dependency has been completely removed. * **Automated installation:** Installs AmneziaWG, DKMS module, dependencies, configures networking, firewall, and sysctl. * **Resume after reboot:** Uses a state file (`/root/awg/setup_state`) to continue after required reboots. * **Automated system optimization:** * Removal of unnecessary packages (snapd, modemmanager, etc.) * Hardware-aware swap and network buffer tuning * **Secure by default:** * `UFW`: Policy `deny incoming`, SSH rate-limiting, VPN port allowed. * `IPv6`: Disabled by default via `sysctl` (the prompt defaults to yes, `--yes` disables it without asking; keep it with `--allow-ipv6`). * `File permissions`: Strict permissions (600/700) on all keys and configs. * `Sysctl`: BBR congestion control, anti-spoofing, TCP optimization. * `Fail2Ban`: Automatic installation and SSH protection. * **Backup:** `backup` command in the management script (including client keys). --- ## 🔐 Obfuscation Parameters (3.1 and 2.0) These obfuscation parameters exist in both profiles, 3.1 and 2.0; only the lower bounds of `S3`/`S4` and the form of `H1`-`H4` differ (the "3.1 profile" column). The parameters that exist only in 3.1 (`HeaderProtectionKey`, `ContentPaddingAddition`) are in [The 3.1 profile](#awg3-not-yet-adv). All values are generated at install time and handed to clients exactly as they are on the server. The source of truth for the `manage` commands and for a reinstall is the live `/etc/amnezia/amneziawg/awg0.conf`; the copy of the set in `/root/awg/awgsetup_cfg.init` is used only when `awg0.conf` is absent, which normally means the first install. The `HeaderProtectionKey` is not written to the init: it lives in `awg0.conf` and in `/root/awg/server_hpk.key`. | Parameter | Description | 2.0 profile | 3.1 profile | Example (2.0) | |-----------|-------------|-------|-------|---------| | `Jc` | Number of junk packets | 3-6 | the same | `5` | | `Jmin` | Min junk size (bytes) | 40-89 | the same | `55` | | `Jmax` | Max junk size (bytes) | Jmin+50..Jmin+250 | the same | `200` | | `S1` | Init message padding (bytes) | 15-150 | the same | `72` | | `S2` | Response message padding (bytes) | 15-150, S1+56≠S2 | the same | `56` | | `S3` | Cookie message padding (bytes) | 8-55 | 12-55 | `32` | | `S4` | Data message padding (bytes) | 4-27 | 12-27 | `16` | | `H1` | Init message identifier | uint32 range | `1` | `134567-245678` | | `H2` | Response message identifier | uint32 range | `2` | `3456789-4567890` | | `H3` | Cookie message identifier | uint32 range | `3` | `56789012-67890123` | | `H4` | Data message identifier | uint32 range | `4` | `456789012-567890123` | | `I1` | CPS concealment packet | Tags `` / `` / `` / `` / `` | the same | A DNS-reply-shaped packet, 70-128 bytes | | `I2`-`I5` | Extra CPS / special-junk packets, optional (carried to clients) | Tags `` / `` / `` / `` / `` | the same | `` | > For `I1`-`I5` use only the ``, ``, ``, ``, `` tags: both implementations, the kernel module and amneziawg-go, understand them. The `` tag exists only in the kernel module; on amneziawg-go (macOS, LXC and Docker without the module, some routers) a config with it does not come up. **Critical constraints:** * In the 2.0 profile the H1-H4 ranges **must not overlap** (guaranteed by the generation algorithm); the generator picks them at random within 5..2147483647. In the 3.1 profile `H1`-`H4` are 1-4: header encryption hides the message type, and random ranges add nothing. * `S1 + 56 ≠ S2` — prevents init and response messages from having the same size. * `S3 ≠ S2 + 28` - keeps the response and cookie messages from having the same size. * `S4` is chosen to avoid the harmful length matches between data packets and handshake packets found by the 2.0 transport model: otherwise in the 2.0 profile some data packets could be taken for a handshake and lost. Harmless matches are allowed. * `S1`-`S4` and `H1`-`H4` **must** match on the server and the clients (in 3.1, the `HeaderProtectionKey` too): the receiver strips that padding and checks those headers. In 2.0, `H1`-`H4` are ranges - the same ranges on both sides. * `Jc`/`Jmin`/`Jmax` and `I1`-`I5` **do not** have to match: they are separate decoy packets, only the handshake initiator sends them and the other side discards them. Each of `I1`-`I5` is sent when it is set; the installer sets only `I1`, so `--no-cps` removes the concealment packet entirely. > `I1`-`I5` (CPS) disguise the handshake as another protocol - the basis of resistance to active probing. Details: [Active Probing and Obfuscation Without a Proxy](#active-probing-adv). ### Presets Presets are ready-made obfuscation parameter sets optimized for specific network conditions. Selected during installation via the `--preset` flag. | Preset | Jc | Jmin | Jmax | When to use | |--------|-----|------|------|------------| | `default` | 3-6 (random) | 40-89 | Jmin + 50..250 | Home/wired internet, standard VPS | | `mobile` | **3** (fixed) | 30-50 | Jmin + 20..80 | Mobile carriers (Tele2, Yota, Megafon, Tattelecom) | **Installation with a preset:** ```bash # Standard preset (default) sudo bash install_amneziawg_en.sh --yes # Mobile preset - for SIM cards, LTE/5G modems, mobile routers sudo bash install_amneziawg_en.sh --preset=mobile --yes ``` > **Note:** `install_amneziawg_en.sh` and `install_amneziawg.sh` are functionally identical — only the output language differs. **Fine-grained overrides (`--jc`, `--jmin`, `--jmax`):** Individual parameters can be overridden on top of any preset: ```bash # Mobile preset, but Jc=4 instead of 3 sudo bash install_amneziawg_en.sh --preset=mobile --jc=4 --yes # Fully manual parameters sudo bash install_amneziawg_en.sh --jc=2 --jmin=20 --jmax=60 --yes ``` | Flag | Range | Description | |------|-------|------------| | `--jc=N` | 0-122 | Number of junk packets; 0 turns them off | | `--jmin=N` | 0-1280 | Minimum junk size (bytes) | | `--jmax=N` | 0-1280 | Maximum junk size (bytes), must be ≥ Jmin | The `Jc` ceiling is 122, not 128: clients on amneziawg-go send the junk packets, `I1`-`I5` and the handshake packet in one batch that holds 128 packets, and crash on the first handshake when `Jc + number of I + 1` is over 128 (amneziawg-go#111). The kernel module is not affected. If on a running server `Jc + number of I + 1` is over 128 (with the single `I1` the installer sets, that is `Jc` from 127), `check`, `diagnose`, `restore`, `add`/`regen` and step 6 of the installer warn about it: set a value up to 122 in `awg0.conf`, restart `awg-quick@awg0` and reissue the profiles with `regen`. The other parameters stay as they are, while `--force --jc=N` would generate them anew. > **Tip:** If VPN works on home Wi-Fi but does not connect over mobile data, check `I1` first: the diagnosis and a ready fix are in The handshake never completes on cellular. On the measured route, lowering the junk packets did not help the handshake. Other mobile carrier cases are in the FAQ. --- ## 🆕 The third AmneziaWG line for self-hosted servers **Terms.** AmneziaWG has protocol generations, also called lines: 1.x, 2.0 (the second line) and 3.x (the third line; the installer issues its 3.1 release). The installer writes one of two profiles into the server and client configs: the "3.1 profile" (the default) or the "2.0 profile". The kernel module and the `awg` tools are also second or third line, and the 3.1 profile needs a third-line module and tools. **What a new install gets.** The 3.1 profile: header encryption and extra padding on top of the second-line obfuscation; the details are in [The 3.1 profile](#awg3-not-yet-adv). To see which generation a server runs: `sudo bash /root/awg/manage_amneziawg.sh check` prints the "Installation protocol generation" line, and `check --json` returns the `protocol` field. In late July 2026 the Amnezia team released **AmneziaWG 3.0** and switched the PPA over to it; on 12 August **3.1** followed inside the same line (tag `v3.1.20260812`), and the PPA has carried it since that day. Since version 6.0.0 a new install gets the **3.1 profile** by default: what it is made of is covered in [The 3.1 profile](#awg3-not-yet-adv), and where 2.0 is installed instead in [Where 2.0 is installed instead of 3.1](#awg3-fallback-adv). A third-line module on x86 with kernel 6.7 or newer was installed before as well, and second-line configs keep working on it. To see what you actually have: ```bash awg --version # amneziawg-tools v3.1.20260812 cat /sys/module/amneziawg/version # 3.1.20260812 - version of the LOADED module ``` ⚠️ Ask the loaded module, not the file on disk. `modinfo amneziawg | grep ^version` reads the metadata of whichever `.ko` was selected on disk, so if a host ends up with two trees carrying a module of the same name (possible on ARM: the pinned 2.0 in `extra/` and a third-line DKMS module in `updates/dkms/`), it names the one that is not running in the kernel. `modinfo` is useful in exactly one case - when the module is not loaded and `/sys/module/amneziawg/` is absent; then it shows what sits on disk. Since v5.25.0 `manage check` and `manage diagnose` ask the loaded module and fall back to `modinfo` only as a second path. ⚠️ The apt package versions do not answer this. There `amneziawg-dkms` reads as `1.0.0-0~202608140352+4680320~ubuntu24.04.1` and `amneziawg-tools` as `1.0.20210914-0~...`: the leading part is packaging, the real content is the commit hash in the suffix. The two commands above answer directly. ### What 3.1 added Inside the third line, release `v3.1.20260812` landed on 12 August 2026 and added exactly two device parameters. The installer does not set them, but they are worth knowing about: enabled by hand, one of them breaks connectivity with every client that does not have it enabled too, whatever version that client is. | Parameter | Class | What it does | |---|---|---| | `RandomTrailers` | 🔴 **two-sided** | Appends a random trailer to handshake packets. A receiver without the flag **does not recognise** the lengthened packet, so enabling it on the server alone cuts off every client that lacks it. | | `DisableCookies` | one-sided | The server stops sending cookie replies, removing a characteristic message type from observable traffic. No matching required between sides. ⚠️ In WireGuard those replies exist as protection against handshake floods from spoofed addresses, so dropping the signature drops that protection with it. | The kernel interface contract is unaffected: `WG_GENL_VERSION` is unchanged, so 3.0-line tools work with a 3.1 module. ### Which module line you get | Condition | What gets installed | Module line | Profile without `--protocol` | |---|---|---|---| | x86_64, kernel >= 6.7 | `amneziawg-dkms` from `ppa:amnezia/ppa` | **3.x** | **3.1** | | any arch, kernel older than 6.7 (Debian 12 on 6.1) | pinned module built from source | 2.0 | 2.0, automatically | | ARM64 / armhf with a prebuilt for your kernel | our prebuilt package | 2.0 | 2.0, automatically | | ARM64 / armhf with no prebuilt, kernel >= 6.7 | `amneziawg-dkms` from the PPA | **3.x** | 2.0, automatically: ARM has not been measured for the third line | A third-line module is necessary but not sufficient for the 3.1 profile: at step 3 the installer also checks the tools and the loaded module, see [Where 2.0 is installed instead of 3.1](#awg3-fallback-adv). That last row is not hypothetical: prebuilt ARM packages are built for Raspberry Pi 3/4/5, Ubuntu 24.04 ARM64, and Debian 12/13 ARM64. Ubuntu 26.04 ARM64 is not on that list yet, and on such a host the installer finds no match, falls through to the normal DKMS path and installs the PPA module, which is the third version. On older kernels we pick 2.0 **on purpose**, not because 3.0 fails to build there. For the first day after the release it genuinely did fail on kernel 6.1; upstream fixed that on 31 July and it builds now. But old kernels are exactly where the 3.0 line has had the least mileage, while the pinned 2.0 is verified against an immutable commit. The threshold will be lifted after a separate validation, not because the build passes again. On ARM the installer tries the prebuilt package first, and if it finds one matching your kernel it never reaches the PPA at all. The prebuilt packages are pinned to 2.0. With no match, the same rule as on x86 takes over: a kernel older than 6.7 gets the pinned 2.0 from source, a newer one gets the PPA module. > ⚠️ **The pinned 2.0 line does not receive updates, and that is a deliberate decision rather than an oversight.** The module on the pinned path is built from the immutable tag `v1.0.20260725`, and upstream has frozen the `1.0.x` line - there will be no new releases in it. The same applies to the prebuilt ARM packages. > > What that means in practice. On Debian 12 with its stock 6.1 kernel, and on ARM with a matching prebuilt package, you get a proven but frozen module. There is no "upgrade to the third version with a plain `apt upgrade`" path for such hosts: the installer keeps `amneziawg-dkms` on `apt-mark hold` precisely so the PPA module does not land next to it as a second tree with the same name. Nothing breaks and nothing stops working - updates simply do not arrive. > > If you specifically need the third version on such a host, there is one supported route: get a kernel 6.7 or newer (on Debian 12 that means the backports kernel) and reinstall. On x86 the normal PPA path takes over from there, and a new install gets the 3.1 profile. ⚠️ On ARM, mind the order: the prebuilt package is tried first, so if a prebuilt exists for your new kernel you will get the pinned 2.0 again - the third version arrives only when no match is found. And the 3.1 profile is not issued on ARM at all for now, whichever module lands. **The PPA module is updated together with the system, and it has no rollback.** On the main path (x86_64, kernel 6.7 or newer) `amneziawg-dkms` comes from the PPA and changes with a plain `sudo apt upgrade`, and also when the installer is run again, since step 1 updates the system. `unattended-upgrades` in its stock configuration does not install packages from the PPA (and if package cleanup ran during the install, it is gone; cleanup is skipped with `--keep-packages`, `--no-tweaks` or a refusal at the installer's prompt; check with `dpkg -s unattended-upgrades`), so the build changes when you update the system. The loaded module stays the same until a reboot. The PPA keeps only the current build: a superseded one disappears from the archive about a day later, so apt cannot take you back to it. To decide yourself when the build changes, hold the package: `sudo apt-mark hold amneziawg-dkms` (release with `sudo apt-mark unhold amneziawg-dkms`); the price is that fixes stop arriving along with new builds. Running the installer again (on a working server this needs `--force`) releases this hold at step 2, after the reboot, and the build itself changes at the next `apt upgrade`, so set the hold again once the installation has finished. The module version string does not tell builds apart; the running one is shown by `cat /sys/module/amneziawg/srcversion`. ### What 3.0 changes on the wire, and what it does not Nothing you have already configured. Measured: four interfaces with configs from different generations come up and pass traffic at the same time on one 3.0 module - with no AWG parameters at all, in 1.x style, in 2.0 style, and with 3.0 parameters. The mechanism: `H1`-`H4` became ranges inside the module, but an interface that does not set them gets single-value ranges holding exactly the WireGuard message types. That is visible on the wire too: | Interface | What goes out | |---|---| | no AWG parameters | bytes `01 00 00 00` at offset 0, i.e. plain WireGuard | | `S1 = 100`, `H1 = 1234567` | the `H1` value at offset 100, junk packets from `Jc` ahead of it | A scalar `H1 = 12345` from a 2.0 config is parsed as a range of one number, and that exact number goes on the wire. So upgrading the module does not by itself break anything for your clients. ### The new 3.0 parameters Compared with the previous `amneziawg-tools` release, seven config keys were added and **none were removed**: everything from 2.0 (`Jc`/`Jmin`/`Jmax`, `S1`-`S4`, `H1`-`H4`, `I1`-`I5`) is still accepted. | Parameter | What it does | Must match on both ends? | |---|---|---| | `HeaderProtectionKey` | ChaCha20 header encryption | **yes, otherwise there is no handshake at all** | | `ContentPaddingAddition` | extra padding inside the encrypted part | no | | `RekeyAfterTime` | when to start rekeying | no | | `RekeyTimeout` | pause between handshake attempts | no | | `RejectAfterTime` | when a session counts as expired | no | | `KeepaliveTimeout` | pause before a keepalive packet | no | | `MaxHandshakeAttempts` | how many handshake attempts to make | no | All seven work through `awg-quick` as well: it hands every key it does not consume itself to `awg setconf`, so hand-written configs need nothing special. 🔴 **Six of the seven take a RANGE rather than a single number, and the value is drawn from it at random on every use.** Those are `ContentPaddingAddition`, `RekeyAfterTime`, `RekeyTimeout`, `RejectAfterTime`, `KeepaliveTimeout` and `MaxHandshakeAttempts`: the format is `lo-hi`, and a bare number is accepted too as a range of zero width. Verified in both implementations from the sources rather than from documentation: the kernel module declares them as `u16_range_t` (`src/device.h`) and draws the value in `u16_range_pick_one()` via `get_random_u32_inclusive()` (`src/type.h`); `amneziawg-go` uses the `UintRange` type with `FromString` and `PickOne` (`device/noise-types.go`) and parses the keys in `device/uapi.go`. That is the whole point: a constant handshake and keepalive interval was a tell in itself, and a range removes it. The one exception among the seven is `HeaderProtectionKey`, which is a key rather than a number. ⚠️ Practical consequence: if you set these by hand, remember that `KeepaliveTimeout = 20-30` is neither a typo nor a "between these" timeout, it means a fresh random pick on every use. Of the seven the installer sets two, `HeaderProtectionKey` and `ContentPaddingAddition` (the 3.1 profile, see [below](#awg3-not-yet-adv)), and leaves the timers alone. The timers act only on the side where they are set: there is no need to match them with the client, and the script does not carry them into client profiles, even if you wrote them into the server's `awg0.conf`. ### What has to match and what does not This is the part most often misread. The kernel module's own README describes only 1.x/2.0. The `amneziawg-go` README covers the third line too (header protection, padding, timings) and splits parameters into server-side (must match) and client-side (need not), but it does not say what happens on a mismatch or how to see it. Below is what was measured. - **`S1`-`S4` have to match.** These are not just padding: the receiver reads the message type **at the offset** given by its own `S`. Different values mean the packet is not recognised. - **`H1`-`H4` have to be compatible.** The sender picks a value from its range and the receiver checks that the value falls inside its own. Keeping them identical is simpler. - **`HeaderProtectionKey` has to be identical.** Tested: the key on the server only, on the client only, or two different keys all give no handshake and 100% loss. It is not a gradual degradation but a switch: the moment the server has it, every peer without it drops. - **`Jc`/`Jmin`/`Jmax` and `I1`-`I5` do not have to match.** Junk and concealment packets are sent by whoever initiates the handshake, and the other side simply ignores them. 🔴 **Do not enable `RandomTrailers` until a fix ships.** The flag itself does not cost speed: on a bare config (`Jc = 0`, `S1`-`S4 = 0`, `H1`-`H4 = 1..4`, no `I1`-`I5`) the measurement was **11.94 MB/s** both with and without it. The drop appears in combination with the rest of the obfuscation: with `Jc`, `S1`-`S4`, `H1`-`H4` and `I1` together with the flag, throughput came out at **0.09-0.14 MB/s**, that is 85 to 133 times lower. What was measured per combination, all values **with the flag on**: `S1`-`S3` together with `H1`-`H4` gave 5.60 MB/s, and adding `S4` dropped it to 0.11. Each group on its own - `S4`, `S1`-`S3`, `H1`-`H4`, `Jc`, `I1` - produced no drop at all: 11.7-11.9 MB/s. So it is not these parameters that cost the throughput but their combination with the flag, and there is no need to turn obfuscation off for the sake of speed. The drop also reproduces on a local network with no internet leg: **44.22 MB/s without the flag against 0.09 with it**. The route is therefore not involved. The earlier explanation through packet loss along the path was checked and refuted: the loss turned out to be a symptom rather than the cause. There is a second, separate reason to keep the flag off. With the flag on and wide `H1`-`H3` ranges, the receiver can take some data packets for handshake packets and drop them. How many depends on `S1`-`S4` and `H4`, and some layouts lose none at all; on a synthetic set with `H3` 3.5% of 2³² wide, 6.7-7.3% of packets were lost, the same on the kernel module and on amneziawg-go. This does not explain the drop in speed, it is a different effect. With this installer's 3.1 profile (`H1`-`H4` = 1-4 and a header protection key) the share is negligible. Analysis and measurements: [amneziawg-linux-kernel-module#226](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/issues/226). ### Three things that can cost you an evening **`HeaderProtectionKey` requires `S1`-`S4` to be at least 12.** The nonce for header encryption is never transmitted; it is taken from the first 12 bytes of the S padding, so a smaller value makes the config invalid. Exactly 12 is fine, even though the kernel message says "must be more then 12". `amneziawg-go` also names the parameter off by one: an error about `S0` means `S1`, one about `S2` means `S3` ([amneziawg-go#201](https://github.com/amnezia-vpn/amneziawg-go/issues/201)). The key itself is 32 bytes in base64, a 44-character string ending in `=`, the kind `awg genkey` prints. A number or a password in its place will not be accepted. **That message is invisible by default.** `awg` prints only `Unable to modify interface: Invalid argument`, while the explanation goes to the kernel debug log. To see it: ```bash echo "module amneziawg +p" > /sys/kernel/debug/dynamic_debug/control # repeat the failing command, then look at: dmesg | tail echo "module amneziawg -p" > /sys/kernel/debug/dynamic_debug/control ``` **A parameter cannot be removed with `awg setconf` or `awg syncconf`.** Drop the line from the config, apply it again, and the value stays: for AWG parameters these commands only add. Getting an interface back to a clean state means recreating it, that is `systemctl restart awg-quick@awg0` (or `awg-quick down` then `up`). Since v5.25.0 the management scripts track this and **warn**: they remember the set of interface parameters applied last time, and if a parameter has disappeared from `awg0.conf`, the log gets a `Removed from the [Interface] section: S1` warning along with the command that carries the removal through to the live interface. The script does **not** restart on its own: that would drop every client connection, and a decision with that price belongs to a person rather than to automation. The warning normally appears once: the snapshot is refreshed after a successful apply, and also after `manage restart`, `manage restore` and an install, that is after everything that recreates the interface. If the apply failed, the snapshot stays as it was and the warning repeats on the next run - deliberately, because otherwise a failure would silently mute the reminder that the parameter is still on the live interface. Parameter values are still applied without a restart: `syncconf` changes those correctly, the problem is only removal. ### The 3.1 profile: what the installer turns on and what it leaves out Since version 6.0.0 a new install without a flag gets the 3.1 profile. It differs from the 2.0 profile like this: | Parameter | 2.0 profile | 3.1 profile | |---|---|---| | `HeaderProtectionKey` | none | a random key per install, one for the server and all of its clients | | `ContentPaddingAddition` | none | `32-128` | | `H1`-`H4` | random non-overlapping ranges | `1`, `2`, `3`, `4` | | `S3` / `S4` | from 8 / from 4 | from 12 / from 12 | | `Jc`/`Jmin`/`Jmax`, `S1`/`S2`, `I1` | as before | as before | - **`H1`-`H4` are set to 1-4 on purpose.** The key encrypts the header, so the message type is no longer visible on the wire whatever `H` is: random ranges add nothing. What makes each install unique now is the key. - **`S3` and `S4` start at 12** because the first 12 bytes of S-padding serve as the nonce for header encryption (details in [Three things that can cost you an evening](#awg3-gotchas-adv)). - **The key** lives in the `[Interface]` section of the server `awg0.conf`, with a copy in `/root/awg/server_hpk.key`. `manage` writes it into every client profile and into the `vpn://` link, and `backup` and `restore` carry it together with the config. Changing the key means reissuing every client profile: a client with the old key will not connect, and will not show an error either. - **`ContentPaddingAddition`** is one-sided and does not need to match. The defect that kept padding out before (an idle tunnel redid its handshake every 15 seconds instead of roughly 147, [issue #186](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/issues/186)) was fixed upstream on 5 August 2026 ([PR #208](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/pull/208)) and verified on a test server. What the profile leaves out, and why: - **`RandomTrailers`** is two-sided, and together with the rest of the obfuscation it cuts throughput by tens of times (measurement in [What has to match](#awg3-must-match-adv)). It stays off until a fix ships. - **`DisableCookies`** takes away, together with the cookie-reply signature, the protection against handshake floods from spoofed addresses (see [What 3.1 added](#awg3-31-adv)). - **The timer parameters** are one-sided and safe, but without a separate measurement they buy little, so the installer does not set them. The configs carry no `RandomTrailers` or `DisableCookies` line at all, not even with the value `off`. ### Where 2.0 is installed instead of 3.1 The installer checks the machine twice. At step 0 it checks the architecture and the kernel: the 3.1 profile is issued only on x86_64 with kernel 6.7 or newer. At step 3, once the packages are in and the reboots are behind, it checks the `awg` tools and the loaded module: the module is checked with a probe, that is, the profile is applied to a temporary interface and read back. | Situation | What the installer does | |---|---| | no generation chosen, the machine does not fit | installs 2.0 by itself: the reason goes to the log as a separate block and is repeated in the final report. At step 3 it first checks the 2.0 set on a temporary interface the same way and, if the module refuses it, stops without touching the settings file | | second-line `awg` tools with a third-line module (`tools_old`) | such tools cannot apply the 2.0 set either (they do not set the `H1`-`H4` ranges), so the install stops and names the command to upgrade `amneziawg-tools`; the settings file is untouched. With a second-line module the 2.0 set passes the probe, and the installer installs 2.0 | | `--protocol=3.1`, the machine does not fit | stops and names the reason code | | `--protocol=2.0` | installs 2.0, no third-line checks needed | | the server has already issued profiles (reached step 6) | never changes the generation. If a 3.1 server stops passing the check, the installer names the reason and the repair (upgrade the tools or the module, boot a suitable kernel); it names removal only as the last resort | Reason codes: `kernel`, `arm`, `arch_unsupported`, `arch_unknown` (checked at step 0 and again at step 3); `tools_old`, `module_line2`, `module_probe_failed` (step 3). The generation a new install gets without `--protocol`, by system, on the stock kernel: | System | Stock kernel | x86_64 | ARM | |---|---|---|---| | Ubuntu 24.04 | 6.8 | 3.1 | 2.0, code `arm` | | Ubuntu 26.04 | 7.0 | 3.1 | 2.0, code `arm` | | Debian 13 | 6.12 | 3.1 | 2.0, code `arm` | | Debian 12 | 6.1 | 2.0, code `kernel` | 2.0, code `arm` | The installer looks at the running kernel, not the stock one: a system on x86_64 booted with kernel 6.7 or newer (from backports, for example) gets 3.1; on ARM the generation is 2.0 with any kernel. That is the step 0 check. At step 3 the installer can also switch to 2.0 on a suitable system when the `awg` tools or the module do not understand the third line or the module check itself fails (codes `tools_old`, `module_line2`, `module_probe_failed`). The generation and how it was chosen are recorded in `/root/awg/awgsetup_cfg.init`: `AWG_PROTOCOL` (`2.0` or `3.1`), `AWG_PROTOCOL_SOURCE` (`default` if the generation came from the default, `explicit` if from the flag) and `AWG_PROTOCOL_FALLBACK` (the reason code if the installer switched to 2.0 by itself). A server installed before version 6.0.0 has no `AWG_PROTOCOL_SOURCE`, and its `AWG_PROTOCOL` marker is `2.0` (since v5.32.0) or absent (earlier); either way it reads as 2.0. **Changing the generation of a running server** takes a reinstall: `--uninstall`, then an install with the `--protocol` you want. Every client profile is issued again, so it is better to decide before installing. The situation on kernels older than 6.7 is covered separately in Debian support. --- ## ⚙️ Client Configuration Details ### AllowedIPs Defines which traffic the **client** routes through the VPN tunnel. 1. **Mode 1: All traffic (`0.0.0.0/0`) - the default** * All client IPv4 traffic → VPN, plus `::/0` so the device's IPv6 does not bypass the tunnel (without `--allow-ipv6-tunnel`; with it the rules differ, see the dual-stack section below). * This exact pair of routes is what the Amnezia app recognises as a full tunnel: its own split-tunneling page stays available, and `awg-quick` on Linux engages policy routing instead of looping the routes. * The cost: the LAN goes into the tunnel along with everything else. Whether it stays reachable is decided by the client, not by the config. 2. **Mode 2: Amnezia List + DNS** * List of public IP ranges + DNS `1.1.1.1`, `8.8.8.8`. Private networks (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`) stay outside the tunnel. * **Purpose:** keep the LAN outside the tunnel. In terms of PUBLIC IPv4 coverage it is the same full tunnel, with no separate gain for bypassing restrictions; what stays out of the list is the private networks, the reserved `0.0.0.0/8` and multicast. * The cost: the Amnezia app sees a list that is not `0.0.0.0/0, ::/0`, concludes that the server already does split routing and disables its own page; on Linux `awg-quick` such a config can produce a routing loop. * IPv6: without `--allow-ipv6-tunnel`, `2000::/3` (all global IPv6) is added to this list, and a service address like `fddd:2c4:2c4:ffff::a09:905/128` to the client's `Address`. The device's internet IPv6 goes into the tunnel and dies there, while the whole LAN stays outside the tunnel, over IPv4 and IPv6 alike. Not `::/0`, because with it the AmneziaWG Windows client blocks all traffic outside the tunnel, and the LAN is gone. If the device's IPv6 has to keep working, see [Device IPv6, `::/0` and the local network](#client-ipv6-adv). 3. **Mode 3: Custom (Split-Tunneling)** * Only traffic to specified networks → VPN. * Example: `192.168.1.0/24,10.50.0.0/16` * `::/0` is not added as long as the list does not cover all public IPv4: the device's IPv6 goes around the tunnel directly. **AllowedIPs Calculator:** [WireGuard AllowedIPs Calculator](https://www.procustodibus.com/blog/2021/03/wireguard-allowedips-calculator/). ### Device IPv6, `::/0` and the local network `--disallow-ipv6` and the `DISABLE_IPV6` key in `awgsetup_cfg.init` turn IPv6 off on the server itself (the host sysctl). They do not touch your device's IPv6 and do not decide whether a `::/0` route goes into the client's `AllowedIPs`. The routing mode decides that: - **a full tunnel** without `--allow-ipv6-tunnel`: mode 1 gets `::/0`, mode 2 gets `2000::/3` and a service IPv6 address. The device's IPv6 goes into the tunnel, gets no further, and the device falls back to IPv4. While the VPN is on, resources reachable only over IPv6 do not open; in mode 1 the home network's local IPv6 is out of reach too, in mode 2 it stays reachable; - **your own network list** (mode 3, `--route-custom`) does not get `::/0` unless it covers all public IPv4. The device's IPv6 goes around the tunnel with its own address; - **with `--allow-ipv6-tunnel`** the rules differ: a full tunnel gets `::/0` only when the server has native IPv6, and then the device's IPv6 goes through the VPN. Without native IPv6 the client gets only the tunnel subnet, and the device's global IPv6 goes around the tunnel again. Details in [IPv6 Dual-Stack Tunnel](#ipv6-tunnel-adv). - **mode 2 with `--client-ipv6-direct`** (the `CLIENT_IPV6_DIRECT=1` key in `awgsetup_cfg.init`): no IPv6 route and no service address. IPv4 goes through the tunnel, the device's IPv6 goes directly through the provider, and the local network stays reachable. The cost: sites opened over IPv6 see the device's real address, and a resource blocked by the provider and reachable over IPv6 stays blocked. The key is not allowed in mode 1 (without `::/0` iOS AmneziaVPN does not bring the tunnel up) or together with `--allow-ipv6-tunnel`, and in mode 3 it changes nothing. A plain `manage regen` takes the route away from clients already issued and leaves hand-made routes alone; import the profiles again after it. To switch it off: the line `CLIENT_IPV6_DIRECT=0` in `awgsetup_cfg.init` and `regen` again. `manage regen` also adds an IPv6 route to already issued full-tunnel profiles whose `AllowedIPs` hold no IPv6 route, including lists edited through `modify`, and replaces `::/0` with `2000::/3` in mode-2 profiles issued with `::/0`. How this looks from a site's side and how to check from the device is covered in [What a site can see when traffic is split by destination](#split-detect-adv). ### What a site can see when traffic is split by destination Splitting traffic by destination picks the exit for a connection from where that connection is headed. That is how the [two-server cascade](CASCADE.en.md) works, and the [WARP scheme for the Russian segment](WARP-RU.en.md), and a client-side `AllowedIPs` list from which some networks were deliberately excluded - to have Russian sites open outside the VPN, say (that is mode 3 above; mode 2 does not belong here, it sends practically all public IPv4 into the tunnel). A site does not learn your address from the connection alone. A script on the page can ask a third-party IP lookup service instead - and the request to that service goes to a different address, so it may well leave through a different leg than the site itself. The service then reports the address of whichever exit it was reached from, the script hands that answer to the site, and the site holds two different addresses: the connection's source IP and whatever the script reported. The mismatch does not always happen: what decides it is which side of the split the service itself falls on. Land it on the same leg as the site and there is one address. The catch is that you do not get to choose, and in the sample below nearly all such services sat on the "foreign" side. The three schemes differ in what it costs you: - **a client-side list with exclusions.** The site sees your home address while the lookup request goes through the tunnel, so the site ends up with both - home and server; - **the cascade.** The site sees the entry server's address and the script reports the foreign exit's. Your home address is not exposed: both addresses are your own servers, provided the device's IPv6 is not going around the tunnel (see below); - **the WARP scheme.** The site sees a Cloudflare address, while the script reports the server's own - which is exactly the datacenter address the scheme was routing around. Adding lookup services to the exception list is an unreliable path. A check on 7 August 2026 against the snapshot of Russian networks in this repository (`cascade/ru.zone`, 8626 networks): I took 24 domains of popular services of this kind, resolved each through two resolvers (`dig @8.8.8.8` and `@77.88.8.8`) and ran the resulting addresses against the snapshot. - two domains landed inside the snapshot - `internet.yandex.ru` and `ipinfo.yandex.ru`, one and the same service under two names, sharing an address. The other 22 sat outside Russian networks, so a request to them leaves through the "foreign" leg; - a name in the `.ru` zone says nothing about the address itself: `2ip.ru` and `myip.ru` answered from addresses that are not in the snapshot; - the addresses are not stable either. `api.ipify.org` handed out three different addresses within a single day, and different resolvers answer differently: it is anycast behind Cloudflare. There is nothing to write into a list once. The check is easy to repeat: take the addresses from `dig` and run them against `cascade/ru.zone` with the `ipaddress` module from Python's standard library. ⚠️ This was measured against the snapshot the cascade uses. The WARP scheme has a list of its own - the antifilter BGP feed - and I did not check its contents this way: the mechanism is the same, but these particular numbers do not apply to it. What these numbers do NOT prove. How often sites actually compare the connection's address with what the script reported is something I did not measure. And the mismatch alone is no proof of a VPN: a dual-stack visitor with no VPN at all can open a site over IPv6 while the lookup service answers over IPv4. What is verified here is only that with traffic split the mismatch can appear, and that it is visible from outside. If a site has to see one and the same address, splitting by destination cannot guarantee that, no matter where the split sits. What removes the split is a full tunnel (`--route-all`): all traffic then leaves through one exit. One caveat: different IPv4 and IPv6 addresses of the same server can still remain, but that is the ordinary picture for any dual-stack connection and is not a sign of split traffic. And if the address has to be Russian, the server has to be in Russia. **IPv6 is a separate layer, and an easy one to mix up.** When the guides say "IPv6 is off, that is how the installer sets it up", they mean IPv6 **on the server** (the host sysctl). IPv6 **on your device** is something the installer never touches, and the traffic split does not apply to it either: the list of Russian networks used by the cascade and the BGP feed used by the WARP scheme are IPv4-only (the `cascade/ru.zone` snapshot holds no IPv6 entries at all). Whether your device's IPv6 goes around the tunnel is decided not by the scheme but by whether an IPv6 route (`::/0` or `2000::/3`) made it into `AllowedIPs`: - **full tunnel without `--allow-ipv6-tunnel`** - both the default "All traffic" mode (`--route-all`) and the "Amnezia" mode land here: the client gets an IPv6 route next to its IPv4 routes (`::/0` in the "All traffic" mode, `2000::/3` with a service address in the "Amnezia" mode), so the device's IPv6 goes into the tunnel. The tunnel itself carries no IPv6, so that traffic gets no further - but it does not leak outside the VPN either. The flip side: while the VPN is on, resources reachable ONLY over IPv6 are unreachable, and in the "All traffic" mode the local network's IPv6 goes into the tunnel too (in the "Amnezia" mode the LAN stays reachable over IPv4 and IPv6 alike); - **split routing** (mode 3, your own network list via `--route-custom`): `::/0` is not added as long as the list does not cover all public IPv4, since that would break the split itself. The device's IPv6 goes around the tunnel with its real address, and then the site needs no scripts at all - it sees that address directly; - **`--allow-ipv6-tunnel` enabled**: the rules are different and depend on whether the server has native IPv6. They are documented in exactly one place, [IPv6 Dual-Stack Tunnel](#ipv6-tunnel-adv) - including the case where the client only gets the tunnel ULA and the device's global IPv6 goes around again. > **Profiles issued before v5.31.0 keep the old list.** The "Amnezia" mode, the default back then, gained `::/0` in v5.31.0; before that only "All traffic" got it, and the device's IPv6 went around the tunnel. Re-issuing delivers the fix to already issued clients: `sudo bash /root/awg/manage_amneziawg.sh regen`, then re-import the profiles on the devices. Individual `AllowedIPs` set through `modify` are preserved. A client issued with `--allow-ipv6-tunnel` will not get `::/0` from a plain re-issue - its IPv6 part counts as an individual setting too; such a client needs `regen --reset-routes`, and the installer prints that hint itself. "Amnezia" mode profiles issued with `::/0` are moved by the same `regen` to `2000::/3` with a service address: with `::/0` the AmneziaWG Windows client cut off the LAN. The `::/0` is replaced on a list whose IPv4 part matches the server's current list (exactly how the installer wrote it), however that list was set. With any other list, and for a client issued with `--allow-ipv6-tunnel`, `::/0` stays as it is; when a full tunnel keeps `::/0`, `regen` warns and says how to move to `2000::/3`: `regen --reset-routes ` on a server in the "Amnezia" mode, `modify` in the other modes. To check from a connected device: `curl -6 ifconfig.co`. Look at the address rather than at the fact that an answer arrived - with IPv6 working inside the tunnel an answer comes back too. If it is your home address, IPv6 is going around the tunnel. If you want a guarantee that nothing does, turn IPv6 off on the device itself: a server-side filter does not help here, because that direct IPv6 traffic never reaches the server. ### Client Isolation By default VPN clients cannot see each other: the server adds a rule to `PostUp`/`PostDown` in the `awg0.conf` config, `iptables -I FORWARD -i awg0 -o awg0 -j DROP` (plus a symmetric `ip6tables` rule if the IPv6 tunnel is enabled) - it cuts traffic between peers before the general `ACCEPT`, regardless of the routing mode. Before this setting, isolation was an accidental side effect of the mode: the "Amnezia" mode and a custom network list isolated clients only because their `AllowedIPs` had no route to their neighbors (the tunnel subnet is not in the list), `--route-all` did not isolate at all, and dual-stack clients in those modes remained reachable to each other over the tunnel's IPv6 subnet. **Disabling it:** the `--isolation=off` flag at install time (or answering `n` to the interactive question "Isolate VPN clients from each other?" on first run without `--yes`). The DROP rule is not added, and the tunnel subnet is appended to clients' `AllowedIPs` (modes 2/3 - mode 1 with `0.0.0.0/0` already covers it), so devices can see each other inside the VPN. **Switching it on an already installed server:** ```bash sudo bash ./install_amneziawg_en.sh --force --isolation=off # or --isolation=on ``` The setting is persisted in `awgsetup_cfg.init` (the `CLIENT_ISOLATION` key). Just like a routing-mode change, changing isolation via reinstall does not touch already-issued client configs - they need an explicit reissue: `sudo bash /root/awg/manage_amneziawg.sh regen --reset-routes` (the installer prints this hint after a reinstall that changes the mode). **Legacy configs.** A config without the `CLIENT_ISOLATION` key (created before this setting existed) is treated as isolated (`1`) - that is the previous default behavior of the list-based modes, so there is no surprise on a reinstall without `--isolation`. ### IPv6 Dual-Stack Tunnel (v5.15.0+) By default the tunnel carries IPv4 only. Starting with v5.15.0 you can also enable IPv6 inside the tunnel - clients get an IPv6 address next to IPv4 (dual-stack). > **IPv6 in the default IPv4-only mode.** When the tunnel is IPv4-only, your device's IPv6 traffic goes out directly, outside the VPN - by design: an IPv4-only tunnel does not carry IPv6, and the server has no say in it (this is a property of the mode, not a server-side leak). One qualifier: that holds for split routing (mode 3, your own network list). In a full tunnel - which means both the default `--route-all` and the "Amnezia" mode - the client is handed an IPv6 route next to its IPv4 routes, so the device's IPv6 goes into the tunnel and dies there rather than going out directly - see [What a site can see when traffic is split by destination](#split-detect-adv). If you want IPv6 inside the tunnel, enable `--allow-ipv6-tunnel` (below). If instead you want a guarantee that nothing goes outside the VPN, turn IPv6 off on the device itself: a server-side filter does not help here, because that direct IPv6 traffic never reaches the server. **When it activates:** only with the explicit `--allow-ipv6-tunnel` flag on `install_amneziawg.sh`. Without the flag the behavior is identical to earlier versions. This is separate from `--allow-ipv6` / `--disallow-ipv6`, which control host-level IPv6 (sysctl) and are unchanged. **Interaction with `--disallow-ipv6`.** In-tunnel IPv6 needs host IPv6 forwarding, so if you combine `--allow-ipv6-tunnel` with `--disallow-ipv6` the tunnel flag wins: the installer logs a warning and keeps host IPv6 forwarding enabled. This does not happen silently. **IPv6 routing mirrors the chosen IPv4 mode (intent-mirroring).** When `--allow-ipv6-tunnel` is enabled, the client's IPv6 `AllowedIPs` mirror the IPv4 mode: - **Full tunnel** (the default "All traffic" mode, or "Amnezia" - both cover all public IPv4 with routes): with native IPv6 on the server, `::/0` is added next to the IPv4 routes and all IPv6 traffic goes out to the internet through the VPN; without native IPv6 the tunnel subnet `fddd:2c4:2c4:2c4::/64` is added instead - IPv6 only works peer-to-peer inside the tunnel. - **Split-tunnel** (custom list via `--route-custom`): the IPv4 list is kept unchanged and ONLY the tunnel ULA subnet `fddd:2c4:2c4:2c4::/64` is added. `::/0` is never added - capturing all IPv6 in split mode would break split routing. IPv6 in a split tunnel reaches other peers but not the internet. > Historical note: in v5.15.0 dual-stack always implied a full tunnel (split-tunnel with IPv6 behaved differently). Since v5.15.1 split-tunnel and IPv6 combine correctly per the rules above. If a client was created on v5.15.0, recreate it (`manage remove` + `add`) to get the corrected `AllowedIPs`. **Subnet:** the private ULA `fddd:2c4:2c4:2c4::/64`. The server takes `::1`, clients get `::2`, `::3`, and so on, mirroring the IPv4 numbering. The subnet can be overridden before the first run via `IPV6_SUBNET=` in `/root/awg/awgsetup_cfg.init`. **How native IPv6 is detected on the server.** The script considers the server to have internet-routable IPv6 only when BOTH conditions hold: 1. a global IPv6 address outside the ULA range (`fc00::/7`, i.e. not `fddd:...`) - checked via `ip -6 addr show scope global`; 2. a default IPv6 route - checked via `ip -6 route show default`. A ULA address has scope global on its own but is not routed to the internet, so an address alone is not enough - a default route is also required. If either condition is missing, the server is treated as having no native IPv6: the client gets the tunnel ULA instead of `::/0` (per the routing rules above) and a warning is logged. The tunnel stays fully functional over IPv4. **How to add it to an existing install.** Re-run the installer with `--force` and the tunnel flag: ```bash sudo bash ./install_amneziawg_en.sh --force --allow-ipv6-tunnel # RU version: sudo bash ./install_amneziawg.sh --force --allow-ipv6-tunnel ``` `--force` is required: without it a run on an already-working server aborts at the idempotency guard and the flag is ignored. It is `--force` that re-renders the server config as dual-stack (`[Interface] Address`, sysctl, PostUp with ip6tables). Without it an existing install is left unchanged and the server never gets its IPv6. Just setting `ALLOW_IPV6_TUNNEL=1` in `/root/awg/awgsetup_cfg.init` is not enough on its own - it does not re-render the server config. Already-issued IPv4-only clients are not changed by this. To give IPv6 to such a client, recreate it - only recreation allocates an IPv6 for that client on the server: ```bash sudo bash /root/awg/manage_amneziawg.sh remove sudo bash /root/awg/manage_amneziawg.sh add ``` Then re-import the new `.conf` on the device. A plain `regen` will not help here: it mirrors the addresses from the client's `[Peer]` entry on the server, and an old client has no IPv6 there yet, so the config stays IPv4-only. `manage list` correctly shows the mixed state (dual-stack next to IPv4-only). **Troubleshooting:** - **ULA subnet collision.** If `fddd:2c4:2c4:2c4::/64` is already used in your network, set a different ULA subnet via `IPV6_SUBNET=` before installing. Not `fddd:2c4:2c4:ffff::/64`: that prefix is taken by the IPv6 sink of routing mode 2, and the installer stops on such a value. - **IPv6 not routing to the internet.** Check both signs of native IPv6: a global address outside ULA (`ip -6 addr show scope global`) AND a default route (`ip -6 route show default`). If either is missing, IPv6 internet egress is not possible - this is expected, and the tunnel works over IPv4. If both are present, check the ip6tables MASQUERADE rule and forwarding (`sysctl net.ipv6.conf.all.forwarding`). - **Rollback / IPv6 cleanup.** Setting `ALLOW_IPV6_TUNNEL=0` does not remove dual-stack `AllowedIPs` entries already added to `awg0.conf`. For a full cleanup: `awg-quick down awg0; sed -i 's|, fddd:[^/]*/[0-9]*||g' /etc/amnezia/amneziawg/awg0.conf; awg-quick up awg0`. ### PersistentKeepalive * **Default value:** `33` seconds. * Maintains the UDP session through NAT. * **Change:** `sudo bash /root/awg/manage_amneziawg.sh modify PersistentKeepalive 25` ### DNS * **Default value:** `1.1.1.1, 1.0.0.1` (Cloudflare, primary + fallback). * DNS server for the client inside the VPN. * **Change:** `sudo bash /root/awg/manage_amneziawg.sh modify DNS "8.8.8.8,1.0.0.1"` ### Your Own DNS Server for Clients (Pi-hole, AdGuard Home, Blocky) You can run an ad blocker or your own resolver on the VPN server itself and give clients the server's tunnel address as their DNS. The installer does not set such a service up, and it does not get in its way either. Skip anything from the list below and the setup will not work, without a single error: 1. **The service listens on the server's tunnel address, taken from the configuration.** It is `10.9.9.1` by default, but with `--subnet` the address is different. `ip -4 addr show awg0` or the `Address` line in `/etc/amnezia/amneziawg/awg0.conf` shows yours. Use it instead of `10.9.9.1` in the commands below. 2. **Clients get this address as their DNS:** `sudo bash /root/awg/manage_amneziawg.sh modify DNS "10.9.9.1"` for every client. To give new clients this address from the start, set `CLIENT_DNS` in `awgsetup_cfg.init`, see [Changing Default Settings](#change-defaults-adv). 3. **The address is inside the client's `AllowedIPs`.** A full tunnel (`0.0.0.0/0` in the `AllowedIPs` line of the client config, the default mode since v5.34.0) covers it. The mode 2 list sends private networks around the tunnel on purpose, and your own mode 3 list does not have the address unless you add it. That holds with client isolation on, which is also the default. Then the query goes around the tunnel and never reaches the server, and the device may be left without DNS or quietly ask the provider's DNS: the blocking looks configured but does nothing. The simplest fix is adding the server's address to the client's list: ```bash # current list: everything after "AllowedIPs = ", IPv6 entries included sudo sed -n 's/^AllowedIPs = //p' /root/awg/.conf sudo bash /root/awg/manage_amneziawg.sh modify AllowedIPs ", 10.9.9.1/32" ``` Do the same for new clients. A plain `regen` keeps such a list, `regen --reset-routes` resets it. 4. **Port 53 is open on `awg0` only.** If UFW is active (`sudo ufw status verbose` shows `Default: deny (incoming)`), incoming traffic is denied, and without a rule the queries from the tunnel are dropped: `sudo ufw allow in on awg0 to any port 53`. A rule without `in on awg0` opens the resolver to the whole internet. If the service runs in Docker, UFW does not filter its published ports, so publish the port on the tunnel address only: `-p 10.9.9.1:53:53/udp -p 10.9.9.1:53:53/tcp`. 5. **Files copied over from Windows are stripped of `CRLF`:** `sed -i 's/\r$//' `. A carriage return at the end of a list line can become part of the domain name, and that line stops blocking. After changing a client, import its config on the device again. These pitfalls were worked out by @Cueuler in [discussion #237](https://github.com/bivlked/amneziawg-installer/discussions/237). ### Changing Default Settings **The DNS of new clients** is set by the line `export CLIENT_DNS='...'` in `/root/awg/awgsetup_cfg.init`. The installer writes it there empty; put the value between the quotes, for example `export CLIENT_DNS='10.9.9.1'`. Installs older than this version have no such line; add it at the end of the file. The value is IP addresses separated by commas. Clients created after the edit (`manage add`) get it, and so does a client whose config `regen` rebuilds from scratch. Configs already handed out do not change: `regen` keeps their DNS, and `modify DNS ...` changes it. An invalid value is not silently replaced: `add` and the installer refuse and say what is wrong, while `regen` of existing clients works as before. An empty value means `1.1.1.1, 1.0.0.1`. A `--force` reinstall keeps the line. **PersistentKeepalive** has no such setting: new clients get 33, and a client's value is changed like this: ```bash sudo bash /root/awg/manage_amneziawg.sh modify PersistentKeepalive 25 ``` **Obfuscation parameters by hand** (`Jc`/`Jmin`/`Jmax`, `S1`-`S4`, `H1`-`H4`, `I1`-`I5`). Edit them in `/etc/amnezia/amneziawg/awg0.conf`: `add`, `regen` and a `--force` reinstall all take them from there. The copy in `/root/awg/awgsetup_cfg.init` is used only when `awg0.conf` is absent, which normally means the first install. You can update it as well so the two files do not drift apart, but an edit made only there reaches neither the server nor the clients (`manage` warns about it). 1. Change the value in the `[Interface]` section of `awg0.conf`. Write the key names exactly as they are in the file (`Jc`, `Jmin`, `Jmax`, `S1` and so on; `I1`-`I5` in upper case only). Optionally put the same value into `awgsetup_cfg.init` too (the `export AWG_S1='...'` line and so on). 2. Restart the service: `sudo systemctl restart awg-quick@awg0`. Without a restart a removed parameter stays on the live interface, see [Three things that can cost you an evening](#awg3-gotchas-adv). 3. Reissue every client profile: `sudo bash /root/awg/manage_amneziawg.sh regen`, and import them again on each device. `S1`-`S4` and `H1`-`H4` must match, so the old profiles stop connecting. `I1` alone needs no reissue: it can be changed in the client `.conf` too. 4. On a 3.1 server leave `H1`-`H4` alone (they are 1-4) and keep `S1`-`S4` at 12 or more. Do not change the `HeaderProtectionKey` by hand: a new key cuts off every client until they are reissued, and the normal way to get one is a reinstall (`--uninstall` and a new install). If you change it anyway, change its copy `/root/awg/server_hpk.key` at the same time, otherwise `manage` refuses to issue profiles. A plain `--force` reinstall keeps the set (except `I1` when the install was made with `--no-cps`: then `I1` is removed again). The `--preset`, `--mobile`, `--jc`, `--jmin` and `--jmax` flags regenerate the whole set on a reinstall, `S`, `H` and `I1` included, and after them every client needs a `regen`. The `--i1` flag changes only `I1` and pins it: later reinstalls keep the value, and `--no-cps` or the same `--preset`, `--mobile`, `--jc`, `--jmin` and `--jmax` drop the pin. After `--i1` a `regen` is needed too, or the clients keep the old `I1`. Editing the `awg_common.sh` library itself is pointless: the installer at step 5 and a script update overwrite it with their own copy. --- ## 🔒 Server Security Settings ### UFW Firewall * **Policies:** Deny incoming, Allow outgoing, Deny routed. * **Rules:** `limit 22/tcp` (SSH), `allow /udp`, `route allow in on awg0 out on ` (VPN traffic forwarding, added in v5.7.6). * **Check:** `sudo ufw status verbose` ### Kernel Parameters (Sysctl) File: `/etc/sysctl.d/99-amneziawg-security.conf`. Includes: * IP forwarding * IPv6 disable (optional) * BBR congestion control + FQ qdisc * TCP hardening (syncookies, rp_filter, RFC1337) * ICMP redirects and source routing disabled * Adaptive network buffers (rmem/wmem based on RAM) * nf_conntrack_max = 65536 * kernel.sysrq = 0 ### The kernel module and user namespaces The vendor's AmneziaWG kernel module has an unfixed bug: its `I1`-`I5` parser accepts a negative length in the ``, `` and `` tags, and in some tag combinations that writes past a kernel buffer ([amneziawg-linux-kernel-module#233](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/issues/233); as of 12 October 2026 the fixes in PRs #237 and #247 are not merged). The bug is in every build of the module the script installs: from the PPA, from the pinned 2.0 source and in the ready-made ARM packages built from it. It does not depend on the protocol generation. **Who it concerns.** Root is not the only one who can hand the module parameters through an interface of their own. The module's commands are open to any process with `CAP_NET_ADMIN` in its own user namespace and its own network namespace, and on Debian 12 and 13 any user can create one by default (the Debian kernel patch decides this, with `kernel.unprivileged_userns_clone = 1`). The module does not have to be loaded: once installed, it may load by itself on the first request, unless module loading is disabled. We checked this on an Ubuntu 24.04 test server without touching the bug itself: with the AppArmor restriction turned off, an ordinary user created their own AmneziaWG interface and set `I1` on it. On Ubuntu 24.04 and newer the AppArmor restriction (`kernel.apparmor_restrict_unprivileged_userns = 1`) is on by default, but it is a policy, not a ban: on our test server it refused a plain attempt but did not stop an ordinary user. What closes the path is `kernel.unprivileged_userns_clone = 0`: with it the kernel refuses an unprivileged process the namespace right in `unshare`/`clone`, before AppArmor is asked (that is how the kernel patch works), and on the test server `unshare` was refused with it. If the server has no other users and no unrelated programs, the risk is small. It appears when the machine has other accounts or services through which an outsider could get onto it. **What the script does.** Our commands never write negative lengths, overlong numbers or a total above 65535 bytes, and refuse them on install, `add`, `regen`, `modify` and `restore`. `manage check` and `manage diagnose` warn when the module is on the system and unprivileged user namespaces are allowed without any restriction, and name the command that closes this path on your machine. Under the AppArmor restriction (stock Ubuntu) they stay quiet: that was decided so the warning does not sit on every Ubuntu server, although the restriction does not close the path. To close it there as well, use the same command as below. The script does not change this setting itself: the cost of such a measure depends on what else runs on the server. **How to close it by hand.** On the stock Debian and Ubuntu kernels (both carry the kernel patch with this setting; on Ubuntu checked on 24.04): ```bash sudo sysctl -w kernel.unprivileged_userns_clone=0 echo 'kernel.unprivileged_userns_clone = 0' | sudo tee /etc/sysctl.d/99-userns.conf ``` The cost: rootless containers (Podman, rootless Docker), application sandboxes and any programs and services that create such namespaces without `CAP_SYS_ADMIN` on the host (root inside an ordinary container included) stop working. The Docker daemon in its usual mode is not affected. Namespaces that already exist are not affected: a process that entered its own namespace earlier keeps its access, so reboot after writing the file to be sure. On kernels without this setting (the Raspberry Pi OS kernel, for example, or one you built; check with `sysctl kernel.unprivileged_userns_clone`) what is left is `user.max_user_namespaces`. Write down its previous value first (`sysctl user.max_user_namespaces`), then: ```bash sudo sysctl -w user.max_user_namespaces=0 echo 'user.max_user_namespaces = 0' | sudo tee /etc/sysctl.d/99-userns.conf ``` This costs more: user namespaces can no longer be created at all, root included, so systemd services with `PrivateUsers=` and Docker with `userns-remap` stop working as well. To undo: `sudo rm /etc/sysctl.d/99-userns.conf`, then, if you set `kernel.unprivileged_userns_clone`, `sudo sysctl -w kernel.unprivileged_userns_clone=1`, and if you set `user.max_user_namespaces`, `sudo sysctl -w user.max_user_namespaces=`. **If you edit `I1`-`I5` by hand.** Lengths in ``, `` and `` must be non-negative whole numbers, at most 1000 per the vendor's documentation; our check does not enforce that limit, it refuses negative lengths, overlong numbers and a total above 65535 bytes. `manage restart`, a service restart, the start at boot and the other paths that apply an existing `awg0.conf` do it without our check, so they will not stop a mistake in a hand edit. ### Fail2Ban (Automatic Setup) * Automatically installed and configured for SSH protection. * **Settings:** Ban via `ufw`, 5 attempts → 1-hour ban. * **Backend:** `systemd` (journald) on both Ubuntu and Debian; on Debian the installer also installs `python3-systemd`. * **Check:** `sudo fail2ban-client status sshd`. #### Safe Configuration Loading (v5.7.2) Starting with v5.7.2, the `awgsetup_cfg.init` parameters file is loaded via `safe_load_config()` — a whitelist parser that only accepts predefined keys (`AWG_*`, `OS_*`, `DISABLE_IPV6`, `ALLOWED_IPS_*`, `NO_TWEAKS`, etc.). The previous `source` method has been completely replaced. The parser correctly handles values in both single and double quotes (`'value'` or `"value"`). This protects against potential code injection: even if the configuration file is modified, arbitrary commands will not execute. ### Rotating the server key You need this if the server private key leaked somewhere: into a screenshot, a forum post, or someone else's hands along with access. There is no dedicated command for it, but reinstalling the server is not required either. First, what not to worry about. **Previously recorded traffic cannot be decrypted** - the handshake's forward secrecy covers that, and session keys are not derivable from the server's static key. The real risk is different: whoever holds the key can **impersonate your server** to a client that trusts it. So rotating closes the problem not when the server changes, but when **every client has the new config**. The key lives in three places and all three have to change together: | File | What it holds | |---|---| | `/etc/amnezia/amneziawg/awg0.conf` | the `PrivateKey` line in the `[Interface]` section | | `/root/awg/server_private.key` | the same private key as a separate file | | `/root/awg/server_public.key` | the public key, used to build client configs | The order is: ```bash # 1. Backup, in case you need to roll back bash /root/awg/manage_amneziawg.sh backup # 2. A new key pair umask 077 awg genkey > /root/awg/server_private.key awg pubkey < /root/awg/server_private.key > /root/awg/server_public.key chmod 600 /root/awg/server_private.key /root/awg/server_public.key # 3. Put the private key into the server config. # The [Peer] section has no PrivateKey line, so the first one in the file # is the right one. awk with -v is safe for keys: base64 contains / and +, # which would need escaping with sed but not here. cd /etc/amnezia/amneziawg awk -v k="$(cat /root/awg/server_private.key)" \ '!d && /^[ \t]*PrivateKey[ \t]*=/ { print "PrivateKey = " k; d=1; next } { print }' \ awg0.conf > awg0.conf.new grep -q '^PrivateKey = ' awg0.conf.new && mv awg0.conf.new awg0.conf && chmod 600 awg0.conf # 4. Restart the service and reissue every client config systemctl restart awg-quick@awg0 bash /root/awg/manage_amneziawg.sh regen ``` `regen` rewrites every client config with the new public key, regenerates the QR codes and `vpn://` links, and leaves client private keys, addresses and `AllowedIPs` alone. After that, **hand out the new configs**: until you do, the old ones do not work, and they are the ones still exposed. ⚠️ **Three easy mistakes:** - **Skipping `server_private.key`.** Change the key only in `awg0.conf` and a `--force` reinstall brings the old one back: server keys are generated only when that file is missing, and the config is built from it. - **Skipping `regen`.** Existing clients keep `.conf`, `.png`, `.vpnuri` and `.vpnuri.png` files carrying the old public key. Anyone importing by QR or by link ends up with a config that cannot connect. - **Restoring an old backup later.** Archives in `/root/awg/backups/` contain both `awg0.conf` and the two key files, so restoring one silently undoes the rotation. Old archives are worth removing after a key change. To roll back: restore the three files from the backup, restart the service and **run `regen` again** - by then the client configs have already been rewritten with the new key. --- ## 🧹 Server Optimization The installer automatically optimizes the server: **Removed packages:** `snapd`, `modemmanager`, `networkd-dispatcher`, `unattended-upgrades`, `packagekit`, `lxd-agent-loader`, `udisks2`. Cloud-init is removed **only** if it does not manage network configuration. 🔴 **Removing `snapd` takes the installed snaps and their data in `/var/snap` with it.** The package is purged, and right after that the installer removes the `/snap`, `/var/snap` and `/var/lib/snapd` directories, so the snaps go either way. There is no undo: `rm -rf` does not roll back, `snapd` itself comes back with `apt install snapd`, but the snaps have to be installed again and the data under `/var/snap` only comes back from a backup. Home directories are not touched by the cleanup, so anything under `~/snap` stays where it is. Since v5.27.0 the installer asks for consent. At step 0, where the other questions live, it prints the packages actually present on the system, warns about the snap directories on a separate line and names the snaps you installed yourself. Only `snapd`, `bare` and `core*` count as base ones carrying no data of yours; `lxd` does NOT, because LXD from a snap keeps its containers in `/var/snap/lxd`. When cloud-init is also due for removal, a separate line mentions `/etc/cloud` and `/var/lib/cloud`. When there is something to lose, removal happens **only on explicit consent**: `y`, `yes` or `да` count as an answer, anything else means keep. Any failed check is read in favour of keeping as well - no terminal available, `dpkg` not answering, an unreadable snap directory. The answer is written to `awgsetup_cfg.init` so a repeated or resumed run does not ask again and does not read silence as consent: the cleanup runs only on recorded consent. There are two ways to opt out, and they are not equivalent: | Flag | What it skips | When you want it | |---|---|---| | `--keep-packages` | the system cleanup only | keep the firewall, Fail2Ban and the optimization, but leave packages alone | | `--no-tweaks` | cleanup, optimization, sysctl hardening, UFW and Fail2Ban | the server is already set up your own way | With `--yes` no question is asked and the behaviour stays as before, that is the packages are removed; the list is still printed to the log. For an unattended install that must keep the packages, add `--keep-packages`. **Hardware-aware settings:** * **Swap:** 1 GB if RAM ≤ 2 GB, 512 MB if RAM > 2 GB. `vm.swappiness = 10`. * **Network buffers:** Automatic `rmem_max`/`wmem_max` tuning based on available RAM. --- ## 📋 Configuration Examples A new install gets the 3.1 profile, so its examples come first; the 2.0 profile examples follow. Keys and addresses are masked, and the parameter values are examples.
awgsetup_cfg.init, 3.1 profile (installation parameters, shortened) ```bash # AmneziaWG installation configuration (Auto-generated) export AWG_PORT=39743 export AWG_TUNNEL_SUBNET='10.9.9.1/24' export DISABLE_IPV6=1 export ALLOWED_IPS_MODE=1 export ALLOWED_IPS='0.0.0.0/0' export AWG_ENDPOINT='' export AWG_MTU=1280 export AWG_Jc=5 export AWG_Jmin=61 export AWG_Jmax=190 export AWG_S1=88 export AWG_S2=47 export AWG_S3=29 export AWG_S4=17 export AWG_H1='1' export AWG_H2='2' export AWG_H3='3' export AWG_H4='4' export AWG_I1='' export AWG_PRESET='default' export AWG_PROTOCOL_SOURCE='default' export AWG_PROTOCOL_FALLBACK='' export AWG_CPA='32-128' export AWG_PROTOCOL='3.1' ``` Only the network and protocol lines are shown; the real file is longer (OS, client isolation, server name, install flags). The `HeaderProtectionKey` is not in the init: it lives in `awg0.conf` and in `/root/awg/server_hpk.key`.
awg0.conf, 3.1 profile (server config) ```ini [Interface] PrivateKey = [SERVER_PRIVATE_KEY] Address = 10.9.9.1/24 MTU = 1280 ListenPort = 39743 PostUp = iptables -I FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE Jc = 5 Jmin = 61 Jmax = 190 S1 = 88 S2 = 47 S3 = 29 S4 = 17 H1 = 1 H2 = 2 H3 = 3 H4 = 4 I1 = HeaderProtectionKey = [HEADER_PROTECTION_KEY] ContentPaddingAddition = 32-128 [Peer] #_Name = my_phone PublicKey = [CLIENT_PUBLIC_KEY] AllowedIPs = 10.9.9.2/32 ``` The `PostUp` and `PostDown` lines are shortened: the real config also carries the MSS clamp and client isolation rules.
client.conf, 3.1 profile (client config) ```ini [Interface] PrivateKey = [CLIENT_PRIVATE_KEY] Address = 10.9.9.2/32 DNS = 1.1.1.1, 1.0.0.1 MTU = 1280 Jc = 5 Jmin = 61 Jmax = 190 S1 = 88 S2 = 47 S3 = 29 S4 = 17 H1 = 1 H2 = 2 H3 = 3 H4 = 4 I1 = HeaderProtectionKey = [HEADER_PROTECTION_KEY] ContentPaddingAddition = 32-128 [Peer] PublicKey = [SERVER_PUBLIC_KEY] Endpoint = 203.0.113.1:39743 AllowedIPs = 0.0.0.0/0, ::/0 PersistentKeepalive = 33 ``` The client's `HeaderProtectionKey` is the same as the server's: there is one key per install.
awgsetup_cfg.init, 2.0 profile (installation parameters) ```bash # AmneziaWG installation configuration (Auto-generated) export AWG_PORT=39743 export AWG_TUNNEL_SUBNET='10.9.9.1/24' export DISABLE_IPV6=1 export ALLOWED_IPS_MODE=1 export ALLOWED_IPS='0.0.0.0/0' export AWG_ENDPOINT='' export AWG_Jc=6 export AWG_Jmin=55 export AWG_Jmax=205 export AWG_S1=72 export AWG_S2=56 export AWG_S3=32 export AWG_S4=16 export AWG_H1='234567-345678' export AWG_H2='3456789-4567890' export AWG_H3='56789012-67890123' export AWG_H4='456789012-567890123' export AWG_I1='' export AWG_PRESET='default' export AWG_PROTOCOL='2.0' ```
awg0.conf, 2.0 profile (server config, keys masked) ```ini [Interface] PrivateKey = [SERVER_PRIVATE_KEY] Address = 10.9.9.1/24 MTU = 1280 ListenPort = 39743 PostUp = iptables -I FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE Jc = 6 Jmin = 55 Jmax = 205 S1 = 72 S2 = 56 S3 = 32 S4 = 16 H1 = 234567-345678 H2 = 3456789-4567890 H3 = 56789012-67890123 H4 = 456789012-567890123 I1 = [Peer] #_Name = my_phone PublicKey = [CLIENT_PUBLIC_KEY] AllowedIPs = 10.9.9.2/32 ``` The `PostUp` and `PostDown` lines are shortened here: a real config also carries the MSS clamp and client isolation rules.
Minimal awg0.conf for AWG 2.0 (for manual setup) If you are setting up the server without my installer (for example, `amneziawg-go` in LXC), the minimum valid `awg0.conf` for AWG 2.0 looks like this — all 11 obfuscation parameters are required; `manage_amneziawg.sh add/regen` will abort if any one of them is missing: ```ini [Interface] PrivateKey = [SERVER_PRIVATE_KEY] Address = 10.9.9.1/24 ListenPort = 51820 Jc = 4 Jmin = 40 Jmax = 90 S1 = 50 S2 = 40 S3 = 12 S4 = 8 H1 = 1234567 H2 = 2345678 H3 = 3456789 H4 = 4567890 ``` Notes for manual setups: - **S3/S4** are AWG 2.0 parameters added to the protocol later than S1/S2. Configs from the earlier AWG 1.x release may not have them - add by hand, `S3` takes `0-64` and `S4` takes `0-32`, the key point is that the keys exist at all. - **H1–H4** can be single-value (`H1 = 1234567`) or a range (`H1 = 100000-200000`); ranges must not overlap. Keep the upper bound at `2147483647` (`INT32_MAX`) or below, otherwise `amneziawg-windows-client` may flag the value as invalid. - **I1-I5** (CPS / special-junk packets) are optional. Without `I1` (and with `I2`-`I5` unset as well) no concealment packets are sent, and the rest of the obfuscation (`Jc`, `S`, `H`) works as usual; to have one, add `I1 = ` (random 128 bytes) or `I1 = ` (binary). ⚠️ The `` example shows the FORMAT, not a recommended value: by the September 2026 measurement a packet of random bytes is dropped on some cellular networks and the handshake never completes. If the tunnel does not come up on cellular, see [the handshake never completes on cellular](#no-hs-mobile-adv). Since v5.18.0 all five (`I1`-`I5`) are carried into client configs, not just `I1`: set `I2`-`I5` in the `[Interface]` section of `awg0.conf`, restart the service (`sudo systemctl restart awg-quick@awg0`), and distribute to clients with `sudo bash /root/awg/manage_amneziawg.sh regen ` - the values flow into the `.conf`, QR, and `vpn://`. Ready-made sets come from, e.g., the VoidWaifu list; the portable tag formats are ``, ``, ``, ``, `` (the `` tag exists only in the kernel module, and on amneziawg-go a config with it does not come up). These values do not have to match the server: only the handshake initiator sends them, and the receiver discards them. Case does matter - uppercase only. Unset `I2`-`I5` are simply not emitted. - **MTU**, **PostUp/PostDown** are optional and depend on the setup (see the `amneziawg-go` LXC section on `iptables` MASQUERADE). After creating such an `awg0.conf`, `manage_amneziawg.sh` also needs a minimal `/root/awg/awgsetup_cfg.init` containing at least `AWG_PORT`, `AWG_TUNNEL_SUBNET`, `AWG_ENDPOINT`. If `/root/awg/server_public.key` is missing, `manage` derives it from the `PrivateKey` in `awg0.conf`. **A 3.1 profile by hand** needs more: in `awg0.conf` the lines `HeaderProtectionKey = ` and `ContentPaddingAddition = 32-128`, `S1`-`S4` at 12 or more, in the init `export AWG_PROTOCOL='3.1'` and `export AWG_CPA='32-128'`, and a copy of the key as one line in `/root/awg/server_hpk.key` (mode 600). Without the `AWG_PROTOCOL='3.1'` marker the server reads as 2.0, and `manage` refuses to issue profiles once it sees a key in the config; if the key in the config and in the file differ, it refuses too.
client.conf, 2.0 profile (client config, keys masked) ```ini [Interface] PrivateKey = [CLIENT_PRIVATE_KEY] Address = 10.9.9.2/32 DNS = 1.1.1.1, 1.0.0.1 MTU = 1280 Jc = 6 Jmin = 55 Jmax = 205 S1 = 72 S2 = 56 S3 = 32 S4 = 16 H1 = 234567-345678 H2 = 3456789-4567890 H3 = 56789012-67890123 H4 = 456789012-567890123 I1 = [Peer] PublicKey = [SERVER_PUBLIC_KEY] Endpoint = 203.0.113.1:39743 AllowedIPs = 0.0.0.0/0, ::/0 PersistentKeepalive = 33 ```
---
## 🖥️ CLI Parameters ### install_amneziawg.sh ``` Options: -h, --help Show help --uninstall Uninstall AmneziaWG --diagnostic Generate diagnostic report --repair-module Repair the kernel module on an installed server (kernel 7.0.0-38 and later, packages left unconfigured by apt); combines only with --verbose and --no-color -v, --verbose Verbose output (including DEBUG) --no-color Disable colored output --port=PORT Set UDP port (1-65535; on mobile networks with DPI, 443/udp often helps) --ssh-port=PORT SSH port for the UFW rule (auto-detected; comma-separated list) --subnet=SUBNET Tunnel subnet, CIDR /16-/30 (e.g. 10.9.0.0/16) --allow-ipv6 Keep IPv6 enabled --disallow-ipv6 Force-disable IPv6 --allow-ipv6-tunnel Enable dual-stack IPv6 inside the tunnel (ULA, opt-in) --route-all Mode: All traffic (0.0.0.0/0) (default) --route-amnezia Mode: Amnezia List + DNS (private networks stay outside) --client-ipv6-direct With --route-amnezia: the devices' IPv6 directly, around the VPN --route-custom=NETS Mode: Only specified networks --isolation=on|off Isolate clients from each other (default on) --endpoint=ADDR External server endpoint: FQDN, IPv4 or [IPv6] (NAT) --server-name=NAME Server name shown in the Amnezia app on vpn:// import (default 'AWG Server') --mobile Mobile setup in one flag: preset mobile + port 443/udp (an explicit --port wins) --protocol=2.0|3.1 Protocol generation for a new install or for one that started but has not reached step 6 (default 3.1). Without the flag, where the installer does not set up 3.1 (kernel older than 6.7, a non-x86_64 architecture, ARM included, a second-line kernel module or a failed module check), 2.0 is installed with an explanation; if the module does not accept the 2.0 set either, the install stops. If the awg tools are older than a third-line module, the install stops and asks to upgrade them. --protocol=2.0 is needed if the clients are routers on stock firmware, Hiddify or apps that are not updated. An explicit --protocol=3.1 on an unsuitable machine ends the install and names the reason. On an already configured server a flag naming a DIFFERENT generation ends the install: the generation of a running install changes only by reinstalling and reissuing every client profile. The spaced form is accepted too: --protocol 2.0 --preset=TYPE Obfuscation parameter preset: default, mobile mobile: Jc=3, narrow Jmax — for mobile carriers (Tele2, Yota, Megafon) --jc=N Set Jc manually (0-122, overrides preset; 0 turns junk packets off) --jmin=N Set Jmin manually (0-1280, overrides preset) --jmax=N Set Jmax manually (0-1280, overrides preset, must be >= Jmin) --no-cps Disable CPS (the I1 parameter) - for desktop clients that do not support it (e.g. macOS) --i1='' Set I1 to your own value: tags , , , , (N from 1 to 1000, the whole packet at most 1024 bytes). The value is pinned, a reinstall keeps it; --no-cps, --preset, --mobile, --jc, --jmin and --jmax drop it --no-prebuilt On ARM, do not install the prebuilt module package from the arm-packages release; build the module with DKMS -y, --yes Non-interactive mode (all confirmations auto-yes) -f, --force Reinstall over a working AWG (ENV: AWG_FORCE_REINSTALL=1) --no-tweaks Skip the system cleanup, the optimization and the hardening (UFW, Fail2Ban); the minimal forwarding sysctl is always applied --keep-packages Do not remove system packages (snapd and others), but keep the firewall, Fail2Ban and the optimization. Removing snapd takes installed snaps and their data in /var/snap with it ``` ### manage_amneziawg.sh ``` Options: -h, --help Show help -v, --verbose Verbose output (for list) --no-color Disable colored output --conf-dir=PATH Specify AWG directory (default: /root/awg) --server-conf=PATH Specify server config file --json Machine-readable JSON output (v5.21.0: most commands, see the "JSON interface" section) --expires=DURATION Expiry duration for add (1h, 12h, 1d, 7d, 30d, 4w) --apply-mode=MODE syncconf (default) or restart (bypass kernel panic) --psk (add only) generate a PresharedKey for the new client (v5.11.1+) --allowed-ips=LIST (add only) per-client AllowedIPs - comma-separated CIDRs; without the flag the server-wide routing mode is used --yes Do not prompt for confirmation (ENV: AWG_YES=1) --carrier=NAME (diagnose only) compare parameters against a carrier profile --reset-routes (regen only) reset client AllowedIPs to the global routing mode ``` > **`--psk`** - optional extra layer on top of the AmneziaWG obfuscation. Generates a 32-byte symmetric key via `awg genpsk` and writes it to both the server `[Peer]` and the client `[Peer]` (`PresharedKey = ...`). Compatible with any WireGuard/AmneziaWG client. In batch mode (`add c1 c2 c3 --psk`) each client gets its own PSK. Without the flag clients are created without `PresharedKey` (by default - the AmneziaWG obfuscation is enough for most scenarios). The flag only affects the new clients created by this `add` invocation - existing clients without PSK stay untouched and keep connecting as before. > **`--allowed-ips=LIST`** - the client's own routes right at creation, without a follow-up `modify` (Issue #253). A comma-separated list of IPv4/IPv6 CIDRs: `add phone --allowed-ips="10.0.0.0/8, 192.168.0.0/16"`. Validated and normalized before the first client is created (an invalid or EMPTY value aborts the command creating nothing) and applied to every name in the batch - like `--expires`. The value goes through the same rules as the global mode: a full-tunnel IPv4 list gets an IPv6 route next to it (the iOS rule): `::/0` with `0.0.0.0/0`, `2000::/3` with a service address for a list, and on dual-stack `::/0` with native server IPv6 or the tunnel ULA without it; a split list never gets `::/0`, and a list with explicit IPv6 tokens (e.g. `0.0.0.0/0, ::/0`) is written as is - and when a full tunnel spells out IPv6 without `::/0`, `add` warns right away, the way `regen` does. Without the flag - the server-wide routing mode, as before. For existing clients `modify` still changes routes, and `regen` preserves such per-client lists. > > **The list replaces the global one entirely.** Including the tunnel subnet the installer appends to `AllowedIPs` with `--isolation=off`: a client created with `--allowed-ips="10.0.0.0/8"` will not see the other peers. Need in-VPN device visibility - add the tunnel subnet (default `10.9.9.0/24`) to the list by hand. **Environment variables:** | Variable | Description | |----------|-------------| | `AWG_SKIP_APPLY=1` | Skip apply_config. For automation: accumulate N operations, apply once | | `AWG_APPLY_MODE=restart` | Full restart instead of syncconf (can be saved in `awgsetup_cfg.init`) | | `AWG_YES=1` | Do not prompt for confirmation (equivalent to the `--yes` flag) | | `AWG_STRICT_CONFIRM=1` | A non-interactive run of a destructive command without `--yes`/`AWG_YES=1` is refused (rc 1) instead of silently proceeding. For cron/CI/bots (v5.21.0) | --- ## 🧑‍💻 Full List of Management Commands Usage: `sudo bash /root/awg/manage_amneziawg.sh `: > **How `manage` finds clients in the server config.** Every `[Peer]` created by my installer or by `manage add` has a marker comment `#_Name = ` on the first line of the block. That marker is what `list`, `remove`, `regen`, `modify` look up. If you are migrating `awg0.conf` from an older server or adding a peer by hand, include `#_Name = ` right after `[Peer]` — otherwise `manage` will not see the client. Example: the `[Peer]` block in the server config above (see [Configuration Examples](#config-examples-adv)). * **`add [name2 ...] [--expires=DURATION] [--psk] [--allowed-ips=LIST]`:** Add one or multiple clients. In batch mode, `awg syncconf` is called once for all. With `--expires` - expiry applies to all clients. With `--psk` - each client gets its own PresharedKey (v5.11.1+). With `--allowed-ips` - each gets per-client routes instead of the global mode (Issue #253). * **`remove [name2 ...]`:** Remove one or multiple clients. In batch mode, apply_config is called once for all. * **`list [-v] [--json]`:** List clients (with details when using `-v`; `--json` - machine-readable, includes the `client_ipv6` and `expires_at` fields). * **`regen [name ...] [--reset-routes]`:** Regenerate `.conf`/`.png` files for the listed clients or all at once. By default preserves the client's individual `AllowedIPs`/`DNS`/`PersistentKeepalive` (set via `modify`). With `--reset-routes` - resets `AllowedIPs` to the current global routing mode from `awgsetup_cfg.init`; use it after changing the mode via reinstall (`--force --route-all` / `--route-amnezia` / `--route-custom=`) so the new mode reaches existing clients (Issue #170). * **`modify `:** Modify a client parameter in the `.conf` file. Allowed parameters: DNS, Endpoint, AllowedIPs, PersistentKeepalive. QR code and vpn:// URI are automatically regenerated after modification; the previous copies are removed before the edit, so a file that could not be rebuilt is missing rather than stale. * **`backup`:** Create a backup (configs + keys + client expiry data + cron). * **`restore [file]`:** Restore from a backup (including expiry data and cron job). * **`check` / `status`:** Check server status (service, port, module, the installation's protocol generation 3.1 or 2.0, and the obfuscation parameters). * **`show`:** Run `awg show`. * **`restart`:** Restart the AmneziaWG service. * **`diagnose [--carrier=NAME]`:** Self-troubleshooting: checks the kernel module, sysctl and UFW; with `--carrier=NAME` it compares AWG parameters against a mobile carrier profile. Names: `beeline_msk`, `yota_msk`, `tele2_msk`, `tele2_krasnoyarsk`, `tattelecom`, `megafon_regions`, `tmobile_us` (the last name is historical: it is the Russian T-Mobile, Moscow and region). * **`repair-module`** (alias `repair`): Rebuild/restore the amneziawg kernel module (DKMS) after a server kernel upgrade; for kernels 7.0.0-38 and later see [Known Limitations](#kernel-70-backport-adv). * **`help`:** Show help. * **`stats [--json]`:** Per-client traffic statistics. With `--json` — machine-readable format for integration. ### Usage Examples ```bash # Change client DNS sudo bash /root/awg/manage_amneziawg.sh modify my_phone DNS "8.8.8.8,1.0.0.1" # Change PersistentKeepalive sudo bash /root/awg/manage_amneziawg.sh modify my_phone PersistentKeepalive 25 # Change AllowedIPs (split-tunneling) sudo bash /root/awg/manage_amneziawg.sh modify my_phone AllowedIPs "192.168.1.0/24,10.0.0.0/8" # Regenerate config for a single client sudo bash /root/awg/manage_amneziawg.sh regen my_phone # Create a backup sudo bash /root/awg/manage_amneziawg.sh backup # Restore from the latest backup (interactive selection) sudo bash /root/awg/manage_amneziawg.sh restore ``` --- ## 🤖 JSON interface for automation (--json) Since v5.21.0 the `--json` flag is supported not only by `list`/`stats` but also by the management commands: `add`, `remove`, `regen`, `modify`, `backup`, `restore`, `check` (alias `status`), `restart`, `repair-module` (alias `repair`). Contract rules: - **stdout = exactly one JSON document** on a single line, on every outcome including errors. Human messages go to stderr. Read stdout as a whole - there is no streaming, but for batch commands `results[]` grows linearly with the number of names. - **The exit code is the source of truth.** The `ok` field mirrors it for bots that only read stdout: `ok=false` on any failure, including a partial one (`add a b` where `b` already exists). - **Compatibility is additive:** new fields may appear; existing ones are never renamed and never change type. The `status` value sets may grow - treat an unknown value as a failure of that entry. `list`/`stats` are frozen as-is in terms of the SHAPE of the reply - bare arrays, no envelope; the freeze does not cover the key set, which grows under the rule above. - **The `error` field is human-readable text** (may be localized); do not parse it - make machine decisions from `ok`, `rc` and `status`. - **Aliases are canonicalized:** the response always says `"command":"check"` and `"command":"repair-module"`, however you typed the command. - `--json` does **not** imply `--yes`: destructive commands still ask for confirmation. - `help`, `show`, `diagnose` do not support the flag - their output stays human. Success form (an `add` example): ```json {"command":"add","ok":true,"added":1,"failed":0,"applied":true,"results":[{"name":"phone","status":"created","conf":"/root/awg/phone.conf","qr":"/root/awg/phone.png","vpnuri":"/root/awg/phone.vpnuri","expires_at":null,"allowed_ips":"1.0.0.0/8, 2.0.0.0/7, 4.0.0.0/6, 8.0.0.0/7, 11.0.0.0/8, 12.0.0.0/6, 16.0.0.0/4, 32.0.0.0/3, 64.0.0.0/2, 128.0.0.0/3, 160.0.0.0/5, 168.0.0.0/6, 172.0.0.0/12, 172.32.0.0/11, 172.64.0.0/10, 172.128.0.0/9, 173.0.0.0/8, 174.0.0.0/7, 176.0.0.0/4, 192.0.0.0/9, 192.128.0.0/11, 192.160.0.0/13, 192.169.0.0/16, 192.170.0.0/15, 192.172.0.0/14, 192.176.0.0/12, 192.192.0.0/10, 193.0.0.0/8, 194.0.0.0/7, 196.0.0.0/6, 200.0.0.0/5, 208.0.0.0/4, 8.8.8.8/32, 1.1.1.1/32, 2000::/3"}]} ``` The form of any emergency exit (die, bad option, confirmation refusal, signal): ```json {"command":"remove","ok":false,"error":"confirmation denied","rc":1} ``` `results[]` entry statuses: `add` - `created|exists|invalid_name|error`; `remove` - `removed|not_found|invalid_name|error`; `regen` - `regenerated|not_found|invalid_name|error`. Field notes: - `applied` - whether the config was applied to the live interface. `regen` and `modify` have no such field: they do not change server state (keys and IPs are reused). Always `false` under `AWG_SKIP_APPLY=1`. - `qr`/`vpnuri` - a path if the file existed at response time. For `add` and `regen`, QR and URI are generated outside the config lock: a parallel operation can remove the file, no freshness guarantee. For `modify` the previous copies are removed before the edit and the rebuild runs under the same lock: a file that could not be built is missing and the field is `null`. Without a parallel operation on the same client, a path leads to a copy built from the new `.conf`. - `results[].allowed_ips` (`add`, entries with status `created`) - the client's applied routes, read from the just-created `.conf`. May differ from the `--allowed-ips` argument: a full-tunnel IPv4 list gets an IPv6 route added (`::/0`, `2000::/3` or the tunnel ULA, the iOS rule). `null` means the value could not be read - the reason goes to the log as an error, like `qr`/`vpnuri`. For an external consumer (a bot) the field removes the verification call after creation (Issue #253). - `restore` returns an envelope on failure too (with `error` and `rolled_back` - the bot needs to know whether a rollback happened). With `rolled_back: true` it also carries `rollback_complete`: `false` means the rollback could not return some files, the reasons and the snapshot path are on stderr. `restored.clients` is the number of `[Peer]` blocks in the restored server config, not files in the working directory. - `repair-module.rc` - the internal module-check code (0 - module and service OK, 1 - module failed, 2 - module OK, service down), not the process exit code. - `check.module.loaded=false` is not an error by itself: userspace installs (amneziawg-go, LXC) never have the module. - `check.protocol` is the protocol generation of the installation as a string (`"2.0"` or `"3.1"`), read from the `AWG_PROTOCOL` marker in `awgsetup_cfg.init` and not from the module version: a third-line module runs a second-line configuration as a matter of course. An installation without the marker is `"2.0"`. `check.protocol_error` is normally `null`; the value `"unreadable"` means the marker is broken (a value outside the set, an empty field, two marker lines, a file that cannot be read): `protocol` is then `null`, `ok=false` and the exit code is 1. Both fields are present in every answer of the check itself. If `awgsetup_cfg.init` is missing or is not a regular file, `check` stops before the check with the generic error envelope (`command`, `ok`, `error`, `rc`), which does not carry them. - `list.expires_at` is unix time as a number for a client with an expiry, or `null` for a permanent one; a canonical decimal of at most 15 digits. `list.expires_at_error` is `null` in the normal case, and `"unreadable"` means the marker exists on the server but no value could be obtained from it (empty file, corrupted content, a directory in its place, a failed read). Both fields are present in every record; see [Temporary clients](#expires-adv) for details. - `check.port.number` is always an integer, and `0` reads as "port unknown". A missing or empty setting is a warning and leaves `ok` alone. Any non-empty value that is not a port in 1-65535 counts as a corrupt config: `ok=false` and exit code 1. The value is normalised, so `0080` in the config comes back as `80`, and surrounding whitespace is ignored. This field alone says nothing about the live port - read it together with `port.listening`. ### Strict confirmation for pipelines (AWG_STRICT_CONFIRM) By default a non-interactive run (cron, CI, a bot) of a destructive command silently proceeds as if you answered "yes" - that is the historical behavior and it is preserved. `AWG_STRICT_CONFIRM=1` enables strict mode: without an explicit `--yes` (or `AWG_YES=1`) the command refuses with rc 1, and with `--json` returns `{"ok":false,"error":"AWG_STRICT_CONFIRM=1: non-interactive run requires --yes","rc":1}`. ```bash # Bot recipe: strict mode + explicit consent AWG_STRICT_CONFIRM=1 bash manage_amneziawg.sh remove phone --json --yes ``` Applies per run, never persisted to the config. Strictly the value `1` activates it. --- ## 🛠️ Technical Details ### Script Architecture | File | Purpose | |------|---------| | `install_amneziawg.sh` | Installer: 8-step state machine with resume support | | `manage_amneziawg.sh` | Management: add/remove/list/regen/stats/backup/restore | | `awg_common.sh` | Shared library: keys, configs, QR, peer management | | `install_amneziawg_en.sh` | Installer (English version) | | `manage_amneziawg_en.sh` | Management (English version) | | `awg_common_en.sh` | Shared library (English version) | `awg_common.sh` is loaded via `source` from both scripts. The installer downloads it at step 5. ```mermaid graph TD A[install_amneziawg.sh] -->|"source (step 6)"| C[awg_common.sh] B[manage_amneziawg.sh] -->|"source"| C A -->|"wget (step 5)"| B A -->|"wget (step 5)"| C subgraph "State Machine (install)" S0[Step 0: Init + generation choice] --> S1[Step 1: Update] S1 -->|reboot| S2[Step 2: DKMS] S2 -->|reboot| S3[Step 3: Module + 3.1 probe] S3 --> S4[Step 4: UFW] S4 --> S5[Step 5: Scripts] S5 --> S6[Step 6: Configs] S6 --> S7[Step 7: Service] S7 --> S99[Step 99: Done] end subgraph "Commands (manage)" M1[add / remove] M2[list / show / check] M3[modify / regen] M4[backup / restore] M5[restart] M6[stats / stats --json] end ``` ### DKMS Automatic rebuilding of the `amneziawg` kernel module on kernel updates. Check: `dkms status`. ### Key and Config Generation **Fully native** generation: * **Keys:** `awg genkey` + `awg pubkey` (standard AmneziaWG utilities). * **Configs:** Bash templates with the obfuscation parameters; the 3.1 profile adds `HeaderProtectionKey` and `ContentPaddingAddition`. * **QR codes:** `qrencode -t png`. * **Python/awgcfg.py:** Completely removed. The config deletion bug workaround is no longer needed. Client keys are stored in `/root/awg/keys/` (permissions 600). Server keys are in `/root/awg/server_private.key` and `server_public.key`. #### Version-Pinned URLs (v5.7.2) The installer downloads `awg_common_en.sh` and `manage_amneziawg_en.sh` (the Russian installer: `awg_common.sh` and `manage_amneziawg.sh`) from URLs pinned to the specific version tag: ``` https://raw.githubusercontent.com/bivlked/amneziawg-installer/v6.1.1/awg_common_en.sh ``` This provides **supply chain pinning**: downloaded scripts match the installer version, even if `main` has already been updated. For development, you can override the branch: ```bash sudo AWG_BRANCH=my-feature-branch bash ./install_amneziawg_en.sh ``` Put the variable after `sudo`, otherwise sudo resets the environment. With a branch other than the version tag, the SHA256 check of the downloaded scripts is skipped (the log shows a warning), so this is for development only. --- ## 🔄 How to Update Scripts To update the management and shared library scripts **without reinstalling the server**, download them into a temporary directory, verify the signatures, and only then replace the files (this needs `minisign`: `sudo apt install minisign`): ```bash cd "$(mktemp -d)" KEY=RWQXfpABHIpZPttqrwYrQNHRTk/iLIz4cVh9KkRwAElHP+CoW/NPEysN M=manage_amneziawg_en.sh C=awg_common_en.sh # Russian version: M=manage_amneziawg.sh C=awg_common.sh ok=1 tag= for f in "$M" "$C"; do wget -q -O "$f" "https://github.com/bivlked/amneziawg-installer/releases/latest/download/$f" \ && wget -q -O "$f.minisig" "https://github.com/bivlked/amneziawg-installer/releases/latest/download/$f.minisig" \ && out=$(minisign -V -P "$KEY" -m "$f" -x "$f.minisig") \ && t=$(printf '%s\n' "$out" | sed -n "s/^Trusted comment: amneziawg-installer \(v[0-9.]*\) $f\$/\1/p") \ && [ -n "$t" ] && [ "${tag:-$t}" = "$t" ] || { echo "NOT VERIFIED: $f"; ok=0; break; } tag=$t done [ "$ok" = 1 ] && { old=$(sudo sed -n 's/^SCRIPT_VERSION="\(.*\)"/v\1/p' /root/awg/manage_amneziawg.sh) && [ -n "$old" ] \ || { echo "NOT VERIFIED: cannot read the installed version from /root/awg/manage_amneziawg.sh"; ok=0; }; } [ "$ok" = 1 ] && [ "$(printf '%s\n' "$old" "$tag" | sort -V | tail -1)" != "$tag" ] \ && { echo "NOT VERIFIED: release $tag is older than the installed $old"; ok=0; } [ "$ok" = 1 ] && { sudo install -m 700 "$M" /root/awg/manage_amneziawg.sh \ && sudo install -m 700 "$C" /root/awg/awg_common.sh && echo "Installed verified release $tag" || echo "NOT FULLY INSTALLED: run the block again"; } ``` If the script printed "NOT VERIFIED", the working files are untouched; "NOT FULLY INSTALLED" means the files were not both replaced, so run the block again. If the installed `manage` predates v5.7.0, it has no version line and the block will refuse: update such a server with the new installer and `--force`. The block only accepts a pair of files from one release that is not older than the installed one, so a tampered "latest release" pointer cannot roll the scripts back to older signed versions. It prints the verified release number: compare it with the [releases page](https://github.com/bivlked/amneziawg-installer/releases). A plain `wget -O` over the working file would leave an empty file if the download failed. How the signature works: [Verifying a release](README.en.md#verifying-a-release). > **Note:** Reinstalling `install_amneziawg.sh` is **not required** for management updates. The protocol generation (3.1 or 2.0) cannot be changed by a script update or `--force`: it takes `--uninstall`, a new install and new profiles for every client, see [Where 2.0 is installed instead of 3.1](#awg3-fallback-adv). --- ## ❓ FAQ (Additional Questions)
Q: Why is my config highlighted yellow in the Amnezia app? A: The yellow mark reports the profile generation, not an error: that is how the Amnezia app marks second-line AmneziaWG configurations. On its own the mark does not say anything is wrong with the connection, and nothing needs redoing because of it. Since version 6.0.0 a new install gets the 3.1 profile by default. A 2.0 profile - on servers installed earlier, installed with --protocol=2.0, or where the installer picked 2.0 by itself (a stock Debian 12 with kernel 6.1, ARM) - gets the mark as expected. Removing it takes a 3.1 server, that is, a reinstall with every profile reissued, and all your clients have to understand 3.1.
Q: How do I get a client on Android that speaks the third line? A: Update the app: the light AmneziaWG on Google Play speaks the third line since version 3.1.4 (21 September 2026), the APK from the project's releases page since v3.1.20260814, the flagship Amnezia VPN since 5.0.1.5, WG Tunnel since 5.6.0. The full table is in Which AmneziaWG generation a client speaks.
Q: Does my router speak third-line AmneziaWG? A: Stable stock firmware has no third line as of 15 October 2026: Keenetic added it in KeeneticOS 5.2 Alpha 11 (development channel), GL.iNet ships only the second line (since firmware 4.9), and the official OpenWrt packages have no AmneziaWG at all. So for a router whose firmware speaks only the second line (stable KeeneticOS 5.1, for example) install the server with --protocol=2.0: the second-line profile is a working path for it rather than a temporary compromise. On routers the third line comes from third-party projects: AWG Manager for Keenetic (v2.19.16 of 14 October 2026; with the stock module on KeeneticOS 5.2 Alpha 11 and later, on other firmware with its own kernel module or its bundled sing-box), awg-openwrt for OpenWrt (only the packages for OpenWrt 25.12.5 and 24.10.8, replaced on 22 September 2026 under the same tags, give the third line; ones downloaded earlier and packages for older OpenWrt do not), and for MikroTik and RouterOS amneziawg-mikrotik-c (v1.4.3 of 8 October 2026) and AmneziaWG-MikroTik. The table is in Which AmneziaWG generation a client speaks.
Q: Can a third-line config be converted back to the second line? A: A third-line client config cannot be edited down to the second line: the generation is set on the server side, and some third-line parameters are two-sided. In a measurement on 30 August 2026 a header protection key left on one side only produced zero handshakes and 100% loss, silently, with no error message anywhere. Devices that stay on the second line need a second-generation server: if you have such devices, install the server with --protocol=2.0.
Q: How do I get a split exit - Russian traffic direct, the rest abroad? A: This is built as a two-server cascade: the client connects to an entry server (ideally in Russia), Russian traffic exits directly from it, and everything else goes through a second server abroad. The cascade is not part of the installer (different scale), but there is a separate step-by-step guide - CASCADE.en.md.
Q: Russian sites refuse to open over the VPN because they block foreign data-center addresses. What can I do with a single server? A: Send only the Russian segment through a second exit via Cloudflare WARP and leave the rest direct. The server receives the list of Russian networks automatically over BGP, so there is nothing to maintain by hand. Russian sites will see a Cloudflare address instead of a data-center one. Step-by-step guide - WARP-RU.en.md. If you need a genuinely Russian IP rather than a Cloudflare one, that requires a second server in Russia and a cascade (CASCADE.en.md).
Q: AmneziaVPN says "this server does not support split tunneling". How do I enable it? A: This is a limitation of the client, not the server. The AmneziaVPN app's built-in split tunneling by sites and apps only turns on when the config sends all traffic through the tunnel. The client looks at AllowedIPs: a full tunnel unlocks the feature, while a partial subnet list is treated as already split at the routing level, so the client hides its toggle with that message. The full-tunnel form it reliably recognizes is the pair 0.0.0.0/0, ::/0, and that is exactly what a default install hands out. The message shows up when the client was handed a subnet list instead of 0.0.0.0/0, ::/0: that is what the "Amnezia" mode (--route-amnezia), your own network list (--route-custom=) and an individual AllowedIPs set through modify all produce. Fix, no docker needed: switch the client to a full tunnel - replace the line in its .conf with AllowedIPs = 0.0.0.0/0, ::/0 and re-import, or re-issue the client in "All traffic" mode (--route-all). The split tunneling page in the app then opens and you pick sites/apps there. If you only need part of the traffic in the tunnel (a network-level split), AllowedIPs already does that - the app feature is not required for it.
Q: The desktop AmneziaVPN on macOS hangs on connect. What can I do? A: According to issue #159 (July 2026), the desktop AmneziaVPN app on macOS could not handle the I1 concealment packet (CPS) and hung on connect; this has not been rechecked on current versions of the app. Mobile (iOS/Android) and CLI clients handle CPS and connect fine. Install with the --no-cps flag: the installer drops I1 from the server config and all clients, and the desktop connects. Only the CPS layer is lost, the rest of the obfuscation (Jc/S1-S4/H1-H4, and on a 3.1 server the header encryption) stays. On an already-installed server it is the same via reinstall (which does not change the protocol generation): sudo bash install_amneziawg_en.sh --force --no-cps, then reissue existing clients sudo bash /root/awg/manage_amneziawg.sh regen (without this the client config still carries I1, and that is exactly what the desktop app chokes on). To re-enable CPS later, reinstall with any set-regeneration flag, e.g. --preset=default - note that this regenerates the WHOLE obfuscation set (H1-H4/S1-S4 too), so after re-enabling you need a regen of all clients again. The flag drops only I1: if you added I2-I5 manually, they stay in the configs. Issue #159.
Q: How do I change the AmneziaWG port after installation? A: 1. Change ListenPort in /etc/amnezia/amneziawg/awg0.conf. 2. Change AWG_PORT in /root/awg/awgsetup_cfg.init. 3. Update UFW (sudo ufw delete allow <old_port>/udp, sudo ufw allow <new_port>/udp). 4. Restart the service (sudo systemctl restart awg-quick@awg0). 5. Regenerate ALL client configs (sudo bash /root/awg/manage_amneziawg.sh regen) and distribute them.
Q: How do I change the internal VPN subnet? A: The easiest way is to uninstall (sudo bash ./install_amneziawg_en.sh --uninstall) and reinstall, specifying the new subnet during initial setup. A reinstall over a live server (--force) with a different subnet aborts while clients exist in the config - their addresses were issued in the old subnet.
Q: How do I change the MTU? A: Starting with v5.7.4, MTU = 1280 is set automatically. To change it, stop the tunnel (sudo systemctl stop awg-quick@awg0) and edit the MTU = <value> line in the [Interface] section of /etc/amnezia/amneziawg/awg0.conf and in client .conf files. In the same awg0.conf, change the --set-mss values in the PostUp and PostDown lines: 40 less than the MTU for the iptables rules, 60 less for the ip6tables rules (if PostUp has them). For MTU 1200 that is 1160 and 1140. Otherwise large packets stop fitting into the tunnel and some sites open only now and then. Then start the tunnel: sudo systemctl start awg-quick@awg0. Stopping it before the edit lets the old MSS rules be removed with their old values. See MTU and Mobile Clients for details.
Q: Where are the obfuscation parameters stored? A: After the install, in the [Interface] section of the server config /etc/amnezia/amneziawg/awg0.conf. That file is the source of truth for manage: add and regen read the values for client configs from there. A reinstall (--force) takes them from there too, unless --preset/--mobile/--jc/--jmin/--jmax regenerate the set. A copy of the parameters (AWG_Jc, AWG_S1..S4, AWG_H1..H4, AWG_I1..I5) also lives in /root/awg/awgsetup_cfg.init, but it is used only when awg0.conf is absent, which normally means the first install: an edit made only there reaches neither the server nor the clients. manage warns when the init was changed after awg0.conf and their values disagree. The steps for a manual edit are in the next question. On a 3.1 server [Interface] also holds HeaderProtectionKey and ContentPaddingAddition, and the key has an extra copy in /root/awg/server_hpk.key.
Q: Can I change the obfuscation parameters after installation? A: Yes. This is useful if your ISP started recognizing your server by its obfuscation parameters. Workflow:
  1. Edit parameters (Jc, S1-S4, H1-H4, I1-I5) in the [Interface] section of /etc/amnezia/amneziawg/awg0.conf; write the key names exactly as they are in the file.
  2. Restart the service: sudo systemctl restart awg-quick@awg0.
  3. Regenerate every client config: sudo bash /root/awg/manage_amneziawg.sh regen. regen reads live values directly from awg0.conf.
  4. Distribute the new .conf / QR codes / vpn:// URIs to clients.
A 3.1 server (the default). H1-H4 are set to 1-4 on purpose, and changing them is pointless: the header protection key hides the message type, and it is also what makes each install unique. S1-S4 and I1 change the same way as above, only S1-S4 not below 12. A new HeaderProtectionKey comes with a reinstall (--uninstall plus an install), and every profile is issued again.

A 2.0 server. S1-S4 and H1-H4 must match on the server and every client, otherwise the handshake fails; Jc/Jmin/Jmax and I1-I5 do not have to match. The easiest way to get a fresh random set of H1-H4 and S1-S4 is a reinstall with a set-regeneration flag (--force --preset=default), followed by a regen of every client.

The detailed steps are in Changing Default Settings.
Q: How is this different from the official Amnezia app? A: The protocol underneath is the same AmneziaWG with the same obfuscation. What differs is how the server is deployed and run. The official Amnezia app is a graphical client: you point it at a server and it installs the server side in Docker containers over SSH, without the host-wide tuning and hardening this installer does. This installer is built to get the most out of a dedicated VPS as a VPN server, so it works differently:
  • AmneziaWG runs as a kernel module (DKMS), with no Docker - no background daemon and none of its RAM/CPU cost.
  • The whole server is tuned to the hardware: sysctl buffers, swap, BBR, unneeded packages stripped.
  • The attack surface is kept small: UFW deny-all, Fail2Ban, strict permissions, sysctl hardening, one service instead of a stack.
  • Fine tuning is available: a mobile-network preset and direct access to the AmneziaWG parameters.
  • Management is from the CLI (manage add/remove/list/--expires), with prebuilt ARM modules and a headless mode for automation.
A detailed comparison is on the comparison page.
Q: My Hetzner server is unreachable from Russia: the handshake completes, then traffic freezes. What do I do? A: The server most likely landed in an AS that is not on Russia's allowlist (Hetzner is AS24940). Ordinary junk does not help; what gets through is an I1/CPS packet disguised as QUIC with an allowlisted SNI (7-zip.org for Hetzner). The method does not work on every ISP. Field results and instructions are in the Host Unreachable from Russia section.
Q: Server behind NAT — how do I specify the external IP? A: Use the --endpoint=<external_IP> flag during installation: sudo bash ./install_amneziawg_en.sh --endpoint=1.2.3.4. On an installed server, put the address into /root/awg/awgsetup_cfg.init (the line export AWG_ENDPOINT='1.2.3.4') and reissue the clients: sudo bash /root/awg/manage_amneziawg.sh regen. With AWG_ENDPOINT empty, regen detects the external IP itself.
Q: How do I set up port forwarding (NAT) for AmneziaWG? A: If the server is behind NAT (e.g., in a cloud with a private IP): 1. Forward the AmneziaWG UDP port (default 39743) to the external IP. 2. During installation, specify the external IP: --endpoint=EXTERNAL_IP. 3. Make sure the provider's firewall allows incoming UDP on that port.
Q: How do I change DNS for all existing clients? A: Use the modify command for each client: sudo bash /root/awg/manage_amneziawg.sh modify <name> DNS "8.8.8.8,1.0.0.1". Then regenerate configs: sudo bash /root/awg/manage_amneziawg.sh regen. The DNS for new clients is set by a CLIENT_DNS line in awgsetup_cfg.init, see Changing Default Settings.
Q: How do I monitor VPN traffic? A: 1. Current connections: sudo awg show. 2. Transfer stats: sudo awg show awg0 transfer. 3. Service logs: sudo journalctl -u awg-quick@awg0 -f. 4. Overall status: sudo bash /root/awg/manage_amneziawg.sh check.
Q: The client says "invalid key" or "unknown key" when importing a config? A: The client does not understand the server's generation. If it trips over s3, s4 or I1, it is older than the second line; if over HeaderProtectionKey or ContentPaddingAddition, it does not understand 3.1. Update the client: amneziawg-windows-client 3.1.0+ for a 3.1 server (2.0.0+ for a 2.0 server) or Amnezia VPN 5.0.1.5+ (4.8.12.7+ for 2.0). Other clients are in the table Which AmneziaWG generation a client speaks.
Q: My AWG 2.0 server can't handshake with my old AWG 1.0 client — why? A: When the server generates S3>0 or S4>0 (cookie / data padding from AWG 2.0), an AWG 1.0 client cannot handshake with it. This is a known upstream issue: amnezia-vpn/amneziawg-linux-kernel-module#168. My installer always generates both >0: S3=8..55 and S4=4..27 in the 2.0 profile, from 12 in the 3.1 profile.

In the typical scenario (Amnezia VPN client / WireGuard-Tools 2.0+ on the clients + client .conf files generated by manage add) there is no issue: manage always writes S3 / S4 into the client .conf. The risk arises only when:
  • client .conf files are hand-edited to remove S3 / S4;
  • the server preset is imported into a WireGuard client without AWG extensions (plain wg-quick on an older kernel without the amneziawg module);
  • migrating from an AWG 1.x setup where clients deliberately used S3=0 / S4=0.
Resolution: use an AWG 2.0-aware client (Amnezia VPN >= 4.8.12.7 or amneziawg-windows-client >= 2.0.0) and keep S3 / S4 in the client .conf identical to the server. A real AWG 1.0 fallback is out of scope for the standard install — track upstream issue #168 for a fix.
Q: DKMS error after kernel update — what should I do? A: 1. Check status: dkms status. 2. Try rebuilding: sudo dkms install amneziawg/$(dkms status | grep amneziawg | head -1 | awk -F'[,/ ]+' '{print $2}'). 3. Make sure kernel headers are installed: sudo apt install linux-headers-$(uname -r). 4. If the error persists, run diagnostics: sudo bash ./install_amneziawg_en.sh --diagnostic.
Q: What changes for me after installing v5.12.0+ when the kernel is upgraded? A: Before v5.12.0, after apt upgrade of the kernel DKMS did not always rebuild the amneziawg module by the next reboot. Symptom: awg-quick@awg0 fails with modprobe: FATAL: Module amneziawg not found, and the VPN is down until manual recovery.

In v5.12.0 I added three safety nets that work transparently:
  1. apt hook /etc/apt/apt.conf.d/99-amneziawg-post-kernel — after apt upgrade the helper /usr/local/sbin/amneziawg-ensure-module --hook rebuilds DKMS for the new kernel. Log: /var/log/amneziawg-ensure-module.log (weekly rotate, 4 copies).
  2. systemd unit amneziawg-ensure-module.service — at boot, before awg-quick@awg0, the helper iterates kernels with already-installed headers, rebuilds DKMS for the current kernel, runs modprobe amneziawg, and verifies the load via lsmod. If headers are not yet installed, it logs a WARN and exits successfully so it does not block boot. Logs in journal: journalctl -u amneziawg-ensure-module.service.
  3. manage repair-module — explicit fallback: sudo bash /root/awg/manage_amneziawg.sh repair-module installs kernel-headers (with AWG_ALLOW_APT_IN_ENSURE=1), rebuilds DKMS, restarts awg-quick.
Manual recovery (if none of the three auto paths fired, or you are still on v5.11.x):
sudo apt install linux-headers-$(uname -r)
sudo dkms autoinstall
sudo modprobe amneziawg
sudo systemctl restart awg-quick@awg0
Limitations:
  • ARM prebuilt (Raspberry Pi, Hetzner CAX, Oracle Ampere) uses a prebuilt .deb rather than DKMS - auto-repair is not engaged. After a kernel upgrade rerun the installer: it will pick a fresh prebuilt or fall back to DKMS. manage repair-module cannot replace a missing prebuilt module.
  • Cloud kernels (Azure / AWS / GCP / Oracle / Debian-cloud) — the installer detects the meta-package from the uname -r suffix (e.g. linux-headers-azure). If you have a custom kernel or an unusual flavor, manage repair-module does the same in reactive mode.
Q: Detailed steps for VPN migration to another server? A: 1. On the old server: sudo bash /root/awg/manage_amneziawg.sh backup. 2. Copy the archive: scp root@old_server:/root/awg/backups/awg_backup_*.tar.gz .. 3. Install AmneziaWG of the same generation as the old server on the new one (for a 2.0 server, with --protocol=2.0): restore brings the server back to the generation of the backup and warns about that. A backup of a 3.1 server carries the header protection key too, so after the move clients need new configs only for the new address. If you want to move from 2.0 to 3.1 along the way, do not restore the backup; hand out new profiles to your clients instead. 4. Copy the backup: scp awg_backup_*.tar.gz root@new_server:/root/awg/backups/. 5. Restore: sudo bash /root/awg/manage_amneziawg.sh restore (interactive selection, or specify the full archive path). 6. Check what came over from the old server: the PostUp/PostDown lines in /etc/amnezia/amneziawg/awg0.conf carry the old machine's network interface name (-o eth0 and so on). If the new server names it differently (ip route get 1.1.1.1), replace the name and restart the service (sudo systemctl restart awg-quick@awg0). ListenPort must match the port open in UFW. If /root/awg/awgsetup_cfg.init sets AWG_ENDPOINT, put the new address there or leave it empty for auto-detection. 7. Regenerate configs with new IP: sudo bash /root/awg/manage_amneziawg.sh regen. 8. Distribute new configs to clients.
Q: Smartphone doesn't connect over cellular / doesn't work on iPhone A: Check the MTU = 1280 line in the [Interface] section of the server and client configs: the installer has set it by itself since v5.7.4, and older configs need it added (cellular networks have lower MTU than the default 1420, and iOS is strict about PMTU, see MTU and Mobile Clients). If the line is there and the handshake still does not complete on cellular, it is usually I1 or the port: see The handshake never completes on cellular, and for an unstable connection the next question. A client of a 3.1 server needs 3.1 support, see the client table.
Q: iPhone connects but traffic stops after ~10 seconds (the tunnel "hangs") A: Fixed in v5.16.1. The routing mode that was default back then (mode 2, "Amnezia List + DNS") started with the 0.0.0.0/5 range, which covers the reserved 0.0.0.0/8. The iOS kernel chokes on that block and never reaches the rest of the routes, so the tunnel comes up and then stalls after ~10 seconds (easy to mistake for DPI). Traced and fixed by @LiaNdrY (Issue #42). In v5.16.1 the first range is split into 1.0.0.0/8, 2.0.0.0/7, 4.0.0.0/6 - the same coverage without the problematic zero block, and private networks still stay outside the tunnel.

On an existing server (before v5.16.1) the stored list lives in /root/awg/awgsetup_cfg.init and a plain --force reinstall does not change it (it is read back from the config). So: (1) quick per-client fix - replace the AllowedIPs = ... line in the iOS client config with AllowedIPs = 0.0.0.0/0; (2) keep the "Amnezia" mode list - edit /root/awg/awgsetup_cfg.init, replace the leading 0.0.0.0/5 with 1.0.0.0/8, 2.0.0.0/7, 4.0.0.0/6, then recreate the client (remove + add); (3) or a clean reinstall (--uninstall, then install v5.16.1) regenerates the list correctly.
Q: VPN connects over cellular only on the third attempt / unstable A: If the handshake never completes on cellular at all, start with The handshake never completes on cellular: the September 2026 measurement on MTS Moscow shows that the shape of the I1 concealment packet decides the handshake, while the number and size of junk packets (Jc, Jmin, Jmax) do not affect it. If the connection is merely unstable, try the --preset=mobile flag (since v5.10.0): it sends fewer and smaller junk packets (Jc=3). In Discussion #38 (@elvaleto), on Tattelecom (Letai) with Jc=4-8 it took multiple attempts to connect, and after setting Jc = 3 it worked immediately.

Fresh install (recommended):
sudo bash install_amneziawg_en.sh --preset=mobile --yes
Existing install — manual edit:
  1. Open /etc/amnezia/amneziawg/awg0.conf and change Jc to 3. If I1 looks like <r N> with N above 64, replace it with a DNS-shaped string (where to get one is in the diagnosis) or with <r 64>. Leave a DNS-shaped I1, written by installs of v5.33.0 and newer, as it is.
  2. sudo systemctl restart awg-quick@awg0
  3. sudo bash /root/awg/manage_amneziawg.sh regen <client_name> for each client.
  4. Redistribute updated configs.
If the handshake still never completes, lowering the junk further is useless: on the measured route the cause was I1, see the diagnosis.

Carrier reports (from issues/discussions):
CarrierParametersRecommendationResult
Tattelecom (Letai)Jc=3, I1=<r 64>--preset=mobile✅
Yota (Moscow)I1=<b 0xce...>, Jmax=261--preset=mobile✅
Yota/Tele2 (Moscow)Jc=3, Jmin=40, Jmax=70--preset=mobile✅
Tele2 (Krasnoyarsk)earlier I1=absent; May 2026: I1=<r 48>--preset=mobile; in the May wave I1=<r 48>✅
MTS (Primorsky Krai)Jc=3, I1=<r 48> (May 2026)--preset=mobile + I1=<r 48>✅
MTS (Moscow)a random I1=<r 96> and longer does not pass, <r 64> and a DNS-shaped I1 pass; Jc/Jmin/Jmax make no difference (measured, September 2026)DNS-shaped I1 (written by the installer on fresh installs since v5.33.0) or <r 64>, see the diagnosis✅
Beelinedefault--preset=default✅
Megafon (Moscow)Jc=3, Jmin=80, Jmax=268--preset=mobile🔄 testing
Megafon (regions)I1=absent--preset=mobile + remove I1✅
T-Mobile (Moscow and region)narrow profile (like the Amnezia app): Jc=6, Jmin=10, Jmax=50, DNS-mimic I1=<r 2><b 0x8580...> (full value in the routers section below); full tunnel 0.0.0.0/0, ::/0manual parameters; the diagnose --carrier=tmobile_us profile (the name is historical, the profile is for this carrier) checks Jc/Jmin/Jmax and that I1 is binary; --preset=mobile does not fit here✅
Tele2 + Megafon (Kemerovo, region 42)random I1 (<r N>) stopped passing after 2+ days; works with QUIC-mimicry I1=<b 0xc3...> or I1=absent--preset=mobile + I1=<b 0xc3...> (QUIC) or remove I1✅

"I1=absent" means: in /etc/amnezia/amneziawg/awg0.conf and in client .conf files, remove the I1 = ... line entirely (do not leave it empty). This is a mode without the concealment packet: the rest of the obfuscation stays, there is just no CPS (unless you set I2-I5), and the handshake clears DPI at some regional carriers where CPS packets themselves trigger blocks (Issue #42, @alkorrnd). On the server: sudo systemctl restart awg-quick@awg0. On clients - sudo bash /root/awg/manage_amneziawg.sh regen <name> for each, then redistribute the configs.
Update, May 2026: in the May blocking wave the I1=absent option stopped working on Tele2 (Krasnoyarsk), while a short I1 = <r 48> cleared DPI. The same worked on MTS (Primorsky Krai). At the time this was put down to size: a smaller value <r 48> may be less conspicuous to DPI. ⚠️ The September 2026 measurement refines the cause: what decides is the SHAPE of the packet, not its size. A packet of random bytes is dropped whether it is large or small (small ones just less often), while a packet of the same length that resembles a real protocol gets through. A short <r 48> helps because there is little for the filter to catch on to in a short piece of noise, and that is weaker than a meaningful shape. Details and the table: the handshake never completes on cellular. If --preset=mobile or I1=absent do not help - try I1 = <r 48>. The diagnose --carrier=tele2_krasnoyarsk profile still reflects the earlier I1=absent (Issue #42), so for the May 2026 wave set I1 = <r 48> manually (Discussion #38, @alkorrnd + @etotent).
QUIC-mimicry I1 (experimental): instead of a random <r N> you can set I1 as a block that mimics the start of a QUIC packet: I1 = <b 0xc30000000108><r 8><b 0x08><r 8><b 0x0045dc><t><r 16>. The first bytes (0xC3 + version) look like a QUIC v1 long-header, and DPI that classifies UDP/443 as QUIC let the flow through in this report. It held for 2+ days on Tele2/Megafon (Kemerovo) (Issue #42, @Fourdot-co). This is a client-side parameter, changed only in client .conf files, no server sync needed; mind that editing just one exported .conf will be lost on the next client regen. Note: do not base it on a TLS ClientHello (<b 0x160301...>) - that is a TCP format, over UDP the DPI will see the TCP structure and drop the packet. For UDP mimicry use a QUIC long-header or DTLS (the same ClientHello handshake type, but with a record header that adds epoch and sequence number).
How to check whether a carrier is blocking your VPN server (DPI/TSPU diagnostics): if AmneziaWG cannot punch through on a particular carrier, first find out whether the server IP itself is blocked and by what signal. The open-source scanner ByeByeVPN inspects the address from the censor's side and helps tell an obfuscation-parameter problem (then tune Jc/I1 per the table above) apart from an AS/IP-level block (then see the Hosting unreachable from Russia section). For reference, a stock install scores 100/100 CLEAN with this scanner (checked against version 2.8.3 in August 2026). Its AmneziaWG probes go to ports 51820 and 55555 while the installer listens on 39743 by default; and even on 51820 the server leaves those probes unanswered, because it silently drops a handshake that was not built with its public key. Do not read that as "undetectable": the scanner asks the server questions, while a carrier's passive traffic analysis works differently and takes no part in this check.
Q: Script breaks the hoster VNC console / network drops on Hetzner A: Before v5.8.2 the script set net.ipv4.conf.all.rp_filter = 1 (strict reverse-path filtering). On Hetzner and similar cloud hosters where the gateway is in a different subnet than the VPS IP, strict mode breaks routing — reply packets fail the reverse-path check. Symptoms: VPS periodically loses network (once a day), and the VNC console fills with [UFW BLOCK] lines from Fail2Ban, making it unusable. Discussion #41 (@z036). As of v5.8.2 rp_filter is set to 2 (loose mode), which validates source IP against any route in the table (not just the same interface), and kernel.printk = 3 4 1 3 is added to suppress non-critical kernel messages on the VNC console. If you are on a pre-v5.8.2 install — fix manually:
  1. Open /etc/sysctl.d/99-amneziawg-security.conf
  2. Change rp_filter = 1 to rp_filter = 2 (both lines: conf.all and conf.default)
  3. Add a line kernel.printk = 3 4 1 3
  4. sudo sysctl -p /etc/sysctl.d/99-amneziawg-security.conf
Q: ping does not work between server and clients inside the tunnel A: The script applies ufw default deny incoming — this blocks all incoming traffic on every interface, including awg0. The forward rule ufw route allow in on awg0 out on <public_iface> only allows tunnel → internet; input on awg0 (packets from clients to the server itself, including ICMP echo-request) is not covered by it.

Additionally: if you edited /etc/ufw/before.rules and replaced ACCEPT with DROP for ICMP without specifying an interface, those rules apply to every interface — including awg0.

Fix:
  1. Open incoming on awg0 in UFW:
    sudo ufw allow in on awg0
    sudo ufw reload
    This allows all incoming on the tunnel interface — narrow ICMP filtering is done via -i in before.rules (see below).
  2. If you edited /etc/ufw/before.rules, add -i <public_iface> to every ICMP DROP line:
    # instead of
    -A ufw-before-input -p icmp --icmp-type echo-request -j DROP
    # use (ens3 — your public iface)
    -A ufw-before-input -i ens3 -p icmp --icmp-type echo-request -j DROP
    Same for destination-unreachable, time-exceeded, parameter-problem. Find your public interface name: ip route get 8.8.8.8 | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}'. Apply: sudo ufw reload.
Does NOT work: ufw allow in on awg0 proto icmp — UFW does not support icmp via the proto flag (only tcp/udp/esp/ah/gre/ipv6).

Verify: from the client ping <server_tunnel_IP>. From the server to a client (ping <client_IP>) the client itself may not reply: on Windows and iOS the built-in firewall often drops echo-request — testing client → server is the cleanest path.

If you manually customized AllowedIPs on the client for split tunneling (only some subnets go through the VPN - e.g. only Telegram/Discord, everything else stays direct), make sure the tunnel subnet (10.9.9.0/24 or your custom one) is in that list. Without it, the client does not route through the tunnel even packets destined for the server itself - ufw status verbose and iptables -L ufw-before-input -v -n can look correct, and ping still fails. Coverage depends on the routing mode chosen at install time: the default --route-all (full tunnel 0.0.0.0/0) includes the tunnel subnet automatically; --route-amnezia (Amnezia List, excludes 10.0.0.0/8) and --route-custom= do not, add it explicitly.

For client-to-client ping (phone ↔ router via the server): sudo ufw route allow in on awg0 out on awg0 && sudo ufw reload. AllowedIPs in client .conf depends on the routing mode chosen at install (see the paragraph above). Discussion #63.
Q: Does AmneziaWG work in an LXC container? A: Not with this installer: it installs a kernel module via DKMS, and a container shares the host kernel and cannot load a module of its own, so inside a container the installer stops right at the start with "Container detected". Options: a full VM (KVM/QEMU) or bare metal for the regular install, or the amneziawg-go userspace implementation inside LXC, set up by hand: LXC / Docker via amneziawg-go.
Q: --endpoint is rejected with "Invalid --endpoint" — what should I check? A: As of v5.8.0, the value of --endpoint is validated before it is written to config files. Three formats are accepted: FQDN (vpn.example.com), IPv4 (1.2.3.4), and bracketed IPv6 ([2001:db8::1]). Newlines, CR, single and double quotes, backslash, spaces, and tabs are rejected — they could inject lines into awgsetup_cfg.init and client .conf files. IPv6 addresses must be wrapped in []. If AWG_ENDPOINT in awgsetup_cfg.init fails validation on a later run, the installer emits log_warn and falls back to automatic detection via get_server_public_ip.
Q: "Another installer instance is already running" — what is this? A: As of v5.8.0, the installer takes a process-wide flock on /root/awg/.install.lock at the beginning of initialize_setup(). This prevents two parallel runs from racing each other on apt-get and corrupting package state. If you see this error but no second installer is actually running (hung / crashed process), remove /root/awg/.install.lock and try again.
Q: Why did --uninstall not disable UFW? A: This is the expected behaviour as of v5.8.0. The installer writes a marker file /root/awg/.ufw_enabled_by_installer only if it had to enable UFW itself (UFW was in inactive state before the install). During --uninstall, UFW is disabled only when that marker is present. If UFW was already active on the VPS before this script was installed (for example, protecting SSH or web services), --uninstall will remove our own rules (VPN port, awg0 routing; the SSH rate-limit rule it added stays in place) but leave UFW running. This protects your firewall posture from destructive uninstall on a VPS that was already hardened. If you want to force UFW off anyway — run ufw disable manually.
Q: regen says "required AWG parameters missing" — what do I do? A: As of v5.8.0, load_awg_params reads AWG parameters directly from the live /etc/amnezia/amneziawg/awg0.conf instead of the cached awgsetup_cfg.init. If you edited awg0.conf by hand and accidentally removed or corrupted one of the required fields (Jc, Jmin, Jmax, S1-S4, H1-H4), regen will fail with this error instead of silently using stale values from the init file. This is split-brain protection between server and clients. How to fix: (1) check that all 11 fields are present with grep -E "^(Jc|Jmin|Jmax|S[1-4]|H[1-4]) = " /etc/amnezia/amneziawg/awg0.conf; (2) if a field was removed, restore it from /root/awg/awgsetup_cfg.init or from an awg0.conf.bak-* backup; (3) restart the service and retry regen. On a 3.1 server regen then also checks the HeaderProtectionKey in awg0.conf against its copy /root/awg/server_hpk.key; if the error is about the key, do not create a new one (it would cut off every client). awg0.conf holds the key configured for the server (the running interface picks up an edit of it only after a restart), and the file holds the key the profiles were issued from. If they differ, decide which one your clients have: compare with any client .conf. If the key in awg0.conf is right, move the file aside (mv /root/awg/server_hpk.key /root/awg/server_hpk.key.old) and run manage regen: manage restores a missing file from the config by itself. If the key in the file is right, put it back into the HeaderProtectionKey line of awg0.conf and restart awg-quick@awg0 (or restore the config with manage restore, keeping in mind the backup may predate your newest clients).
Q: amneziawg-windows-client underlines H2-H4 in red and will not let me edit the config A: This is an upstream bug in the standalone Windows client amneziawg-windows-client (a wireguard-windows fork with AWG patches). Its built-in config editor in ui/syntax/highlighter.go caps H1-H4 at [0, 2147483647] (2^31-1, INT32_MAX), even though the AmneziaWG spec allows the full uint32 (0-4294967295). Values above 2^31-1 work fine on the server, but the client underlines them as invalid and may block saving. Upstream issue: amnezia-vpn/amneziawg-windows-client#85 (open since February 2026, not yet fixed). As of v5.8.1 our installer generates H1-H4 in the safe half of the range [0, 2^31-1] — fresh installs are compatible with the Windows client out of the box. If you already have a v5.8.0 install with "bad" H values: (1) upgrade via --uninstall + reinstall with v5.8.1 — new H values will be in the safe range; or (2) manually edit H2/H3/H4 in awg0.conf to values less than 2147483647, restart the service, and regenerate client configs with manage regen <name>; or (3) use the cross-platform Amnezia VPN client instead of amneziawg-windows-client — it does not have this limit. Discussion: #40.
Q: Which client should I use for AmneziaWG? A: On Android, WG Tunnel (>= 5.6.0 for 3.1) or the lightweight AmneziaWG (>= 3.1.4 on Google Play for 3.1). In our check on 13 October 2026 with a 3.1 profile, both imported <name>.png with their own scanner, reconnected in under a second and kept the tunnel when the phone moved from Wi-Fi to mobile data. On iPhone, Mac and Windows, the lightweight AmneziaWG for your platform (for 3.1: iOS and macOS >= 3.1.4, Windows >= 3.1.0); iPhone and Mac were not part of that check. Amnezia VPN (>= 5.0.1.5 for 3.1, >= 4.8.12.7 for 2.0) is supported too and accepts the vpn:// link; on Android in the same check, version 5.0.3 took about 2 seconds to connect, and after Wi-Fi was switched off the tunnel did not recover, not even with the Connect button, until the app was restarted. On Linux, awg-quick from amneziawg-tools with a module or amneziawg-go that understands the server's generation (for a 3.1 server, the third line; a third-line build works with 2.0 too), see Connecting a Linux machine as a client. The standard WireGuard client does not support AWG parameters. See Client Compatibility for the full table.
Q: The tunnel is up, but the first request opens with a delay A: Clients built on the amneziawg-go engine (the lightweight AmneziaWG for Android, iOS, macOS and Windows, Amnezia VPN) have a known race: the very first data packet after the tunnel comes up can leave with the default S4 padding (0) instead of the configured one, and the server drops it (amneziawg-go#168; a fix is proposed in #184 and is not merged as of 13 October 2026). Losing the first packet usually means a TCP or DNS retry, so a delay; if the application does not retry, the request can time out. WG Tunnel is built on its own fork of that engine; whether the fork has this race we did not check. On its own this race does not point to a problem in the config or on the server.

In our check on 13 October 2026 with a 3.1 profile there was no delay attributable to this race: AmneziaWG 3.1.4 on Android opened the page in under a second in 10 reconnects out of 10, WG Tunnel 5.8.1 just as fast. On Windows, AmneziaWG 3.1.0 opened the first page after about 3 seconds, but the 2.0 profile behaved the same way: that is the time Windows spends checking the address of the tunnel adapter (DAD), and the protocol itself accounts for about 0.2 seconds. The result applies to these devices and networks; others can differ.
Q: How do I limit bandwidth for clients? A: AmneziaWG has no built-in bandwidth limiting. Use tc (traffic control): sudo tc qdisc add dev awg0 root tbf rate 100mbit burst 32kbit latency 400ms. This limits total interface throughput. For per-client limits, a more complex setup with tc and iptables (mark + class) is required.
--- ## 🩺 Diagnostics and Uninstall * **Diagnostics:** `sudo bash /path/to/install_amneziawg_en.sh --diagnostic`. The report (obfuscation parameters included, `PrivateKey`, `PresharedKey` and `HeaderProtectionKey` replaced with `[HIDDEN]`) is saved to `/root/awg/diag_*.txt`. * **Uninstall:** `sudo bash /path/to/install_amneziawg_en.sh --uninstall`. Will ask for confirmation and offer to create a backup. ### Diagnostic Report Contents The report (`--diagnostic`) includes the following sections: | Section | Description | |---------|-------------| | OS | OS and kernel version (`uname -a`) | | Hardware | RAM, CPU, Swap | | Configuration | Contents of `awgsetup_cfg.init` (secrets hidden) | | Server Config | `awg0.conf` (keys hidden) | | Service Status | `systemctl status awg-quick@awg0` | | AWG Status | Output of `awg show` | | AWG Version | Output of `awg --version` | | Network Interfaces | Output of `ip a` | | Listening Ports | Output of `ss -lunp` | | Firewall Status | `ufw status verbose` | | Routing Table | Output of `ip route` | | Cascade / Split Routing | The `awg-routing` unit, the `ip rule` by mark, table 100, `ipset` sets, the cascade `mangle` and `nat` rules; "not configured" when there is no cascade | | Kernel Params | `net.ipv4.ip_forward`, `net.ipv6.conf.all.disable_ipv6` | | AWG Journal (last 50) | Last 50 lines of the `awg-quick@awg0` journal | | Client List | Client names | | DKMS Status | Output of `dkms status` | | Module Info | Details of the loaded `amneziawg` module | --- ## 🔧 Troubleshooting (Detailed)
The server does not answer after the reboot in step 1 The installer finished step 1, rebooted, and the machine no longer answers: no SSH, no ping. The hosting panel still shows it powered on. First make sure the server is unreachable from everywhere, not only from Russia: try ping or SSH from a network outside Russia, or a reachability checker with nodes in several countries. If it answers from outside Russia, the server is alive and the hosting provider's address or subnet is probably blocked, see [Hosting unreachable from Russia](#as-blocking-adv). But first make sure your own address is not the one banned: Fail2Ban and the SSH rate limit on the server ban by source address, and a successful ping does not yet mean SSH works. If it answers nowhere, there is no way in over the network any more, so the first thing you need is the **hosting provider's console** (VNC, KVM or serial). It shows whether the system got as far as booting at all, which the control panel does not tell you. What you will see there: - **a `login:` prompt** - the system booted, the network is broken. Look at `ip -br a`, `ip route`, `ss -lntp`; - **`You are in emergency mode`** - the boot stopped on its own, see below; - **a kernel panic or the bootloader menu** - it never got to the system, photograph the whole screen. Step 1 updates the system and may install a new kernel; if the boot breaks on exactly that kernel, reboot and pick the previous kernel in the GRUB menu (the "Advanced options for Ubuntu" or "Advanced options for Debian GNU/Linux" entry; if the menu does not show, hold Shift or Esc while booting). Once it boots, attach the output of `uname -r` and `journalctl -b -1 -p err` to an issue (the journal remembers the previous boot only if it is persistent, which a stock Debian 12 is not). **Emergency mode** means systemd could not mount something from `/etc/fstab` and deliberately stopped. This does not clear up by itself: while the cause is there, every boot ends in the same place. Capture three blocks from the console: ```bash journalctl -b --no-pager | grep -iE "Dependency failed|Timed out|emergency" | tail -20 cat -A /etc/fstab; findmnt --verify blkid; ls -l /dev/disk/by-label/ ``` How to read them: - a line `Timed out waiting for device dev-disk-by\x2dlabel-...` means systemd never saw a partition that fstab refers to by label. It waits 90 seconds per partition by default, then drops into emergency mode; - `blkid` shows the label while `/dev/disk/by-label/` is empty or absent - the symlinks were not created by udev. Check that it is there at all: `command -v udevadm || echo "the udev package is missing"`; - `findmnt --verify` reports anything other than `0 parse errors` - fstab itself is damaged, look at the `cat -A` output: it draws a `$` at the end of every line, so a glued line is visible at once. **The `udev` package is missing.** Restore it and rebuild the initramfs, naming the packages in one command: ```bash apt-get install udev systemd initramfs-tools reboot ``` `systemd` is on that list for a reason: `udev` is built from the same source and does not get along with a `systemd` older than itself, so apt will not install them separately and answers `udev : Breaks: systemd (< ...)`. Leaving out `-y` is deliberate too: if apt intends to remove anything, read the list before you confirm. If apt complains about one more package that is not in the command (of the form `X : Breaks: Y` or `X : Depends: Y`), add Y to it and try again. In issue #223 that package turned out to be `systemd-resolved`: it pinned `systemd` to the old version through a dependency on an exact version. ⚠️ Only add what apt named itself. `systemd-resolved` in particular must not be installed on a hunch: on Debian with a static resolver or with `resolvconf` it may never have been there, and it declares `Conflicts: resolvconf` and `Replaces: resolvconf`, so installing it removes `resolvconf` and can leave an already damaged server without DNS as well. **udev is there but the symlinks are not.** Try creating them by hand and letting the boot finish: ```bash udevadm trigger --action=add --subsystem-match=block; udevadm settle; ls /dev/disk/by-label/ systemctl default ``` **The symlinks still do not appear.** Take the boot's dependency on those partitions away: the `nofail` option tells systemd not to treat their absence as a reason to stop. ```bash cp /etc/fstab /etc/fstab.bak sed -i 's/defaults/defaults,nofail/; s/umask=0077/umask=0077,nofail/' /etc/fstab cat /etc/fstab reboot ``` Check the `cat` output: `nofail` must appear on the `/boot` and `/boot/efi` lines while the root line stays untouched. ⚠️ While `nofail` is in place, `/boot` is not mounted and kernel updates write exactly there. Do not install new kernels and postpone `apt upgrade` until udev is fixed. From **v5.28.0** on, the installer checks this itself and stops BEFORE the reboot if the system upgrade took away packages the server cannot boot without. From **v5.28.1** the step 1 upgrade cannot remove an installed package at all: `apt full-upgrade` was replaced with `apt-get upgrade --with-new-pkgs`, which has no such right. That stop is explained in the next block.
The installer stopped with "Boot-critical packages missing" (v5.28.0+) Step 1 records which boot-critical packages are installed - `udev`, `initramfs-tools`, `openssh-server` and the network stack - and compares that list before it takes the machine into a reboot. The list lives in `/root/awg/boot-critical.pkgs` and survives installer restarts, so a package lost during an aborted run is not forgotten. If something is missing and could not be restored, the installer stops **while the server is still reachable**. This is not the installer failing but the installer refusing to reboot into a system that will not come back: after the reboot the repair would need the hosting provider's console. What to do, in order of how often it applies: 1. **The package really is gone.** Install everything in one command, naming all of them, then run the installer again: ```bash apt-get install ... ``` Together rather than one at a time: related packages such as `udev` and `systemd` will not go in separately, because each individual command has to leave the rest untouched. If apt does not know one of the names at all, it aborts the whole transaction - drop that name and repeat. No `-y`: if apt intends to remove anything, read the list before you confirm. 2. **apt refuses because of held packages.** `apt-mark showhold` lists the holds. Release the one you need: `apt-mark unhold `. 3. **The package is gone from the repositories.** Its name may have changed with a distribution release upgrade. Drop the stale line from `/root/awg/boot-critical.pkgs`. The file is removed when the installation completes, so nothing is left over from previous installs and it has no time to go stale.
No internet after connecting to VPN 1. Check IP forwarding: `sysctl net.ipv4.ip_forward` (should be 1) 2. Check NAT rules: `iptables -t nat -L POSTROUTING -v` 3. Check client AllowedIPs (routing mode) 4. Check DNS: `nslookup google.com` from VPN 5. Check MTU: `ping -s 1280 -M do ` - if it fails, reduce MTU, and the MSS along with it (see "How do I change the MTU?" in the FAQ)
Handshake succeeds but traffic doesn't flow 1. Check MTU: add `MTU = 1280` to `[Interface]` in both server and client configs 2. Check iptables: `iptables -L FORWARD -v` — there should be an ACCEPT rule for awg0 3. Check NIC: `ip route get 1.1.1.1` — make sure PostUp/PostDown use the correct interface
AmneziaWG handshake completes but traffic then dies (Russian DPI / TSPU, Hetzner, endless re-handshakes) This symptom is different from the item above: the handshake **completes once** (`Received handshake response` shows up in the client log), traffic may flow for a couple of seconds, then it goes silent. The client loops `Handshake did not complete after 5 seconds` and `stopped hearing back`, while `awg show` on the server shows a sharp asymmetry: the client sent tens of KiB, the server received only a couple of KiB, and `latest handshake` never refreshes. The server is not the problem here - the config is fine. This is DPI filtering by the host IP/AS: in-path equipment (in Russia, the TSPU) lets the initial handshake through, then chokes the established flow to near zero. The tell in `awg show`: the client received about 92 bytes (a WireGuard-level handshake response; on the wire the packet is larger due to obfuscation) and nothing more, even though it sent tens of KiB. From my own measurements Hetzner (AS24940) is consistently affected; large datacenter networks (OVH, AWS, Azure and the like) are also a risk zone - test by the specific IP and route, the block is not total. Quick way to confirm it is the path, not the config: bring the same config up **from a different network** (mobile data, another country). If the tunnel holds from there, the config works and the route to your current host is being cut. What to do: 1. Spin up a test server at a different host or in another country. If the handshake holds there, the issue is your current host's AS. 2. Move the server to a host with clean IPs that are not flagged as datacenter ranges. My pick is in the [Hosting](README.en.md#hosting-recommendation) section (FreakHosting): I tested it on my own Russian routes, and as of this writing AmneziaWG runs through it reliably, unlike Hetzner. This is not a guarantee - DPI shifts, so test a small VPS before migrating. 3. Or put a relay/bridge in a "clean" network in front: client -> relay -> exit. The client->relay leg is not subject to the destination filter, and relay->exit runs between data centers.
AmneziaWG won't connect: the handshake never completes, though plain WireGuard works on the same server Symptom: the client tries to connect, but the server does not process the AmneziaWG handshake - `tcpdump` shows packets arriving on the right port and the server accepting them, yet `latest handshake` in `awg show awg0` stays empty. Meanwhile plain WireGuard comes up fine on the same server. This happens outside Russia too, where DPI is not involved at all. Since vanilla WireGuard works, the network, the port and the firewall are ruled out. The difference from AmneziaWG is the obfuscation layer: `Jc`/`Jmin`/`Jmax`, `S1`-`S4`, `H1`-`H4`, `I1`-`I5`, and in the 3.1 profile the `HeaderProtectionKey` header encryption too. Parsing an incoming packet relies on `S1`-`S4`, `H1`-`H4` and (in 3.1) the key: if any of those does not match byte-for-byte between server and client, the server cannot parse the handshake and silently drops it - from the outside it looks like "packets arrive but are not processed". **First find out the server's generation:** `sudo bash /root/awg/manage_amneziawg.sh check` prints the line "Installation protocol generation: 3.1" (or 2.0). On a 3.1 server the client has to understand 3.1 (the table [Which AmneziaWG generation a client speaks](#client-gen-adv)) and carry the same `HeaderProtectionKey` as `awg0.conf`. A client without 3.1 support often shows no error: there is simply no handshake. For the yellow profile label in the Amnezia app, see [its own question](#faq-yellow-adv). What to check: 1. Compare the obfuscation parameters in the `[Interface]` section on the server and the client. `S1`-`S4` and `H1`-`H4` must match, and on a 3.1 server the `HeaderProtectionKey` too: the receiver strips that padding and checks those headers, so any mismatch here means the packet is dropped and the handshake never completes. In 2.0, `H1`-`H4` are ranges - the same ranges on both sides. `Jc`/`Jmin`/`Jmax` and `I1`-`I5` do not have to match: they are separate decoy packets, the handshake initiator sends them and the other side discards them, so there is nothing here to **mismatch**. ⚠️ That does not make `I1` irrelevant to the handshake: it requires no agreement between the sides, but its shape can stop the handshake packet from arriving, because the two travel in the same burst. That is a separate case, covered below: [the handshake never completes on cellular](#no-hs-mobile-adv). Case does matter for `I1`-`I5`: uppercase only. 2. Compare versions on both ends: `awg --version`. A client built separately (for example `amneziawg-tools` from the AUR on Arch) is often older than the server's generation - then the server expects one envelope while the client sends another. See [Client Compatibility](#client-compat-adv) for the list of compatible clients. The specific case (an AWG 2.0 server with `S3`/`S4` > 0 and an old AWG 1.0 client) is a known upstream issue, covered in the [FAQ](#faq-advanced-adv).
The handshake never completes on cellular, while the same profile works on Wi-Fi **Symptom.** On a cellular network the client hangs at "connecting" forever and no traffic flows. The very same profile, server and port come up and work fully over Wi-Fi. 🔴 **From the server side this looks like a working connection, and that is the trap.** In `awg show` the peer IS there: `endpoint` switches to the client's cellular address and the received byte counter grows. Exactly one field never updates: `latest handshake`. **Why.** A transport packet travels alone and gets through. A handshake packet does not: ahead of it goes a burst of junk packets (`Jc` of them) and the concealment packet `I1`. If the network drops that burst, the handshake goes down with it. Individual transport packets from the client still reach the server, which updates `endpoint` from them, and that is where the misleading picture comes from. **Telling it apart from neighbouring causes.** All three read as "no connection", but they look different: | What the server shows | Cause | |---|---| | no packets from the client at all | wrong port, firewall, address-based block | | packets arrive, `latest handshake` empty, `endpoint` does NOT change | `S1`-`S4`, `H1`-`H4` or (on a 3.1 server) `HeaderProtectionKey` mismatch, or the client does not understand 3.1 (see the previous entry) | | **packets arrive, `endpoint` changes, counter grows, `latest handshake` empty** | **this case: the burst is filtered in transit** | **How to confirm.** On the server, look at the peer and take a capture: ```bash awg show awg0 | grep -A5 '' tcpdump -i any -n "udp port and host " ``` If the capture shows packets from the client and replies from the server, yet `latest handshake` has not moved after a minute of attempts, this is it. **Cause and what to do.** Installations made before September 2026 carry the concealment packet as `I1 = `, that is N random bytes, and some cellular networks drop such a packet. Since September 2026 the installer writes a DNS-reply-shaped packet instead, and on the measured route a fresh install no longer reproduces the problem; what needs fixing is what was deployed earlier. ⚠️ This is not a promise for every network: for the carriers that the table above wants `I1` absent for, a fresh install keeps the problem. Ways out, best first: 1. **Replace `I1` with a packet that has a meaningful shape.** A string like `` imitates a DNS response: two random bytes are the transaction id, then the header and a record. A ready-made QUIC string comes from the generator in [AS-based blocking and the I1/CPS workaround](#as-blocking-adv); it fits here too, because what decides is the packet's shape, not its size. A string of this kind - DNS-shaped rather than QUIC-shaped - is what the installer itself emits from September 2026 onwards, so you can take one from a fresh install. `I1` does not have to match between the peers, and concealment packets are sent by the handshake initiator - so for a single client an edit in its own `.conf` is enough and the server can be left alone. For every client at once, reinstall with `--i1=''`, then run `regen` and re-import the profiles on the devices. 2. **Drop `I1` entirely** by reinstalling with `--no-cps`. Blunter: the handshake loses its disguise along with the problem. 3. Lowering `Jc`, `Jmin`, `Jmax` **does not help** - see the measurement below. **Measurement (September 2026, MTS Moscow, server in the US).** Exactly one parameter changed at a time, everything else identical across runs: | `I1` | `Jc` | `Jmin`-`Jmax` | Handshake | |---|---|---|---| | `` | 3 | 40-90 | **yes** | | `` | 6 | 40-90 | **yes** | | `` | 6 | 89-339 | **yes** | | `` | 6 | 89-339 | no | | `` | 6 | 89-339 | no | | `` | 6 | 89-339 | no | | `` | 6 | 89-339 | no | | **DNS-shaped, 128 bytes** | 6 | 89-339 | **yes** | The last two rows are the deciding pair: **the same 128 bytes, different content.** The random one does not pass, the DNS-shaped one does. So it is neither the packet size nor the number of junk packets: `Jc = 6` and junk up to 339 bytes pass perfectly well. ⚠️ **Limits of the measurement.** One carrier (MTS, Moscow), one route (Russia to the US), one device. One alternative shape was tried, DNS. Whether it is the DNS shape specifically or any non-random structure was not separated. Other carriers and other routes may behave differently: on the Kazakhstan route, by our earlier measurements, even the full burst passes.
Port is occupied by another process 1. Identify the process: `ss -lunp | grep :` 2. Change the AmneziaWG port or stop the conflicting service 3. For port change instructions, see the FAQ "How do I change the port"
Install aborts at step 6: "Failed to detect network interface" Step 6 detects the primary network interface (for NAT/MASQUERADE) via a chain: `ip route get 1.1.1.1`, the default IPv4 route, the first global-IPv4 interface, the default IPv6 route. If every method comes back empty, the provider blocks or null-routes `1.1.1.1`, policy-routing is in use, or egress is IPv6-only (seen on Ubuntu 26.04 / Timeweb). Set the interface manually and re-run the install: 1. List interface names: `ip -br link` (for example `eth0`, `ens3`) 2. Re-run the install with the interface on the same command line: `sudo AWG_MAIN_NIC=ens3 bash install_amneziawg_en.sh` - the value is picked up at step 6. A separate `export AWG_MAIN_NIC=...` + `sudo bash ...` does not work: sudo resets the environment (if you are already root, a plain `export` works) The value is validated (an existing interface with no special characters); on a typo the installer warns in the log and falls back to auto-detection.
--- ## 📊 Traffic Statistics (stats) The `stats` command displays per-client traffic statistics. **Standard output:** ```bash sudo bash /root/awg/manage_amneziawg.sh stats ``` ``` [2026-09-26 14:05:02] INFO: Client traffic statistics: Name | IP | Received | Sent | Last handshake | Status ----------------------------------------------------------------------------------------------- my_phone | 10.9.9.2 | 1.24 GiB | 356.69 MiB | 2026-09-26 14:02:11 | Active my_laptop | 10.9.9.3 | 892.30 MiB | 128.40 MiB | 2026-09-26 14:04:30 | Active guest | 10.9.9.4 | 0 B | 0 B | never | Inactive [2026-09-26 14:05:02] INFO: Total: Received 2.11 GiB, Sent 485.09 MiB ``` **JSON output:** ```bash sudo bash /root/awg/manage_amneziawg.sh stats --json ``` ```json [ { "name": "my_phone", "ip": "10.9.9.2", "rx": 1332477952, "tx": 374083174, "last_handshake": 1710312180, "status": "Active", "status_code": "active" } ] ``` > **Note:** the `status` field is localized (EN `Active` / `Recent` / `Inactive`, RU `Активен` / `Недавно` / `Неактивен`) and is meant for humans. For automation, use the machine-stable `status_code` field - it does not depend on the script language and takes values from a fixed set: `active` (handshake < 3 min), `recent` (< 24 h), `inactive` (stats: no or stale handshake), `no_handshake` (list: no or stale handshake), `key_error` (list: client key not found in the server config), `no_data` (list: not enough data). The `status_code` field is present in both `list --json` and `stats --json`. --- ## ⏳ Temporary Clients (--expires) Create clients with automatic removal after expiration. **Creating:** ```bash sudo bash /root/awg/manage_amneziawg.sh add guest --expires=7d ``` **Duration formats:** | Format | Description | |--------|-------------| | `1h` | 1 hour | | `12h` | 12 hours | | `1d` | 1 day | | `7d` | 7 days | | `30d` | 30 days | | `4w` | 4 weeks | **How it works:** 1. When a client is created with `--expires`, an expiration timestamp is saved to `/root/awg/expiry/`. 2. A cron job `/etc/cron.d/awg-expiry` checks every 5 minutes. 3. Expired clients are automatically removed (config, keys, server config entry). 4. When the last expiry client is removed, the cron job is automatically cleaned up. **Checking:** `list -v` shows remaining time for each client with an expiry. **Machine-readable:** `list --json` returns two fields in every record. `expires_at` is unix time as a number, or `null` for a permanent client; the semantics match the `add --json` success record in `results[]`. `expires_at_error` is `null` in the normal case. An external consumer (GUI, bot, monitoring) does not have to read `/root/awg/expiry/` over SSH. ```bash sudo bash /root/awg/manage_amneziawg.sh list --json ``` ```json [ {"name":"guest","ip":"10.9.9.4","client_ipv6":"","status":"Active","status_code":"active","expires_at":1788912000,"expires_at_error":null}, {"name":"pc","ip":"10.9.9.3","client_ipv6":"","status":"Recent","status_code":"recent","expires_at":null,"expires_at_error":null} ] ``` > The `expires_at` value is a canonical decimal of at most 15 digits. A leading zero, an empty > marker, junk and an over-long number do not count as a value: JSON forbids numbers with a > leading zero, and a number beyond int64 is rejected by Go and .NET parsers, so one corrupted > file would break the parsing of the whole document. > When the marker EXISTS on the server but no value could be obtained from it, `expires_at` stays > `null` and `expires_at_error` becomes `"unreadable"`. That covers an empty file, corrupted > content, a directory in place of the file, and a failed read. Note that a PERMISSION denial is not one of them in practice: the script runs as root > and root bypasses permission checks - measured on a stand, `chmod 000` on a marker did not hide > its value. What remains is an I/O error, a security module refusing the read, and a truncated > read. On its own, `expires_at: null` would read as "permanent by design", while an > expiry was in fact set for that client. The reason goes to the log: a JSON consumer needs one > question answered, whether the value can be trusted, and it is the administrator who deals with > the particular file. The expiry check does not remove a client whose marker looks like this. --- ## 📱 vpn:// URI Import When a client is created, a `.vpnuri` file holding a `vpn://` URI is written too and, since v5.11.3, a QR code `.vpnuri.png` encoding the same URI - for import into the Amnezia VPN app (Android / iOS / Desktop). Option 2, pasting the link, is the most reliable: the app's built-in scanner does not always cope with this code. **File locations:** - `/root/awg/.vpnuri` — plain-text `vpn://` URI - `/root/awg/.vpnuri.png` — QR code of that URI (since v5.11.3) **Format:** the configuration is compressed via zlib (Perl `Compress::Zlib`) and Base64-encoded, forming a URI like `vpn://...`. > Perl with `Compress::Zlib` and `MIME::Base64` modules must be present on the server. On Ubuntu and Debian they are installed by default. If Perl is absent, neither `.vpnuri` nor `.vpnuri.png` is created, but `.conf` files work as usual. `qrencode` (already required for `.conf` QR) is also used for `.vpnuri.png`. **Option 1 - QR code:** 1. Copy `/root/awg/.vpnuri.png` to your computer (`scp`) or open it locally. 2. In the Amnezia VPN app on the phone: "Add VPN" → "Scan QR code". 3. Point the camera at `.vpnuri.png`. If the scanner recognizes the code, the client imports automatically; if not, see the paragraph below. The code is dense: with our profile the link is about 1100 characters long. On Android, the built-in scanner of Amnezia VPN 5.0.3 did not recognize it (checked 13 October 2026), while the stock camera of our test phone (Samsung) read it at once. In that case, read the code with the phone's camera, copy the `vpn://...` text and paste it into Amnezia VPN as in option 2. **Option 2 - URI paste:** 1. Copy the contents of the `.vpnuri` file. 2. Open Amnezia VPN → "Add VPN" → "Paste from clipboard". 3. The configuration is imported automatically. > Alongside sits `.png` - the QR of `.conf` for AmneziaWG clients that can import by QR code; AmneziaWG for Windows and `awg-quick` take the same `.conf` as a file, and a plain WireGuard client cannot import it. The two formats target different clients: the Amnezia VPN app scans `.vpnuri.png`, AmneziaWG clients with QR import scan `.png`. Do not mix them up. > For existing clients created before v5.11.3, `.vpnuri.png` appears after one `manage regen `. New clients get both QR codes out of the box. **Permissions:** `.vpnuri` and `.vpnuri.png` have 600 permissions (root only). --- ## 📱 MTU and Mobile Clients Starting with v5.7.4, `MTU = 1280` is set automatically in both server and client configs. **Why:** Cellular networks (4G/LTE) often have an effective MTU below the default 1420, causing packet fragmentation or drops. iOS is strict about Path MTU Discovery and may fail to connect. 1280 is the minimum IPv6 MTU (RFC 8200), guaranteed to pass through any network. The speed impact is negligible. **Keep 1280 with the 3.1 profile.** The 3.1 profile adds `ContentPaddingAddition` padding to data packets (up to 128 bytes in ours), so the size headroom matters no less than before. Raise the MTU (say, to the WireGuard default of 1420) only after measuring the PMTU on each client's path, and keep the server's MTU no larger than the clients': in [#3089](https://github.com/amnezia-vpn/amnezia-client/issues/3089) a server with 1420 sent packets larger than a client interface with 1376 accepted, and they were lost, and in [#3274](https://github.com/amnezia-vpn/amnezia-client/issues/3274) (server 1420, client 1200) setting 1280 on both sides cut the loss considerably. This is a recommendation, not a proven ceiling. **For installations before v5.7.4:** Add `MTU = 1280` to the `[Interface]` section of both server and client configs manually. Restart the service: ```bash sudo systemctl restart awg-quick@awg0 ``` > The vpn:// link carries the MTU from the client `.conf`, 1280 by default. ### Automatic MSS clamp (since v5.17.0) Starting with v5.17.0 the server additionally clamps the TCP MSS to the tunnel size. This is a `TCPMSS` rule in the `mangle` table's `FORWARD` chain, added as separate commands in the `PostUp`/`PostDown` of `awg0.conf`. The value is derived from `MTU` (1280 by default): MSS 1240 for IPv4 and 1220 for IPv6, in both directions. **Why:** even with `MTU = 1280`, large pages and downloads sometimes stall on mobile carriers, behind double-NAT, and in a two-server cascade. The cause is a PMTU blackhole: when ICMP "Fragmentation needed" (or ICMPv6 "Packet Too Big" for IPv6) is filtered along the path, oversized TCP segments with the DF flag are silently dropped at the tunnel, and the connection hangs on large transfers (small requests still go through). The MSS clamp tells both sides a tunnel-safe segment size up front, so those packets never appear. It complements `MTU = 1280` rather than replacing it. The rule is applied automatically on install and reinstall (`--force`) for v5.17.0 and later. It needs no client config regeneration - it lives on the server and applies to every client. The MSS value is derived from `MTU` when the config is generated; if you change `MTU` manually after install, re-run the installer with `--force` without the `--preset`, `--jc`, `--jmin`, `--jmax` flags (with them the MTU comes from `awgsetup_cfg.init` and the manual edit is rolled back), or adjust the rules in `PostUp` and `PostDown` as described in the FAQ "How do I change the MTU?". > The MSS clamp only affects TCP. Video and QUIC/HTTP3 (UDP) are untouched: if that is the traffic that stalls, the cause is elsewhere - see the obfuscation parameters and carrier presets. ### Carrier blocks the port (no connection) If the tunnel does not come up at all on cellular (the handshake never completes) while it works fine on Wi-Fi, there are TWO known causes, and one look at the server tells them apart. **Cause one: the port.** By default the server listens on UDP 39743; some carriers drop that non-standard UDP port but reliably pass `443/udp` - it looks like QUIC/HTTP3. `--preset=mobile` does not help here: it changes the traffic disguise, not the port. The tell: the server sees **nothing at all** - `tcpdump` on your port is silent and there is no peer activity in `awg show`. **Cause two: the shape of the `I1` concealment packet.** The tell is the opposite: packets from the client **do arrive**, the peer's `endpoint` updates, the received counter grows, and `latest handshake` never moves. Analysis and measurement: [the handshake never completes on cellular](#no-hs-mobile-adv). ⚠️ Do not assume the port by default. In our September 2026 measurement on MTS (Moscow) a non-standard UDP port passed freely and the cause was entirely `I1`. Look at the server first, change the port second. Set the port at install time - `sudo bash install_amneziawg_en.sh --port=443 ...`. On an already running server, change the port without reinstalling: ```bash sudo sed -i 's/^ListenPort = .*/ListenPort = 443/' /etc/amnezia/amneziawg/awg0.conf sudo sed -i 's/^export AWG_PORT=.*/export AWG_PORT=443/' /root/awg/awgsetup_cfg.init sudo ufw allow 443/udp sudo systemctl restart awg-quick@awg0 ``` Then re-issue the client configs (`sudo bash /root/awg/manage_amneziawg.sh regen `) - the new port lands in `Endpoint` - and re-import them on the devices. Port 443 also works on regular networks, so you can move all devices to it. > If, on the other hand, the tunnel comes up but some sites (e.g. YouTube) do not open while the VPN is connected, the cause is usually not the port but the device's IPv6 going around the tunnel. Since v5.31.0 that applies to profiles issued earlier and to your own network list (`--route-custom`): a full tunnel, the default mode included, takes IPv6 in, so this cause no longer applies to it. See the [IPv6 in IPv4-only mode](#ipv6-tunnel-adv) section for the explanation and fix. --- ## 🚧 Host Unreachable from Russia (Hetzner and others): AS-based Blocking and the I1/CPS Workaround **Symptom.** A VPN server on Hetzner behaves like this: the handshake completes, the client shows a recent handshake and receives a few kilobytes, after which the flow stops. The in-tunnel ping to the server (`10.x.x.x`) goes to 100% loss, incoming traffic freezes, and new handshakes never complete. It looks like the server died, even though it is alive and reachable over SSH. **Mechanism.** It looks like this is not a per-IP block but the server address landing in an autonomous system (AS) that is not on the allowlist. According to DPI researcher 0ka ([Habr, article 997088](https://habr.com/ru/articles/997088/), in Russian), the filtering relies on an allowlist of roughly 72 ASes; it excludes, for example, Hetzner `AS24940` as well as ranges of OVH, DigitalOcean, AWS and Cloudflare, and traffic to them is degraded at the carrier (TSPU) level. The key point: ordinary junk parameters (`Jc`/`Jmin`/`Jmax`) do not help here, because they change the packet signature rather than the destination AS. In our tests, what got through was an `I1`/CPS packet disguised as a QUIC handshake to an allowlisted SNI. ⚠️ **Refinement from the September 2026 measurement: AS-based blocking alone does not explain it.** The same effect reproduced against a server in the US that has nothing to do with Hetzner: an `I1` concealment packet made of random bytes did not get through, while a packet of the same length shaped like DNS did. So the filter reacts to the **shape of the packet itself**, not only to the destination address, and swapping `I1` helps for a reason other than dodging an AS list. The detailed measurement is in [the handshake never completes on cellular](#no-hs-mobile-adv). The SNI is chosen per hoster; for Hetzner, `7-zip.org` worked. **Field test (June 2026).** The recipe was verified live on a clean Hetzner server (AS24940) from a Russian client across three Moscow ISPs, comparing the then-default configuration (generic `I1 = `) against the QUIC mimicry (`I1` generated for SNI `7-zip.org`). | ISP | Default (generic I1) | QUIC I1 (SNI 7-zip.org) | |-----|----------------------|--------------------------| | Rostelecom | blocked (handshake, then silence) | **access restored**: 0% loss, real web through the tunnel | | ecotelecom | blocked | **access restored**: 0% loss | | Seven Sky | blocked | **no effect** (two SNIs tested: `7-zip.org` and `www.google.com`) | Control: a tunnel to a server in a different AS (US) came up and held on all three ISPs, so in our test on Seven Sky it was Hetzner specifically that stayed unreachable, not VPN as such. On Seven Sky the block reproduced consistently, both evening and night, and the QUIC mimicry with two different SNIs did not lift it. In summary: 0ka's method does work, but not universally - the outcome depends on the ISP and the regional TSPU configuration. **How to apply manually.** The installer does not generate the QUIC mimicry `I1` on its own - this is a manual step: 1. Generate the `I1` string for your hoster in [Mini QUIC Generator](https://sageptr.github.io/mini_quic_generator/) (by SagePtr): enter the SNI (`7-zip.org` for Hetzner) and copy the value from the "AmneziaWG 1.5+ (I1 = )" field. 2. Replace the `I1 = ...` line in the `[Interface]` section of the server config `/etc/amnezia/amneziawg/awg0.conf` (`I1`-`I5` are case-sensitive, uppercase only). 3. Restart the service and regenerate clients - `regen` writes the new `I1` into the client configs from `awg0.conf`, then redistribute the updated `.conf` files: ```bash sudo systemctl restart awg-quick@awg0 sudo bash /root/awg/manage_amneziawg.sh regen ``` 4. If access is not restored, try a different SNI or a different hoster: on some ISPs (like Seven Sky in our test) the mimicry did not pass with any of the SNIs we tried. > Method discussion and SNI selection: [ntc.party #12845](https://ntc.party/t/12845) (in Russian). If your Hetzner server goes silent after the handshake, first check whether this is the cause: bring up a test server with a different hoster (for example, in the US), and if the tunnel there works, the cause is destination-AS blocking. --- ## 🛡️ Active Probing and Obfuscation Without a Proxy **What it is.** Deep packet inspection (DPI) does not always work passively. In active-probing mode it sends crafted or replayed packets to the server's UDP port and treats the endpoint's behavior as one signal: a real service answers as expected, while the absence of an expected cover-service response can raise suspicion (though it is not decisive on its own). DPI also passively inspects the signature of the handshake packets themselves. **What the protocol does on its own (no separate daemon).** AmneziaWG conceals the handshake shape at the protocol level and reduces WireGuard-identifying responses for valid AWG traffic, without standing up extra services: - **Header encryption (the 3.1 profile, the default).** The message type is encrypted with the `HeaderProtectionKey`, a key of its own per install, so the header no longer carries a fixed WireGuard mark for any `H1`-`H4`. Details in [The 3.1 profile](#awg3-not-yet-adv). - **`I1`-`I5` (CPS concealment).** Concealment packets can make the handshake resemble allowed traffic - for example QUIC-like traffic associated with a whitelisted SNI, depending on configuration - so the observable handshake signature, and any response to a valid AWG initiation, look unlike stock WireGuard. It does not actively answer arbitrary QUIC/TLS/DNS probes as a real service. This is the mechanism used by the AS-based-blocking recipe; the concrete SNI selection is in [AS-based blocking and the I1/CPS workaround](#as-blocking-adv). - **A whitelisted port.** Running the VPN on port 443 or 53 (`--port=443`) places it on commonly allowed service ports, which may reduce simple port-based blocking but does not by itself defeat active probing. **An honest boundary: where a separate proxy goes further.** In the most aggressive active-probing scenarios a separate responding proxy (for example wiresock's `amneziawg-proxy`) is objectively stronger: it actually answers a probe as a real service - it speaks QUIC/TLS on 443, answers DNS on 53, handles STUN and SIP. That is a different engineering trade-off: an always-on daemon, an extra port, a Rust build, and for full bidirectional masking their own client. Here the obfuscation is built into the protocol itself and targets common passive and mobile DPI patterns without a separate daemon, an extra port, or a paid client; stronger active-probing resistance is the proxy's role. Which one you want depends on how aggressive the probing is on your network. > A comparison with the official Amnezia app and other tools is on the [comparison page](https://bivlked.github.io/amneziawg-installer/compare/). --- ## 📋 Client Compatibility A client has to understand your server's generation: a new install gives the 3.1 profile by default, and with `--protocol=2.0`, on a kernel older than 6.7 (a stock Debian 12) and on ARM the 2.0 profile. The installer prints the server generation in the final report, on the "Protocol generation" line, and later `manage check` shows it. The table below is a short list with our notes; the full table with versions and sources is in [Which AmneziaWG generation a client speaks](#client-gen-adv). | Client | Platform | AWG 3.1 | AWG 2.0 | Notes | |--------|----------|---------|---------|-------| | [Amnezia VPN](https://github.com/amnezia-vpn/amnezia-client/releases) | Windows, macOS, Linux, Android, iOS | ✅ (>= 5.0.1.5) | ✅ (>= 4.8.12.7) | Supports vpn:// URI import. On Android, after Wi-Fi was switched off the tunnel did not recover, even with the Connect button; an app restart helped (5.0.3, check of 13 Oct 2026). Not in the Russian App Store | | [AmneziaWG](https://github.com/amnezia-vpn/amneziawg-android) | Android | ✅ (Google Play >= 3.1.4, APK >= v3.1.20260814) | ✅ (>= 2.0.0) | Recommended for Android. Lightweight tunnel manager. Import via `.conf` and QR | | [AmneziaWG](https://apps.apple.com/app/amneziawg/id6478942365) | iOS, macOS | ✅ (>= 3.1.4) | ✅ | Lightweight client, available in the Russian App Store | | [AmneziaWG](https://github.com/amnezia-vpn/amneziawg-windows-client/releases) | Windows | ✅ (>= 3.1.0) | ✅ (>= 2.0.0) | Lightweight tunnel manager. Import via `.conf` | | [WG Tunnel](https://github.com/wgtunnel/android) | Android | ✅ (>= 5.6.0, better >= 5.7.5) | ✅ | Recommended for Android. FOSS client with auto-tunneling and split tunneling; Google Play, IzzyOnDroid | | [WG Tunnel Desktop](https://github.com/wgtunnel/desktop) | Windows, Linux, macOS | ✅ (v2 line) | ✅ | Same engine as the Android version | | `awg-quick` ([amneziawg-tools](https://github.com/amnezia-vpn/amneziawg-tools) + a module or amneziawg-go) | Linux | ✅ (third-line module and tools) | ✅ | For servers and machines without a desktop, see [Connecting a Linux machine as a client](#linux-client-adv) | | [WireSock VPN Client](https://www.ntkernel.com) | Windows | not claimed | ❌ (no `S3`/`S4`) | Commercial, its own engine. By the vendor's documentation (wiresock.net, October 2026) `I1`-`I5` are silently dropped and `S3`/`S4` are not supported; both our profiles set `S3`/`S4`, so do not expect it to connect | | Hiddify | all | ❌ | - | Its engine is below the third line: for Hiddify install the server with `--protocol=2.0` | | Standard WireGuard | All | ❌ | ❌ | Does not support AWG parameters | > If a client shows an error about an unknown parameter (`HeaderProtectionKey`, `ContentPaddingAddition`, S3, S4, I1, or H1 as a range), it does not understand the server generation: update it or take a client from the first rows of the table. A client that does not know 3.1 may show no error at all - then there is simply no handshake. ### Which AmneziaWG generation a client speaks Recorded 15 October 2026 from the release pages, the app stores and the projects' code, and you can check the same way. Clients move every few days, so the rows carry dates. "3.1 since" comes from the projects' own release notes, pages and code, "Basis" says how the generation is known. Rows checked later carry their own date in the "Basis" column. | Client | 3.1 | Version on 15 Oct 2026 | 3.1 since | Where to get it | Basis | |---|---|---|---|---|---| | Amnezia VPN (flagship, all platforms) | yes | 5.0.3.0 (GitHub), 5.0.3.1 (Google Play) | 5.0.1.5 (21 Aug 2026) | GitHub, app stores; not in the Russian App Store; on Windows use the direct link from the releases page | project pages | | AmneziaWG for Android (light) | yes | 3.1.4, Google Play (21 Sep 2026) | 3.1.4 on Google Play, v3.1.20260814 as APK | Google Play, releases page | project pages | | AmneziaWG for iOS and macOS (light) | yes | 3.1.4 (24 Aug 2026) | 3.1.4 | App Store, the Russian one included | project pages | | DefaultVPN (iOS, by the Amnezia authors) | yes | 2.0.2 (21 Sep 2026) | 2.0.1 (24 Aug 2026) | App Store, the Russian one included | project pages, 21 Oct 2026 | | AmneziaWG for Windows (light) | yes | 3.1.0 (21 Aug 2026) | 3.1.0 | project releases page | project pages | | AmneziaWG for Windows 7 and 8 (RomikB build, third-party) | yes | v3.1.1 (28 Aug 2026) | v3.1.1 | project releases page, files with windows7 in the name | project code | | WG Tunnel (Android, third-party) | yes | 5.8.2 (19 Oct 2026) | 5.6.0 | Google Play, IzzyOnDroid, APK | project code, 21 Oct 2026 | | WG Tunnel Desktop (third-party) | yes | v2.4.1 (17 Oct 2026) | 2.0.0 (13 Sep 2026) | project releases page, choco, apt, AUR | project code, 21 Oct 2026 | | mihomo (core for Clash Verge Rev and others) | yes | v1.19.32 (30 Sep 2026) | v1.19.30; 3.1 is on only with `version: 3` in `amnezia-wg-option` | project releases page | project code | | FlClash (Android, Windows, macOS, Linux, third-party) | yes | v0.9.1 (17 Oct 2026) | v0.8.97 (10 Sep 2026); mihomo core: needs `version: 3` in `amnezia-wg-option` | project releases page | project code, 21 Oct 2026 | | Throne (Windows, Linux, macOS, third-party) | yes | 1.3.2 (29 Sep 2026) | 1.3.0 (17 Sep 2026); in 1.3.x the padding of some packets is cut to the room left in the buffer; fixed in pre-releases 1.4.0-beta.1 to beta.3 | project releases page | project code, 21 Oct 2026 | | L×Box (Android, third-party) | yes | v2.25.16 (19 Oct 2026) | v2.23.0 (5 Sep 2026) | project releases page | project code, 21 Oct 2026 | | ZedSecure (Android, third-party) | yes | v3.1.6 (13 Oct 2026) | - | project releases page | project code | | network-manager-amneziawg (Linux, third-party) | yes | v0.9.12 (13 Oct 2026) | v0.9.12; no engine of its own: needs a third-line kernel module, and in external mode third-line awg tools as well | project releases page | project pages | | gluetun (Docker, third-party) | in the daily feed, no tag | latest image from master; tag v3.41.3 has no AmneziaWG | master (26 Sep 2026); a 3.0 engine: it does not read RandomTrailers and DisableCookies lines, and this installer's profile does not write them | Docker Hub, tag latest | project code | | Hiddify | no | 4.1.1 (5 Mar 2026) | - | - | project code | | KeeneticOS (stock firmware) | development channel only | 5.2 Beta 1 (19 Oct 2026) | 5.2 Alpha 11 (25 Sep 2026); not in stable 5.1 | Keenetic development channel | project pages, 21 Oct 2026 | | AWG Manager (Keenetic, third-party) | yes | v2.19.19 (20 Oct 2026) | with the stock module on KeeneticOS 5.2 Alpha 11 and newer; on other firmware with its own kernel module or its bundled sing-box (needs Entware and the WireGuard component) | project releases page | project code, 21 Oct 2026 | | OpenWrt (official packages) | no | no package; openwrt/packages PRs #30492 and #30493 are open, without maintainer approval | - | - | project pages | | awg-openwrt (OpenWrt, third-party) | yes | v25.12.6, v24.10.8 | v25.12.5, v24.10.8 (22 Sep 2026); the packages of these releases were replaced under the same tags; ones downloaded earlier and packages for older OpenWrt have no 3.1 | project releases page, project feed | project code, 21 Oct 2026 | | GL.iNet (stock firmware) | no | AmneziaWG 2.0 since firmware 4.9; no date for 3.x | - | - | project pages | | amneziawg-mikrotik-c (MikroTik, third-party) | yes | v1.4.3 (8 Oct 2026) | - | project releases page | project pages | | AmneziaWG-MikroTik (third-party) | yes | Containers_3.1 (30 Aug 2026) | Containers_3.1 | project releases page | project pages | - **sing-box-extended** speaks 3.1 since v1.14.0-extended-2.7.0 (4 September 2026) but crashes with `ContentPaddingAddition` above 16 in the device's own config and traffic from TUN ([issue #158](https://github.com/shtorm-7/sing-box-extended/issues/158), open as of 15 October 2026), while our 3.1 client profile carries `32-128`. It is the client's padding that matters, not the server's: the parameter is one-sided, so in that device's profile the line can be lowered to 16 or removed, and the server does not need to change. On OpenWrt this build runs AmneziaWG in tachyon (the successor of forkop) when the extended variant is chosen; podkop takes AmneziaWG from the system interface and is not affected, and forkop does not build AmneziaWG tunnels itself. NyameBox moved to this build on its master branch only, its releases do not have it. Throne does not crash: in 1.3.x the padding is cut (see the table), and the 1.4.0 pre-releases reserve room for it in the buffer. - **amneziawg-mikrotik-c** (v1.4.3) accepts `ContentPaddingAddition` but does not apply it to outgoing packets, and says so in its log. The connection is not affected, there is simply no added padding. - **amneziawg-mikrotik-c** before v1.4.2 did not send `I1`-`I5` with `Jc = 0`, so a profile made with `--jc=0` lost its concealment packet there. - **Karing** is not recommended: its release notes do not mention AmneziaWG support. - **Windows 11 26H2:** according to a single report ([amnezia-client#3277](https://github.com/amnezia-vpn/amnezia-client/issues/3277)) the AmneziaWG built into Amnezia VPN 5.0.3.0 passes no packets there (error `WSAEFAULT`), while the light AmneziaWG for Windows 3.1.0 works. > ⚠️ For third-party projects the generation comes from their code and their authors' notes, for > the official ones from release notes and app stores. This table has not been verified by > behaviour on a generated 3.1 profile. Separately, on 13 October 2026 AmneziaWG for Android 3.1.4, > WG Tunnel 5.8.1 and Amnezia VPN 5.0.3 on Android and AmneziaWG 3.1.0 on Windows connected to a > server with a 3.1 profile from this installer; the results are in the FAQ entries > on the delayed first request and on choosing a client. ### Router Clients | Project | Platform | Description | |---------|----------|-------------| | [AWG Manager](https://github.com/hoaxisr/awg-manager) | Keenetic (Entware) | Web interface for managing AWG tunnels on Keenetic routers; the third line with the stock module on KeeneticOS 5.2 Alpha 11 and later, on other firmware with its own kernel module or its bundled sing-box | | [awg-openwrt](https://github.com/Slava-Shchipunov/awg-openwrt) | OpenWrt | kmod/tools/luci packages; the third line is in the packages for OpenWrt 25.12.5 and 24.10.8 (replaced on 22 September 2026). Official OpenWrt packages have no AmneziaWG, and the patch in the `amnezia-vpn/amneziawg-openwrt` repository is, by our reading, AWG 1.0 level (no `S3`/`S4`, no `I1`-`I5`), so it does not work with our profiles | | [AmneziaWG for Merlin](https://github.com/r0otx/asuswrt-merlin-amneziawg) | ASUS (Asuswrt-Merlin) | AWG 2.0 addon with web UI, GeoIP/GeoSite routing; the third line was not checked by us | | [amneziawg-mikrotik-c](https://github.com/timbrs/amneziawg-mikrotik-c) | MikroTik (RouterOS Container) | Docker container bridging MikroTik's native WireGuard to AmneziaWG | > **Keenetic with stock firmware.** The stable Keenetic firmware (5.1 as of October 2026) understands only the second line, so for it the server is installed with `--protocol=2.0` (see [Does my router speak third-line AmneziaWG?](#faq-router-adv)). Older firmware (4.x, 5.0) has a built-in AmneziaWG client too, but not all of it understands `H1`-`H4` ranges: 5.0.6 on the Speedster, for example, does not, see the note below. If the tunnel connects but traffic doesn't flow - the issue is the I1 format. Working options: `I1 = ` or the DNS-mimicking pattern `I1 = `. `I1` does not have to match the server, so changing it in the router's `.conf` is enough; if you change it in the server config, follow with `sudo systemctl restart awg-quick@awg0` + `manage regen `. [Discussion #45](https://github.com/bivlked/amneziawg-installer/discussions/45). > > 🔴 **The `` prefix in this pattern is mandatory - do not copy the block without it.** Those two random bytes are the DNS transaction ID that every packet of the protocol starts with. Without them the whole payload shifts by two bytes and the packet does not parse as DNS at all: the ID field receives `0x8580`, the flags become `0x0001` - the packet declares itself a query rather than a reply - the additional-records counter grows to 1641, and the first name label claims 99 bytes with 29 remaining. And a second reason: without the random prefix all 42 bytes are constant, so the packet comes out byte-for-byte identical for everyone who copied the recipe from here. ⚠️ The prefix only makes the packets differ from one another; the constant 42-byte block inside stays and remains usable as a signature to search for. > **Keenetic Speedster (firmware 5.0.6), AmneziaWG won't connect:** older firmware does not yet parse H1-H4 as ranges (`lower-upper`) and raises an `invalid H1` error. Set H1-H4 to concrete numbers - the other obfuscation layers (Jc/Jmin/Jmax, I1, S1-S4) keep working. If the handshake clears but no traffic flows - lower the junk (`Jc=3`, `Jmin=10`, `Jmax=50`), and if needed remove the `I1` line and zero out `S3`/`S4`. The third-party [AWG Manager](https://github.com/hoaxisr/awg-manager) (a web interface for AmneziaWG tunnels on Keenetic, on the kernel module) works around some firmware limits; it gives the third line even on firmware without it, through its own kernel module or its bundled sing-box. [Discussion #81](https://github.com/bivlked/amneziawg-installer/discussions/81). --- ## 🐧 Debian Support **In short.** Debian 13 (trixie) on x86_64 gets AmneziaWG 3.1, like Ubuntu (the caveat about 7.0.0-38 kernels is in [Known limitations](#kernel-70-backport-adv)). On a stock Debian 12 (bookworm, kernel 6.1) the installer sets up 2.0 by itself: it issues the 3.1 profile only on kernel 6.7 or newer. For 3.1, take Debian 13 or Ubuntu 24.04/26.04, or boot Debian 12 into a 6.7+ kernel from backports (details below). The installer fully supports Debian 12 (bookworm) and Debian 13 (trixie). That is support from the INSTALLER side: Debian 12 itself left regular support on 2026-07-11 and now gets updates only through Debian LTS, until 2028-06-30, so prefer Debian 13 for a new server. **Ubuntu vs Debian differences:** | Aspect | Ubuntu 24.04 | Debian 12 (bookworm) | Debian 13 (trixie) | |--------|-------------|---------------------|-------------------| | PPA codename | native | mapped to `focal` | mapped to `noble` | | APT format | DEB822 `.sources` | `.list` | DEB822 `.sources` | | Headers | `linux-headers-$(uname -r)` | fallback to `linux-headers-amd64` | fallback to `linux-headers-amd64` | | snapd/lxd cleanup | Yes | Skipped | Skipped | **Debian prerequisites:** Minimal Debian installations do not include `curl`: ```bash apt-get update && apt-get install -y curl ``` **Expected warnings:** During installation on Debian, you may see a `sudo removal refused` warning — this is normal, as Debian uses `sudo` as a system package and the script correctly skips its removal. ### AmneziaWG 3.0 and kernels older than 6.7 (Debian 12) - v5.23.0 In late July 2026 the Amnezia team released **AmneziaWG 3.0** and switched the `ppa:amnezia/ppa` PPA to it. At first the 3.0 module did not build on kernels older than 6.7: it called `nla_put_uint`, which Linux only has since 6.7, and on **Debian 12 (bookworm) with kernel 6.1** the install died on `implicit declaration of function 'nla_put_uint'`. The developers fixed that on 31 July ([issue #204](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/issues/204)): in module version `v3.0.20260731-04` the build on kernel 6.1 goes through, verified on Debian 12 with kernel 6.1.0-51. The installer still keeps the **pinned last AmneziaWG 2.0 module** (tag `v1.0.20260725`, verified by commit hash) on such kernels: starting with **v5.23.0**, if the kernel is older than 6.7, the module is not taken from the PPA but built from source via DKMS. That is now a deliberate choice rather than a way around a broken build: in its first days the 3.0 line managed to break and then fix the build on old kernels specifically, so that is where it is least proven. The `amneziawg-tools` userland still comes from the PPA - the 3.0 tools work correctly with a 2.0 module, that part is verified. You do not need to install anything by hand; it all happens at step 2. On kernels 6.7 and newer (Ubuntu 24.04/26.04, Debian 13 trixie) the behaviour is unchanged - the module is installed from the PPA. If you specifically want the third AmneziaWG line on Debian 12, the simplest route is to deploy the server afresh on Debian 13 / Ubuntu 24.04+. The other route is to install a 6.7+ kernel from `bookworm-backports` and **reboot into it**: the installer looks at the running kernel, not at the installed one, so after the reboot it follows the normal path. On a server that is already set up, do not install on top: back it up (`sudo bash /root/awg/manage_amneziawg.sh backup`), remove the current install (`sudo bash install_amneziawg_en.sh --uninstall`) and install again - otherwise the pinned 2.0 module stays registered in DKMS under the same name as the PPA package. Since version 6.0.0 a new install on kernel 6.7 or newer gets the 3.1 profile by itself, and on the stock 6.1 kernel the installer installs 2.0 automatically, see [Where 2.0 is installed instead of 3.1](#awg3-fallback-adv). --- ## 🔧 Raspberry Pi and ARM64 Support The installer works on ARM systems, but the protocol there is AmneziaWG 2.0: as of v6.0.0 the 3.1 profile is not issued on ARM with any module. This is a deliberate caution until a separate measurement on ARM (see [Where 2.0 is installed instead of 3.1](#awg3-fallback-adv)). Clients of such a server need 2.0 support. An explicit `--protocol=3.1` on ARM stops the install. **Supported platforms:** | Platform | Architecture | Kernel headers package | |----------|-------------|----------------------| | Raspberry Pi 3, 4 (64-bit) | ARM64 (aarch64) | `linux-headers-rpi-v8` | | Raspberry Pi 5 | ARM64 (aarch64) | `linux-headers-rpi-2712` | | Raspberry Pi 3, 4 (32-bit) | ARMv7 (armhf) | `linux-headers-rpi-v7` or `linux-headers-rpi-v7l`, by the kernel suffix in `uname -r` | | Ubuntu ARM64 (AWS Graviton, Oracle Ampere, Hetzner) | ARM64 | `linux-headers-generic` | | Debian ARM64 (cloud VPS) | ARM64 | `linux-headers-arm64` | **How it works:** 1. The installer detects the kernel version and architecture automatically. 2. If a prebuilt `amneziawg.ko` package matching your kernel exists in the [arm-packages release](https://github.com/bivlked/amneziawg-installer/releases/tag/arm-packages), it is downloaded and installed via `dpkg`. This takes 2-3 minutes. 3. If no prebuilt package matches, the installer falls back to DKMS compilation from source. This works on any kernel but takes longer (10-30 min depending on hardware). 4. You can skip the prebuilt package altogether: with `--no-prebuilt` the module is built with DKMS right away. The prebuilt packages are built by this project's CI, and the installer checks them only against a SHA256 file from the same release; they are not signed. Note that this changes the module too: the prebuilt packages are built from the pinned AmneziaWG 2.0 source, while on kernels 6.7 and newer DKMS installs `amneziawg-dkms` from the PPA, the same third-line module an x86 server gets. The client profiles are second generation either way. > **Prebuilt ARM coverage:** prebuilt packages are built for Raspberry Pi (3/4/5), Ubuntu 24.04 ARM64 and Debian 12/13 ARM64. Ubuntu 26.04 ARM64 has no prebuilt yet - the module comes as the `amneziawg-dkms` package from the PPA and is built via DKMS (slower on first install, then works normally). **Raspberry Pi kernel detection:** Raspberry Pi Foundation kernels have a `+rpt` suffix in their version string (e.g. `6.12.75+rpt-rpi-v8`). The installer maps this suffix to the correct headers package. Standard Debian/Ubuntu ARM64 kernels use their default headers. **Troubleshooting:**
Q: Module load fails on Raspberry Pi Check that kernel headers match your running kernel: uname -r vs ls /lib/modules/. If they differ, update your kernel: sudo apt update && sudo apt upgrade, reboot, and re-run the installer.
Q: DKMS compilation takes a very long time on Pi 3 Raspberry Pi 3 has 1 GB RAM and 4 cores at 1.2 GHz. Kernel module compilation can take 20-30 minutes — this is normal. Make sure swap is enabled (the installer configures it automatically).
Q: How do I check if the prebuilt module was used? Look for Prebuilt installed in the install log (/root/awg/install_amneziawg.log); the Russian installer writes Предсобранный пакет установлен. If it is not there, the module was built with DKMS: on the fallback the log says falling back to DKMS build.
--- ## 🐧 Connecting a Linux machine as a client Mobile and desktop clients take the config through the app, a QR code, or a vpn:// URI. To connect an ordinary Linux box as a client (a home server, a second machine, a Linux router), you need a kernel module or a userspace implementation of AmneziaWG that understands the server's generation: for a 3.1 server (the default), the third line (a third-line build works with a 2.0 server too). Plain `wireguard` will not do, it knows nothing about Jc/S/H/I or the header protection key. Two paths. ### 1. Kernel module + tools (Ubuntu / Debian) On the SERVER, issue a client config and grab it: ```bash sudo bash /root/awg/manage_amneziawg.sh add my-linux-box # ready config: /root/awg/my-linux-box.conf ``` On the CLIENT, install the AmneziaWG module and tools. These are the same packages the installer uses, but a client needs no UFW / Fail2Ban / server tuning: ```bash # Ubuntu sudo add-apt-repository -y ppa:amnezia/ppa sudo apt update sudo apt install -y amneziawg-dkms amneziawg-tools linux-headers-$(uname -r) ``` Debian has no separate PPA packages. The installer works around this by remapping the suite to the nearest Ubuntu one (bookworm -> focal, trixie -> noble), but as a manual step on a client that is fragile - for a Debian client prefer userspace `amneziawg-go` (path 2) or build the module from source. Place the config under the name `awg0` and bring the tunnel up: ```bash sudo mkdir -p /etc/amnezia/amneziawg sudo cp my-linux-box.conf /etc/amnezia/amneziawg/awg0.conf sudo chmod 600 /etc/amnezia/amneziawg/awg0.conf sudo awg-quick up awg0 sudo systemctl enable awg-quick@awg0 # start on boot ``` Check: `sudo awg show` shows a `latest handshake`, but that alone is not enough - check the traffic too (why, see [below](#linux-client-loop-adv)). On a full tunnel `curl ifconfig.me` returns the server IP; on a split tunnel verify with traffic to an address covered by `AllowedIPs`. Stop the tunnel with `sudo awg-quick down awg0`. ### 2. amneziawg-go (userspace, no kernel module) If the module cannot be installed (no kernel headers, DKMS blocked, an exotic architecture), the userspace [`amneziawg-go`](https://github.com/amnezia-vpn/amneziawg-go) runs over `/dev/net/tun` on any Linux at the cost of ~30-50% CPU overhead. The client needs `amneziawg-go` plus `amneziawg-tools`. `awg-quick up awg0` picks up the userspace implementation if the `amneziawg-go` binary is in `PATH` (or set `WG_QUICK_USERSPACE_IMPLEMENTATION=/path/to/amneziawg-go`); it needs access to `/dev/net/tun` and `CAP_NET_ADMIN`. Building and running it is covered in [LXC / Docker via amneziawg-go](#lxc-userspace-adv). ### A split route list and the server address Linux `awg-quick` sorts the routes out by itself only for a full tunnel, `0.0.0.0/0`: it sets the mark and the rule that send packets to the server around the tunnel. With a list of routes (mode 2 `--route-amnezia`, mode 3, a list set with `modify`) there is no such rule. If the server address falls inside the list, the encrypted packets to the server are routed into the tunnel itself. The Windows, Android and iOS apps add a route to the server themselves, so they do not fail this way. The sign: `awg show` shows a fresh `latest handshake` (the first packet leaves before the routes), but no data goes through. Received bytes freeze at about 92, sent bytes grow to gigabytes within seconds, `ping` says `No buffer space available`. In this failure a fresh `latest handshake` says nothing about a live tunnel. Since version 6.0.0 the installer and `manage` cut a hole in the list at the server address themselves: the route that holds it is replaced by its complement around that address. Clients issued earlier only need a reissue (`regen`). The one cost: traffic to the server address itself, such as a site on the same server, goes around the tunnel. The hole is made only for an IPv4 address. If `Endpoint` holds a name (its address may change) or IPv6, add the route to the server yourself, before `awg-quick up`: ```bash sudo ip route add /32 via # IPv4 sudo ip -6 route add /128 via # IPv6 ``` `modify AllowedIPs` writes the list as given and warns if it holds the server address. ### Careful on a remote machine (risk of losing SSH) If the Linux client is a remote server you manage over SSH, a full tunnel (`AllowedIPs = 0.0.0.0/0`) pushes all traffic into the tunnel, including your return SSH: the machine starts answering over the VPN, and the current session almost certainly drops. - For a client that only needs part of its traffic in the VPN, give it a split config: `manage_amneziawg.sh modify my-linux-box AllowedIPs "the-subnets-you-need"`. SSH stays on the direct route. - If you do need a full tunnel, before `awg-quick up` add an exception for your admin IP via the original gateway (a policy route), otherwise you lose access to the machine. - Test a full tunnel on a machine with a local console or KVM/IPMI, not blind over SSH. --- ## 📦 LXC / Docker via amneziawg-go (userspace) If the **AmneziaWG kernel module cannot be installed on the host** — provider restriction, shared-kernel LXC without host privileges, or a shared Proxmox running other containers that you don't want to reboot for a DKMS build — there is an alternative: the [`amneziawg-go`](https://github.com/amnezia-vpn/amneziawg-go) userspace implementation. It is a TUN-backed build that does not need a kernel module. Throughput is lower than kernel-native (~30–50% CPU overhead at 1 Gbps), but it runs on any Linux with `/dev/net/tun`. My installer `install_amneziawg.sh` **does not cover this path** — I deliberately ship kernel-native for performance and to avoid pulling a Go compiler onto a production server. This section describes a manual setup on top of a running Debian / Ubuntu LXC. ### ⚠️ Security tradeoff Running `amneziawg-go` inside LXC usually requires: - A **privileged container** (root mapped to host) or an `unprivileged` one with `/dev/net/tun` passthrough and `CAP_NET_ADMIN` — both give the container broad access to the host network stack. - **Nesting** (`features: nesting=1` on Proxmox) — needed for `iptables` inside the container. On Proxmox 9 with Debian 13 LXC (systemd 257+) Proxmox itself warns at container creation: `WARN: Systemd 257 detected. You may need to enable nesting.` — without nesting the container also stalls at the systemd level. - **tun passthrough** via `lxc.cgroup2.devices.allow` plus a bind-mount of `/dev/net`. If container isolation is critical (multi-tenant host, privacy-sensitive workload), use a KVM/QEMU VM with the regular installer instead of LXC. ### LXC host requirements **Proxmox LXC config** (`/etc/pve/lxc/.conf`): ```conf features: nesting=1 lxc.cgroup2.devices.allow: c 10:200 rwm lxc.mount.entry: /dev/net dev/net none bind,create=dir ``` Restart the container after editing the config: `pct stop && pct start `. **Forwarding inside the container:** ```bash echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-awg-forwarding.conf sudo sysctl --system ``` ### Installing amneziawg-go and amneziawg-tools **Build from source.** The project ships no prebuilt binaries: it publishes no GitHub releases, only tags (checked on 14 October 2026). For the third line take a `v3.1.*` tag; the latest on that date is `v3.1.20260828`. It needs Go 1.25 or newer, and the system `golang-go` on Debian 12/13 and Ubuntu 24.04 is older (below 1.25), so install Go from [go.dev/dl](https://go.dev/dl/): ```bash sudo apt install -y git make iptables curl # Go 1.25+ from https://go.dev/dl/ (the system golang-go is too old for this tag), for example: curl -fsSL https://go.dev/dl/go1.25.0.linux-amd64.tar.gz | sudo tar -C /usr/local -xz && export PATH=/usr/local/go/bin:$PATH # for ARM use linux-arm64; check the current version on go.dev/dl git clone --branch v3.1.20260828 https://github.com/amnezia-vpn/amneziawg-go cd amneziawg-go && make && sudo make install cd .. ``` The two repositories number their tags differently, which is normal: each has its own numbering. **amneziawg-tools** provide the `awg` and `awg-quick` CLI. The system `awg-quick` from `wireguard-tools` does not understand the obfuscation parameters (`Jc/Jmin/Jmax/S1-S4/H1-H4/I1-I5`), so this fork is required, third line as well: ```bash git clone --branch v3.1.20260812 https://github.com/amnezia-vpn/amneziawg-tools cd amneziawg-tools/src && make && sudo make install cd ../.. ``` ### Config and launch Create `/etc/amnezia/amneziawg/awg0.conf` - replace the `YOUR_*` values with your own. Generate keys with `awg genkey | tee /etc/amnezia/amneziawg/server_private.key | awg pubkey`, and the header protection key with a separate `awg genkey`. The example below is the 3.1 profile. Of the obfuscation params, `S1`-`S4`, `H1`-`H4` and the `HeaderProtectionKey` must match the client; `Jc`/`Jmin`/`Jmax` and `I1` do not have to: ```conf [Interface] Address = 10.9.9.1/24 ListenPort = 39743 PrivateKey = YOUR_SERVER_PRIVATE_KEY Jc = 4 Jmin = 50 Jmax = 150 S1 = 64 S2 = 37 S3 = 21 S4 = 15 H1 = 1 H2 = 2 H3 = 3 H4 = 4 HeaderProtectionKey = YOUR_HEADER_PROTECTION_KEY ContentPaddingAddition = 32-128 PostUp = iptables -I INPUT -p udp --dport 39743 -j ACCEPT PostUp = iptables -I FORWARD -i eth0 -o awg0 -j ACCEPT PostUp = iptables -I FORWARD -i awg0 -j ACCEPT PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PostDown = iptables -D INPUT -p udp --dport 39743 -j ACCEPT PostDown = iptables -D FORWARD -i eth0 -o awg0 -j ACCEPT PostDown = iptables -D FORWARD -i awg0 -j ACCEPT PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE [Peer] PublicKey = YOUR_CLIENT_PUBLIC_KEY PresharedKey = YOUR_PRESHARED_KEY AllowedIPs = 10.9.9.2/32 ``` In the 3.1 profile `S1`-`S4` are 12 or more: the first 12 bytes of the padding serve as the nonce for header encryption. For the 2.0 profile remove the `HeaderProtectionKey` and `ContentPaddingAddition` lines and set `H1`-`H4` to non-overlapping ranges below 2147483647 (for example `H1 = 134567-245678`); `S3` from 8 and `S4` from 4 are allowed there. The client has to get the same `S1`-`S4`, `H1`-`H4` and key. Swap the interface name (`awg0`) and external NIC (`eth0`) for yours. For IPv6 add symmetric `ip6tables` rules. `ListenPort` must be open at the host level (UFW or host iptables) and at the cloud-provider level. Smoke test: ```bash sudo awg-quick up awg0 sudo awg # should show the interface and peers sudo awg-quick down awg0 ``` Run as a systemd service: ```bash sudo systemctl enable awg-quick@awg0 sudo systemctl start awg-quick@awg0 sudo systemctl status awg-quick@awg0 ``` ### Does `manage_amneziawg.sh` work on top? My `manage_amneziawg.sh` targets a kernel-native setup (expects `/root/awg/awgsetup_cfg.init`, relies on `awg-quick` + `awg` from `amneziawg-tools`). In theory it should work on top of a manual `amneziawg-go` + `amneziawg-tools` install, provided the interface is named `awg0` and `/etc/amnezia/amneziawg/` exists. I **do not test or support this path** — for full client management either edit configs manually or pick a userspace-oriented tool. ### Source The minimal working recipe for Debian 13 in a privileged LXC on Proxmox was shared by [@Akh-commits](https://github.com/Akh-commits) in [issue #51](https://github.com/bivlked/amneziawg-installer/issues/51#issuecomment-4288953829) - this section builds on it with additions covering the security warning and the 3.1 profile. We have not run the recipe with a third-line build in an LXC on a test server ourselves. --- ## 🗄️ AmneziaWG on a Synology NAS (DSM) My installer does not run on DSM, but AmneziaWG itself can be brought up on a Synology by hand, through the userspace implementation. Everything below was checked on a DS918+ running DSM 7.4.1. ### Why the installer does not fit | what I checked | what DSM 7.4.1 has | |---|---| | kernel version | 4.4.302+ | | minimum for the AmneziaWG module | 5.15; below that the installer warns and asks for confirmation | | build tree `/usr/lib/modules/$(uname -r)/build` | missing | | compiler `gcc` / `make` | missing | | DKMS | missing | It all comes down to the kernel version. The CPU is fine: a DS918+ is ordinary x86_64. The kernel, though, is about ten years older than what the module builds against. And there is nothing on the box to build it with, no headers and no compiler. Cross building with Synology's toolchain is possible in principle, but you would have to redo it for every DSM update. ### The built-in DSM VPN client It speaks three protocols: L2TP, OpenVPN and PPTP. WireGuard is not among them, there is no module in the kernel, and no `wg` utility in the system either. So stock DSM cannot bring this protocol up at all, and my script has nothing to do with it. ### What does work: userspace [`amneziawg-go`](https://github.com/amnezia-vpn/amneziawg-go) is the official userspace implementation. It needs no kernel module, only `/dev/net/tun`. DSM has it, and the `tun` module is already loaded. I checked it for real: a tunnel came up from a DS918+ to a server installed by this script. The handshake went through, data flowed both ways, and a ping inside the tunnel answers. Obfuscation was fully on the whole time - `Jc`/`Jmin`/`Jmax`, `S1`-`S4`, `H1`-`H4` and the `I1` concealment packet. What runs is a normal AmneziaWG 2.0 profile, not a hollowed out WireGuard. We have not checked the 3.1 profile on DSM. ### What it costs you - **Speed.** The CPU does the encryption with no offload of any kind. On a Celeron in the J3455 class expect tens of megabits. I will not quote exact numbers, I did not measure them. - **Your time.** All of it happens by hand from a console: you need SSH into the NAS and root. `/dev/net/tun` is owned by `root:root`, and the web interface will not get you there. The binary has to be built on another machine and copied over, since DSM has neither Go nor a compiler. - **Autostart.** DSM has no systemd; autostart lives in Task Scheduler under root. Skip that step and the tunnel disappears on the first reboot of the NAS, by which point remembering why will be hard. ### Traps - **`/tmp` is mounted `noexec`.** A binary placed there will not start, and `Permission denied` does not even hint at the real cause. Put it on the volume. - **No `nc`, `socat` or `perl`.** There is `python3` and `curl` though, and the control socket is easiest to drive from Python. - **The client address usually comes with a `/32` mask.** With it no route to the server subnet appears on its own, and packets leave outside the tunnel. You have to add it by hand: normally `awg-quick` does that from `AllowedIPs`, but DSM does not have it. - **A DSM update may wipe whatever sits in system directories.** Where to keep the binary so that does not happen is something I did not look into. ### If what you want is a server on the NAS itself On models with Virtual Machine Manager you can run an ordinary Ubuntu in a virtual machine, and the installer works inside it just as it does on any VPS. The DS918+ has everything for that: the VMM package installed, hardware virtualisation on the CPU, a btrfs volume. Two caveats. The VM will eat a noticeable share of the NAS memory. And from the outside it is still a server behind a home router: you need a UDP port forward, and the VM address is worth reserving, otherwise the forward points nowhere after a reboot. I did not run the whole chain of DSM plus VMM plus Ubuntu plus installer, only the last link, the installer working on ordinary Ubuntu. ### What I did not check Routing all of the NAS traffic through the tunnel - I checked the tunnel itself and left the default route alone. Also: how this survives reboots and DSM updates, whether it works through Container Manager, and what throughput comes out. DSM is not among the supported platforms: the installer does not run there, and I do not test it as a target. --- ## ⚠️ Known Limitations * **Kernel 7.0.0-38 on Ubuntu 26.04 and the HWE kernel 7.0.0-38 on Ubuntu 24.04: the PPA module does not build (since 1 October 2026); since v5.37.1 the installer fixes it itself.** These kernels changed the `setup_udp_tunnel_sock` function, and the AmneziaWG module from the PPA (tag `v3.1.20260906`) does not handle that yet. A fix is proposed in the module repository ([#218](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/pull/218), [#250](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/pull/250)) but not merged. Who is affected: Ubuntu 26.04 after the kernel update to 7.0.0-38, and Ubuntu 24.04 with the HWE kernel 7.0.0-38 (`linux-generic-hwe-24.04`). In our build tests this error did not occur on Ubuntu 24.04 with the regular kernel 6.8.0-146, Ubuntu 26.04 on the previous kernel 7.0.0-34, Debian 12 (6.1.0-53) and Debian 13 (6.12.107 and 6.12.111). What it looks like: `apt upgrade` ends with a DKMS error, and the build log has `passing argument 2 of 'setup_udp_tunnel_sock' from incompatible pointer type`. The new kernel packages stay unconfigured, and later installs and upgrades through `apt` run into the same error again. The tunnel keeps working on the current kernel. On our test server (24.04 with HWE) the bootloader was not updated and the server came back on the old kernel after a reboot, but do not count on that: do not reboot into the new kernel until the module is built. With an installer older than v5.37.1, the easiest way on Ubuntu 24.04 is to stay out of this: keep the regular kernel and do not install `linux-generic-hwe-24.04`. `dpkg -l 'linux-generic*' | grep ^ii` shows which metapackage you have. Holding the metapackage with `apt-mark hold` does not fix a kernel that is already installed. **Starting with v5.37.1 the installer does this**. The `amneziawg-ensure-module` helper, which `apt` runs after every kernel update, applies the fix from #218 to the module source - only when the source matches the tested one byte for byte - and builds the module for every kernel with headers. A fresh install and later kernel updates need no manual steps. On a server that is already installed the helper is old, and it needs updating once: download the new installer with its signature, check the signature and run the repair. You need `minisign` (`sudo apt install minisign`); the block checks the signature with the same key and the same way as the block in [How to Update Scripts](#update-scripts-adv), and does not run the installer if the signature does not match or the release is older than v5.37.1: ```bash cd "$(mktemp -d)" KEY=RWQXfpABHIpZPttqrwYrQNHRTk/iLIz4cVh9KkRwAElHP+CoW/NPEysN f=install_amneziawg_en.sh # Russian version: f=install_amneziawg.sh tag= wget -q -O "$f" "https://github.com/bivlked/amneziawg-installer/releases/latest/download/$f" \ && wget -q -O "$f.minisig" "https://github.com/bivlked/amneziawg-installer/releases/latest/download/$f.minisig" \ && out=$(minisign -V -P "$KEY" -m "$f" -x "$f.minisig") \ && tag=$(printf '%s\n' "$out" | sed -n "s/^Trusted comment: amneziawg-installer \(v[0-9.]*\) $f\$/\1/p") if [ -z "$tag" ]; then echo "NOT VERIFIED: $f, not running it" elif [ "$(printf '%s\n' v5.37.1 "$tag" | sort -V | head -1)" != v5.37.1 ]; then echo "Release $tag is older than v5.37.1: it has no such command, use the manual way below" else echo "Verified release $tag"; sudo bash "$f" --repair-module fi ``` `--repair-module` does not run the normal install. It deploys the new helper, fixes the source (only the tested one), builds the module for the kernels with headers and configures the packages (`dpkg --configure -a`) only if no unfinished kernel is left without the module. At the end it names the kernels that have no module and prints the time of the check. It does not reboot the server and does not test loading the module in the new kernel. Do not run `apt` while it works. It does not update the management scripts: for `manage repair-module` to go through the new helper too, and for `manage diagnose` to show the module per kernel, update them with the block in [How to Update Scripts](#update-scripts-adv). Before rebooting into the new kernel, check the same as in steps 5 and 6 of the manual way below. If the helper's fix gets in the way (for support): `sudo amneziawg-ensure-module --revert` puts the source back as it was and leaves it alone from then on, `sudo amneziawg-ensure-module --enable` allows the fix again. What is tested: builds with the fix on every kernel listed above and on both affected ones; other compilers, Secure Boot and ARM we have not tested. **Manual way** (v5.37.0 and older, or if `--repair-module` refused): the same fix from #218 helps, it checks the function signature itself. The commands below run as root: open a root shell with `sudo -i`. The block in step 2 can be pasted as a whole: it stops at the first error. If any command fails or a hash check prints `FAILED`, go no further. 1. Let the current `apt` command finish. Leave the running kernel and the tunnel alone. 2. Apply the fix to the module source, checking the hash before and after: ```bash ( set -e cd /usr/src/amneziawg-1.0.0 echo 'b14346040ce0188c47e2db2baad1a4f21aa784510f6c95bbb4aa58d5bbe691c9 compat/compat.h' | sha256sum -c - curl -fsSL -o /root/pr218.diff https://github.com/amnezia-vpn/amneziawg-linux-kernel-module/commit/62189503fa51bc049d826e226fdb49ee683f53f2.diff cp -p compat/compat.h /root/compat.h.orig patch -p2 --forward < /root/pr218.diff echo '8d47a358b4df0b2187788ce6f88ad63128218de1be22c78b263ef3e5d770c26b compat/compat.h' | sha256sum -c - ) ``` If the very first check prints `FAILED`, check the second hash: `cd /usr/src/amneziawg-1.0.0 && echo '8d47a358b4df0b2187788ce6f88ad63128218de1be22c78b263ef3e5d770c26b compat/compat.h' | sha256sum -c -`. If it matches, the fix is already applied; go to step 3. If not, you have a different source, and this fix is not for it. 3. Build the module for the new kernel. Its full name is in the DKMS error and in `ls /lib/modules`, for example `7.0.0-38-generic`. Do not use `uname -r`, that is the current kernel: ```bash dkms install -m amneziawg -v 1.0.0 -k 7.0.0-38-generic ``` 4. Finish configuring the packages: `dpkg --configure -a`. If dependency errors remain, run `apt-get -f install` and then `dpkg --configure -a` again. If `apt` offers to remove anything, read the list first: the running kernel must stay. 5. Before rebooting, check: `dpkg --configure -a` finished without errors (it is what updates the initramfs and the bootloader), `dpkg --audit` prints nothing, `dkms status amneziawg` shows `installed` for the new kernel. As a cross-check, `/boot/initrd.img-7.0.0-38-generic` exists and `/boot/grub/grub.cfg` has an entry for this kernel (`grep -c 7.0.0-38-generic /boot/grub/grub.cfg` is above zero). If Secure Boot is on (`mokutil --sb-state`), the module has to be signed with a key the system trusts; we have not tested this under Secure Boot, and there is no need to turn it off. If anything is off, do not reboot and do not remove the old kernel. 6. Reboot into the new kernel when you have the provider's console at hand. `installed` in DKMS does not yet mean the module will load: after the reboot check `lsmod | grep amneziawg` and `systemctl status awg-quick@awg0`. We have not tested this fix on ARM. The fix stays in `/usr/src/amneziawg-1.0.0` until the next `amneziawg-dkms` package update. If the new PPA version already contains the fix, there is nothing to repeat; if it does not, the build for the next kernel fails again and the steps need repeating. If you see the same error but the hash in step 2 does not match, open an [issue](https://github.com/bivlked/amneziawg-installer/issues) with the output of `dkms status` and `uname -r`. * **LXC containers are not supported by my installer.** AmneziaWG requires a kernel module (DKMS). LXC shares the host kernel — loading a custom module from inside a container is not possible. Options: a full VM (KVM/QEMU) or a bare-metal server for a kernel-native setup, or the `amneziawg-go` userspace implementation inside LXC (see [LXC / Docker via amneziawg-go](#lxc-userspace-adv)). * **Assumes a dedicated server.** The script configures UFW, Fail2Ban, sysctl and optimizes the system for VPN. On servers running other services, use `--no-tweaks` to skip hardening, or `--keep-packages` if the only problem is the package removal and you still want the firewall and the optimization. * **Do not install on a host running a server from the Amnezia app.** The official app deploys AmneziaWG in a Docker container on userspace `amneziawg-go`. As soon as our kernel module appears on the host, `awg-quick` inside the container switches to it and stops falling back to userspace, and the AmneziaWG container stops accepting connections ([issue #209](https://github.com/bivlked/amneziawg-installer/issues/209)). Pick one: a separate server for the installer, or remove the container through the app first. If you already installed on top, `--uninstall` plus a reboot should bring the container back to userspace. UFW has nothing to do with it: Docker's published ports go through FORWARD, not INPUT. * **One protocol generation per server.** All clients get a profile of one generation: either all 3.1 or all 2.0. If even one device does not understand 3.1, install the server with `--protocol=2.0`; changing the generation takes `--uninstall`, a new install and new profiles for every client, see [Where 2.0 is installed instead of 3.1](#awg3-fallback-adv). AWG 1.x clients are supported by neither profile. * **Ubuntu other than `noble`, `jammy` and `focal`:** the installer requests `dists//Release` from the PPA, and if there is no answer (a 404 or the PPA is unreachable), switches the PPA codename to `noble`. Ubuntu 26.04 normally gets its own `resolute` suite (the PPA has published it since 14 September 2026), and if the check fails it is switched to `noble` too. Debian is always mapped to the nearest Ubuntu release: 12 -> `focal`, 13 -> `noble`. The module still comes from the PPA DKMS package and is built for your kernel, except on ARM systems with a prebuilt module. * **IPv6 Dual-Stack Tunnel - rolling back `ALLOW_IPV6_TUNNEL=0`:** Setting `ALLOW_IPV6_TUNNEL=0` in `awgsetup_cfg.init` (or re-running without `--allow-ipv6-tunnel`) does **not** remove existing dual-stack `AllowedIPs = ..., fddd::.../128` entries from `[Peer]` blocks already written to `awg0.conf`. The entries remain and the kernel keeps IPv6 routes for those peers. `manage_amneziawg.sh regen ` (or the full path `/root/awg/manage_amneziawg.sh regen `) after disabling the flag rebuilds only the client `.conf` - it becomes IPv4-only, since `regenerate_client` reads `ALLOW_IPV6_TUNNEL`. But `regen` does **not** remove the IPv6 `AllowedIPs` from the server `[Peer]` block. To clear the server side too, use the sed cleanup across all peers: `awg-quick down awg0; sed -i 's|, fddd:[^/]*/[0-9]*||g' /etc/amnezia/amneziawg/awg0.conf; awg-quick up awg0`, or `manage_amneziawg.sh remove ` + `add `. --- ## 🤝 Contributing Suggestions and fixes are welcome! Create an Issue or Pull Request in the [repository](https://github.com/bivlked/amneziawg-installer). --- ## 💖 Acknowledgements * The [Amnezia VPN](https://github.com/amnezia-vpn) team. ---

↑ Back to top