amneziawg-installer/CHANGELOG.en.md
Ivan Bondarev df9b6134ea
Some checks are pending
ShellCheck / Lint and syntax check (push) Waiting to run
Tests / unit-tests (push) Waiting to run
Docs consistency / docs-consistency (push) Waiting to run
chore(release): v5.37.0 (#315)
Version 5.37.0 in the six scripts, AWG_COMMON_VERSION, the README badges,
the pinned raw URLs, the SECURITY table and the facts block; CHANGELOG
sections moved from Unreleased; SHA pins recomputed; the six scripts signed
for v5.37.0.

The release review of the whole v5.36.2..HEAD diff found four paths where a
failure passed silently, all fixed here with tests:

- A failed restore now takes back expiry stamps that came from the archive.
  The rollback copied the pre-restore stamps back but left the archived ones,
  so a client that was permanent before restore could be deleted by cron at
  the archived deadline while the JSON said rolled_back.
- An explicit --ssh-port is taken only as a whole. A bad element, an empty
  element or an empty value stops the install at step 0, before any change;
  the value is not expanded as a glob, and a newline no longer hides what
  follows it. Dropping one element could lock out SSH on the real port.
- A CLIENT_DNS line the parser does not recognise is named in a warning
  instead of giving new clients the default DNS without a word.
- An unknown AWG_APPLY_MODE is named in a warning instead of falling back to
  syncconf silently.

The remaining Russian comments in awg_common_en.sh are translated, and the
comments on the old mode 2 IPv6 route name its last release, v5.36.1.
2026-09-26 23:22:06 +00:00

287 KiB
Raw Blame History

RU Русский | EN English

Changelog

All notable changes to this project are documented in this file.

The format is based on Keep a Changelog.


Unreleased

5.37.0 - 2026-09-27

v5.37.0 - client DNS in the config, --no-prebuilt for ARM, and a docs review.

Added

  • The DNS for new clients is set in awgsetup_cfg.init. A line export CLIENT_DNS='10.9.9.1' (IPs separated by commas) gives that DNS to clients created after the edit and to a client whose config regen rebuilds from scratch. Configs already handed out keep their DNS. An invalid value is not silently replaced: creating a client and the installer refuse with a clear message, while regen of clients already handed out works as before. An empty value means 1.1.1.1, 1.0.0.1. A line the parser does not recognise (spaces around =, an indent, lower case) is named in a warning by the installer and manage instead of being skipped silently. Before this the only way was to edit the library, and the installer and script updates overwrote that edit. Idea from PR #294 by @rootcreep.
  • The --no-prebuilt flag. On ARM the installer skips the prebuilt module package from the arm-packages release and builds the module with DKMS. The prebuilt packages are built by this project's CI and checked only against a SHA256 from the same release, with no signature; the flag is for those who care. On kernels 6.7 and newer it is also a different module: instead of the pinned 2.0 one, amneziawg-dkms from the PPA is installed, as on x86. If a prebuilt package is already there from an earlier run, the installer stops and says how to remove it, so no second module is built next to it. A NO_PREBUILT other than 0 or 1 stops the install too. The choice is saved in awgsetup_cfg.init and survives the install's reboots.

Changed

  • Step 1 no longer turns off GRO/GSO/TSO on the network card or installs ethtool. The change lasted only until the reboot that follows step 1, and nothing of it remained on a finished server. A finished server behaves exactly as before.

Fixed

  • modify <name> DNS no longer expands globs. The DNS list check parsed the value in an unquoted loop, so 1.1.1.* passed whenever a file named 1.1.1.1 sat in the current directory. The check is now shared with CLIENT_DNS and lives in awg_common.sh.
  • --uninstall no longer runs apt-get autoremove. It removes everything apt considers unneeded across the whole system, not just what the installer left; at install time in #84 it took netplan-generator that way and the server came back without an IP. Installs made after the #223 fix protect such packages, older ones do not. Packages the installer added explicitly (dkms, the compiler, kernel headers) were never removed by the uninstall; now the automatic dependencies of the purged packages stay as well, and the uninstall says so in the log. The uninstall also removes /etc/apt/apt.conf.d/99-amneziawg-lock-timeout, which the installer creates and the uninstall used to leave behind.
  • Raspberry Pi kernel headers follow the kernel flavour. When the exact linux-headers-$(uname -r) package is missing, the installer picks the meta-package from the kernel suffix: rpi-v6, rpi-v7, rpi-v7l, rpi-v8 or rpi-2712. Previously everything but a Pi 5 got rpi-v8, which the 32-bit Raspberry Pi OS repository does not carry.
  • --diagnostic reports the loaded module separately from the file on disk. The Module Info section now has two parts: the version and srcversion of the loaded module from /sys/module/amneziawg, and modinfo for the file on disk. The report compares their srcversion: a mismatch usually means the module was updated but not reloaded. "No file", "no permission" and "empty" are now told apart.
  • --ssh-port without a valid port stops the install. A value such as 2222/tcp or 222222 used to become 22 silently, and with --yes UFW came up with a rule for port 22 only, cutting off SSH on the real port. The installer now stops already at step 0 with a clear message and leaves UFW alone. An overlong number is refused too instead of wrapping into a small port number. An explicit list is taken only as a whole: --ssh-port=22,500022 (one digit too many in 50022) stops the install instead of leaving port 22 alone. An empty --ssh-port= and an empty list element (22,, which is what an unset variable looks like) are refused too, and the value is not expanded as a file name pattern. Without the flag the SSH port is detected automatically, as before.
  • The restart apply mode works everywhere it is promised. --apply-mode=restart and AWG_APPLY_MODE=restart in the environment are no longer overridden by the value in awgsetup_cfg.init during add, and a mode saved in the init file is now honoured by remove and by the cron removal of expired clients as well. The order is: flag or environment, then the init file, then syncconf. An unknown value (such as Restart) still gives syncconf, now with a warning.
  • Step 7 no longer mistakes a working service for a broken one on a server with many clients. The interface, port and obfuscation checks read their output through | grep -q under pipefail, and on long output the interface or port check could fail by mistake and the parameter check could give a false warning. The output is now read in full first.
  • manage check no longer says "port is not listening" about a listening port. Same cause: | grep -q under pipefail on a long socket list.
  • manage diagnose no longer hangs on an I1 with a space inside a tag length, such as <r 1 0>. The tag parser lost its position and went round in circles.
  • An empty --expires= in add is now an error. The client used to be created permanent without a word while JSON reported success; this happens when a bot passes an empty expiry variable. Without the flag the client is permanent, as before.
  • The installer checks the write of awgsetup_cfg.init. On a full disk an empty or truncated file replaced the working one, the next run generated new obfuscation parameters, and the clients already handed out stopped connecting. A failed write now stops the installer before the replace, and the working file stays as it was.
  • An expiry stamp no longer attaches to a same-named permanent client. After restore or a new add with the same name, a client could inherit the deadline of the previous one and be deleted by cron at that deadline. add and restore now drop such a stamp; if it cannot be removed, add refuses that client and restore rolls back. Stamps from the archive are restored as before, and restore now checks that they actually landed, rolling back otherwise. An archive without an expiry/ directory had no deadlines in it, so clients restored from it lose their stamps. If restore rolls back, the rollback also takes away stamps that came from the archive, so a client that was permanent before restore stays permanent.
  • IPV6_SUBNET can no longer land in the IPv6 sink prefix. The routing mode 2 sink lives in fddd:2c4:2c4:ffff::/64 and is recognised by that prefix. With the tunnel subnet set inside it, a client's real IPv6 address was taken for a sink: regen and modify could drop it, and vpn:// and list did not show it. The installer with the IPv6 tunnel now stops on such a value already at step 0, and creating a client refuses with a clear message. On a server that already has clients the installer only warns: the subnet cannot change under live clients, and the clean path is --uninstall and an install with another ULA subnet.
  • The English manage warning about an outdated library is no longer cut off. The ADVANCED section name sat in double quotes inside a double-quoted string, so the message stopped at "(section How". The Russian version was already correct.

Documentation

  • Mobile network advice now follows the measurement. The tip after the flag table and the FAQ "VPN connects over cellular only on the third attempt" no longer suggest lowering the junk packets further (--jc=2 --jmin=20 --jmax=60): the September 2026 measurement on MTS Moscow shows the shape of I1 decides the handshake, while Jc, Jmin and Jmax do not affect it. The manual edit step no longer swaps a DNS-shaped I1 for a random one. Both now point to "The handshake never completes on cellular" first. The carrier table gains an "MTS (Moscow)" row with the results of the same measurement.
  • Changing the MTU now mentions the MSS. The FAQ "How do I change the MTU?" and the "reduce MTU" troubleshooting step say that --set-mss in PostUp and PostDown must change along with the MTU (MTU minus 40 for IPv4, minus 60 for IPv6), with the tunnel stopped before the edit. Without it, large packets stop passing through the tunnel after the MTU is lowered (discussion #38). The MSS section now says that --force recomputes the MSS from the new MTU only without the --preset, --jc, --jmin, --jmax flags: with them the manual MTU edit is rolled back.
  • Your own DNS server for clients. ADVANCED gains a section on giving clients an ad blocker or your own resolver running on the VPN server (Pi-hole, AdGuard Home, Blocky), and on what breaks that setup without a single error: an example address instead of the configured one with --subnet, a DNS address never given to clients, the DNS address outside the client's AllowedIPs with the list-based routing modes, port 53 closed on awg0 (and open to the whole internet when the resolver runs in Docker), and CRLF in files copied over from Windows (discussion #237).
  • The README first screen describes the install more accurately. The installer creates two clients, my_phone and my_laptop, and saves their configs, QR codes and vpn:// links in /root/awg/ rather than printing them; it offers to turn on the UFW firewall. The Russian subtitle now names the project, and a link to the English README sits next to it.
  • The right QR code for the Amnezia VPN app. It is name.vpnuri.png, the QR of the vpn:// link; name.png is the QR of the .conf itself, for AmneziaWG clients. The README and the English install guide used to point to name.png.
  • Two installer questions that are easy to miss are described. The package cleanup question and "Enable UFW? [y/N]" in the third run: pressing Enter leaves the firewall off.
  • The full list of flags after which configs must be reissued. Besides --preset, --jc, --jmin and --jmax these are --mobile and --port: they change the obfuscation parameters or the port. --endpoint is covered separately: only reissued configs get the new address. The mobile advice no longer suggests simply reinstalling a running server with --mobile.
  • ARM and kernel upgrades. The FAQ answer and the guide say that on ARM with a prebuilt module there is no DKMS, and the installer has to be run again after a kernel change.
  • Signature verification compares the whole key. The command uses minisign -P with the full public key, and 3E598A1C01907E17 is called a key ID rather than a fingerprint: another key can carry the same ID. The section explains what the signature covers and what it does not give you. The same in SECURITY and SIGNING_DESIGN.
  • Five new questions in the README FAQ: mobile data, the yellow config in the Amnezia app, the local network and IPv6, Synology, and "stopped connecting after an update".
  • Script updates with signature verification. The update block in README and ADVANCED downloads manage_amneziawg.sh and awg_common.sh into a temporary folder, verifies each file with minisign -P and replaces the working copies in /root/awg/ only after a successful check. 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, and it prints the verified release number. The integrity line in README matches the download from the latest release.
  • What the installer changes and what --uninstall leaves. README lists the main changes the way the code makes them now, including what --yes and --no-tweaks do. README and both install guides say what removal takes away and what stays on the server: dependency packages, swap, the module package on ARM, and the /root/awg_uninstall_backup_*.tar.gz archive with configs and private keys, which you have to delete yourself.
  • How to remove the cascade. CASCADE gains a section with removal commands for both servers. CASCADE and WARP-RU warn that they have to be removed before --uninstall: the installer only stops awg0 and deletes /etc/amnezia and /root/awg as a whole.
  • The English and Russian install guides are aligned. The English one gains the hoster's emergency console, the table of four client files with two QR codes, scp from your own computer with -P for a non-standard port, a "What next" section and the unreachable-address item. The Russian one gains the reboot diagram and the related reading list.
  • Corrections in ADVANCED and README. Containers: the installer stops inside them, and the LXC path is amneziawg-go. Moving to a new server: check the network interface, ListenPort and AWG_ENDPOINT. regen takes the address from AWG_ENDPOINT. The "Amnezia list" mode is no longer called split routing. The Russian install guide describes the switch to noble as conditional. The English install guide gives I1 = <r 48> for Tele2 (Krasnoyarsk). For a leaked config the guides advise remove and add: regen keeps the same keys. The hoster affiliate link is marked as one.
  • Issue templates. The issue and Q&A templates ask for the client app, the network and the routing mode, show how to get the version and link the English FAQ.
  • Contributor documents. ROADMAP, SIGNING_DESIGN, RELEASE_PROCESS, the PR template and CONTRIBUTING describe the process that runs now: signatures before the tag, AWG_COMMON_VERSION among the version places, the commit hygiene check.
  • wgcf is checked in the WARP guide. The binary is downloaded under its release name, checked against checksums.txt from the same release and run only after that check passes; a wgcf left from an earlier run is removed first. The security section says what that check does not protect against.
  • How the installer picks the repository on newer Ubuntu. README, both guides, ADVANCED, the support matrix and CONTRIBUTING describe the mechanism: the installer checks whether the PPA answers for the release codename and switches to noble only if it does not. Debian uses a fixed mapping: 12 -> focal, 13 -> noble.
  • Samples match what the code produces. The stats output is rendered with the same functions as in manage; the diagnostic report table lists all 17 sections; the client configs in ADVANCED and CASCADE show two DNS servers and 0.0.0.0/0, ::/0; the log lines for a prebuilt ARM module and the DKMS fallback, the Fail2Ban backend, the MTU in vpn:// and the fact that manage derives server_public.key itself now follow the code.
  • Five ADVANCED FAQ questions get their own anchors (yellow config, mobile data, iPhone, "third attempt", changing parameters); links from README and the guides that pointed at the top of the FAQ now go straight to the answer. The diagnose --carrier profile names are listed; tmobile_us is a historical name for the Russian T-Mobile (Moscow and region).
  • Smaller fixes. The add guest command in README has the full path, links in the Russian install guide lead to the right sections, the English guide's long paragraph became a flag list, AWG_BRANCH goes after sudo, cascade step 1 warns about the two reboots, --force does not keep the WARP PostUp line, the threat model link points to docs/SIGNING_DESIGN.md, SECURITY names both language versions of the scripts and the release pipeline, and signing/README.md uses scripts/sign-release.sh and verification with the whole key.
  • The documentation guard checks more. Cross-file and HTML links to anchors, equal anchor sets in the RU/EN pairs of ADVANCED, CASCADE and WARP-RU, the return of the old wording about modes 2 and 3, and the version placeholder in the discussion template.

5.36.2 - 2026-09-24

v5.36.2 - the "Amnezia" mode keeps the local network reachable in the AmneziaWG Windows client again.

Fixed

  • The "Amnezia" routing mode (mode 2) keeps the local network reachable in the AmneziaWG Windows client again. Since v5.31.0 this mode's list got ::/0 appended so the device's IPv6 would not go around the tunnel. But with ::/0 the Windows client blocks all traffic outside the tunnel, and the router, other computers and SSH on the home network went with it - exactly what people choose this mode for. The list now gets 2000::/3 (all global IPv6), and the client's Address a service address like fddd:2c4:2c4:ffff::a09:905/128: without an IPv6 address Windows does not install that route. The device's IPv6 still goes into the tunnel and dies there, and the local network stays outside the tunnel. Checked on Windows, on Linux, and on Android on a mobile network with IPv6. Profiles already issued are updated by a plain manage regen, then re-imported on the devices; ::/0 is replaced only on a list matching the server's current list, clients issued with --allow-ipv6-tunnel are left alone, and if a full tunnel keeps ::/0 (a different list, or the server's list changed since), it warns and says how to move to 2000::/3. The "All traffic" mode does not change.

5.36.1 - 2026-09-23

v5.36.1 - the PPA key is embedded in the installer, step 2 no longer depends on a keyserver.

Fixed

  • Step 2 no longer fetches the PPA key from keyserver.ubuntu.com: the public Amnezia PPA key is embedded in the installer. The key used to come only from there, with no time limit, so a server that could not reach that host stopped at step 2 after a couple of minutes of waiting, even when the PPA itself was reachable (Discussion #292). Getting the key needs no network now. The check against the full fingerprint 75C9DD72C799870E310542E24166F2C257290828 stays: a key with another fingerprint, or a block with an extra key, is not installed. If gpg cannot read the written key during the check, its message goes into the error text and the install log. If Amnezia ever changes the PPA key, a new installer version will be needed.

5.36.0 - 2026-09-22

v5.36.0 - protocol generation in manage, keys hidden in service status.

Added

  • Groundwork for the third-line path: the installer can tell whether the loaded kernel module understands the 3.1 parameters. The check is not active in this version: --protocol=3.1 is still refused at step 0 for the same reasons as before, and an ordinary install does not run it. It starts working when the 3.1 path opens. The line cannot be told from the module version string: the same string was seen on different builds. So the capability is probed: a header protection key and a padding range are set on a temporary interface and read back. Once the path opens, a second-line module will be named as such together with the way to update it, and a check that could not be made will be reported as such instead of a guess. The probe key is one-shot, never appears in a command line and never in the log, --verbose included, and the temporary interface is removed on every ordinary exit, a signal included; and when it cannot be removed, the installer names it or says how to find it, and how to take it away by hand.
  • manage check, show and diagnose show the protocol generation of the installation, and check --json returns it in the protocol field. The generation comes from the AWG_PROTOCOL marker, not from the module version: a third-line module runs a second-line configuration as a matter of course, and an "AmneziaWG 2.0" header next to a module version 3.1 confused people. Command headers no longer name a generation; it has a line of its own. A broken marker never turns into "2.0": check treats it as a problem (ok=false, protocol_error: "unreadable"), show and diagnose say the marker cannot be read and exit with an error.
  • diagnose reports Jmin greater than Jmax on the live interface as a FAIL (exit code 1). Such a pair is invalid, and the kernel module does not check it and writes past a buffer end with it (amneziawg-linux-kernel-module#225). The installer never produces this pair; it only appears after a hand edit of awg0.conf. A missing Jmax with Jmin set counts as the same error.

Changed

  • Removing RandomTrailers or DisableCookies from awg0.conf gives the same warning as the other interface parameters. awg syncconf does not clear them, so the script points at a service restart.

Fixed

  • The header protection key no longer reaches the manage show and manage check output or the installer diagnostic report when WG_COLOR_MODE=always is set. With that variable awg show colours its labels even when the output is not a terminal, and the secrets filter did not recognise the header protection key line. The scripts now run the tools with WG_COLOR_MODE=never. Under the same variable the step 7 service check of the installer failed falsely, check reported the obfuscation parameters as not detected, and diagnose found no jc, jmin and jmax; that is fixed too.
  • The service status text in manage check, and on a failed restore or restart, no longer shows keys from amneziawg-tools messages. systemctl status ends with the unit's journal lines, and with a malformed key in awg0.conf (a character lost in a hand edit, PrivateKey inside [Peer]) those carried the amneziawg-tools message with the key value: Line unrecognized: ... or Key is not the correct length or format: .... This text used to go to the screen, to the manage log and into reports people post in issues. It now goes through the same secrets filter as the installer's diagnostic report; if the filter itself fails, check hides the status text and exits with an error, while restore and restart say the status is hidden and name the command to see it. With --json this text still goes to stderr, and in the plain mode systemctl's own warnings stay on stderr too.
  • The secrets filter catches a key where a hand edit broke the line. amneziawg-tools strip whitespace from an unrecognized line, so a missing =, a typo in the key name, a colon instead of = or the tail of a key wrapped onto the next line gave a Line unrecognized: ... message with the key still visible. Where the line still reads as Name = value for a known parameter (for example RandomTrailers outside its section), only the name now stays visible and the value is hidden; any other such line, a broken separator included, is hidden in full. On top of that, a value in `...' quotes that looks like a key as a whole is hidden in any message, for example Unable to parse Jc: ... when a key landed in the wrong field, and in Unable to parse IP address: ... a value that cannot be an address is hidden: this is how amneziawg-tools answer a key pasted into AllowedIPs. Addresses, endpoints, ports and numbers in these messages stay visible. This applies to the installer's diagnostic report, manage check, show, diagnose and the service status text.

5.35.0 - 2026-09-17

v5.35.0 - keys stay out of traces and check and show output, steadier install on Debian 12.

Changed

  • The server config check understands the header protection key. restore runs it before starting the service and rolls back on a refusal, and the installer runs it too, as a warning. When HeaderProtectionKey is set in [Interface], the check accepts H1-H4 as a single number, requires S1-S4 to be at least 12, as the kernel module and amneziawg-go require, and refuses a repeated key, a key of the wrong form and a key outside [Interface]. With such a key Jc, Jmin, Jmax, S1-S4 and H1-H4 are read the way amneziawg-tools read them: section and key case-insensitive, the last value applies, and a value above 4294967295 is refused before any comparison. ContentPaddingAddition is checked in any config: a value above 65535 is now refused, because amneziawg-tools silently wrap it modulo 65536, and a reversed range is refused before the service starts. H1-H4 are now read in base 10, as amneziawg-tools read them, so a value with a leading zero is checked correctly, and a value above 4294967295 is refused at once rather than when the service starts. Configs without HeaderProtectionKey and ContentPaddingAddition and with ordinary H1-H4 ranges are checked as before.

  • add, regen, modify, vpn:// issuing and a repeated installer run no longer produce broken profiles when HeaderProtectionKey is written into the server config. The header protection key is two-sided: a profile without it does not connect. Previously, on an installation marked as generation 2.0 whose awg0.conf had the key added by hand, these commands silently issued client profiles without the key. They now refuse and name the reason: the AWG_PROTOCOL marker in awgsetup_cfg.init and the key in the config contradict each other. modify checks this before touching any client file, and the installer stops before rewriting the server config. The same refusal applies when a key exists and the marker cannot be read; without a key a broken marker only produces a warning in manage commands. Otherwise servers without HeaderProtectionKey work as before.

Fixed

  • An install no longer stops with "Failed to hold amneziawg-dkms" while the hold is actually in place (issue #285). The hold check runs on a kernel older than 6.7, Debian 12 for example, and on ARM with a prebuilt module on any kernel. After apt-mark hold the installer compared the result with the apt-mark showhold list piped into grep, and on some systems it refused with the hold in place: in the report from the issue dpkg showed hold ok not-installed and a manual apt-mark showhold listed the package. One mechanism for such a refusal reproduces: apt-mark writes the list line by line, grep -q quits at the first match, and if more held packages follow in the list the pipeline can count as failed under pipefail. Whether that was the cause on the reporter's host is not established yet. The check now looks where apt itself takes the hold from, the package selection in dpkg, and reads the apt-mark showhold list as a fallback, without a pipe. If neither shows the hold, the install still stops, because without it amneziawg-tools can pull the AmneziaWG 3.0 module from the PPA, bypassing the chosen path. Such a refusal now prints what apt-mark hold answered and the package status in dpkg, not only a general hint to check the apt lock.
  • manage check and manage show no longer print the header protection key. awg show prints it in clear text, and check carried that output to the screen and the log. The key line is now hidden the same way as in the diagnostic report, and the rest of the output is unchanged; if the filter itself fails, check hides the output, show reports that the output was not shown in full, and both commands exit with an error. In diagnose the first error line of awg show goes through the same filter.
  • A hung awg show is now named as a timeout. In check and show the exit status was lost ever since the timeout check was added, so a generic error was printed instead of "did not answer within 10 seconds".
  • The trace of the installer with --verbose and of manage under bash -x no longer shows keys. It used to show server and client private keys, PresharedKey values of carried-over peers, the header protection key and vpn:// links: while generating keys, rendering the server config, creating the default clients, and reading and checking the server config. Such functions now run entirely with tracing off, and tracing is turned back on after them. The same applies to manage run under bash -x (list, stats, check, diagnose, modify, add, regen, remove and the service status printed when restore or restart fails), where keys also leaked while regenerating clients and applying the config. A value passed directly on the command line is still shown by the trace before the function is entered. The service status text itself (check, restore, restart) is still printed unmasked; only its copy in the trace is gone.

Documentation

  • Where device IPv6 is decided. ADVANCED gains a section "Device IPv6, ::/0 and the local network". It says plainly: --disallow-ipv6 and DISABLE_IPV6 turn IPv6 off on the server, while whether a client gets ::/0 is decided by the routing mode. The mode 2 description now says plainly that only the IPv4 side of the LAN stays outside the tunnel, the README hint about disabling IPv6 makes clear it means the server, and the IPv6 walkthrough in ADVANCED names the exact condition: without --allow-ipv6-tunnel, your own network list gets ::/0 when it covers all public IPv4. It also says that with --allow-ipv6-tunnel on a server without native IPv6 the client gets only the tunnel subnet.

5.34.1 - 2026-09-14

v5.34.1 - I1-I5 tag checks and fresh QR and vpn:// files after modify.

Fixed

  • Tag lengths in I1-I5 are now checked before profiles and the server config are issued. amneziawg-go accepts <r>, <rc> and <rd> with a negative length and crashes when it builds the packet, while the kernel module, when a <b> stands next to it, accepts it without an error and writes past the end of the buffer (upstream amneziawg-linux-kernel-module#233). The installer itself never generates such a value, but it could reach awg0.conf through a manual edit or someone else's config, and regen would hand it to every client. Issuing profiles and the server config with such a value now stops with a clear error, modify does not reissue a profile whose .conf already carries such a value, and restore refuses such a backup and rolls back. Only what is proven dangerous is refused: a negative length, a length that is not a number or has more than nine significant digits, and a total above 65535 bytes in one value. The value is read the way the implementations apply it: a comment after # dropped, an unterminated last tag parsed too; restore also checks every I1-I5 line regardless of key and section case, as amneziawg-tools read them, and modify checks the client .conf the same way. A trailing comment is now also dropped when awg0.conf is read, so a # ... tail no longer reaches client profiles and vpn://. Unknown tags and other malformed input are still rejected by the implementations themselves. The boundary: the check guards issuing profiles, generating the server config and restore. Paths that only start or reload the service apply the awg0.conf already in place without it, for example systemctl restart, manage restart, repair-module, the start at boot and the apply after remove or after expired clients are removed.
  • After modify, QR and vpn:// files match the edited .conf or are absent. After editing a client .conf, the command rebuilds NAME.png, NAME.vpnuri and NAME.vpnuri.png. When one of them could not be rebuilt, for example without qrencode or the perl modules, the previous file stayed in place with the previous values while the command reported success, so a consumer such as a bot could hand it out as current. With the I1-I5 check in this same release, a server with a refused value and a clean client .conf became one more such case: the .conf edit goes through, but vpn:// cannot be built. These files are now removed before the edit and rebuilt after it, so a failed or interrupted rebuild leaves a missing file rather than a stale copy; a modify whose own value check fails (for example an invalid DNS) leaves the files alone. The --json reply of modify gains the qr and vpnuri fields, as in add: a path when the file exists, null when it does not. If a previous file cannot be removed, modify fails and leaves the .conf unchanged; the files removed before it are rebuilt by repeating the modify once the cause is fixed.

5.34.0 - 2026-09-12

v5.34.0 - the default routing mode becomes the full tunnel: client profiles get AllowedIPs = 0.0.0.0/0, ::/0 instead of the subnet list that some clients read as split routing already configured on the server.

Changed

  • 🔴 The default routing mode is now the full tunnel. A fresh install hands clients AllowedIPs = 0.0.0.0/0, ::/0 instead of the 34-subnet list ("Amnezia List + DNS"). The old mode has not gone anywhere: it is still the second menu entry and the --route-amnezia flag. Why. The Amnezia app opens its own split-tunneling page only when it sees 0.0.0.0/0 among the client routes; the form it reliably recognises is the pair 0.0.0.0/0, ::/0. Handed a subnet list, the app concludes that split routing is already configured on the server, reports that the server does not support it, and hides its own toggle. On Linux, awg-quick engages its fwmark machinery only for a /0 route, and a list-shaped IPv4 config has none: on our test bench that produces a routing loop in which the encrypted packets to the server itself are sent back into the tunnel. The Amnezia app's own server template, incidentally, also emits 0.0.0.0/0, ::/0. The cost, plainly. The LAN goes into the tunnel along with everything else; whether it stays reachable is decided by the client, not by the config. If you need your home devices while the VPN is up, install with --route-amnezia. A running server keeps its mode. The new default applies to new installs only; a reinstall takes the mode recorded in awgsetup_cfg.init, and client profiles do not change by themselves. To switch the mode on a running server: reinstall with the flag you want, then manage regen --reset-routes.
  • The routing menu was rewritten. The old one called the list-based mode the one recommended for bypassing restrictions, although it sends the same public IPv4 into the tunnel as the full tunnel does; the only difference is the private networks, which stay outside. Each mode now names both its benefit and its price.

Fixed

  • The network prompt of the "only the listed networks" mode could spin forever. The input was read without checking the exit status: on end of input (Ctrl-D) the string came back empty, and with the terminal gone the read did not execute at all - either way the "try again" loop ran endlessly, flooding the server log with format errors. The exit status is now checked, the number of attempts is bounded, and the refusal names a ready command.
  • Reinstalling silently changed the routing mode of a working server. When the server settings carried a mode but an empty route list, the installer assigned the default mode over the saved one. The list is now rebuilt for the saved mode, and a config that says "only the listed networks" without carrying that list is refused with a ready command instead of falling through to the interactive prompt.
  • The ALLOWED_IPS_MODE value in the server settings was never validated. It was the only routing key without a check; an arbitrary value reached the client route calculation and was treated as "your own network list", so with client isolation off the tunnel subnet was appended to the routes and the profile stopped being a full tunnel in the shape clients recognise. Only 1, 2 and 3 are accepted now; anything else stops the install with a clear message.
  • Changing the mode without a flag printed no re-issue hint. The regen --reset-routes hint was tied to an explicit --route-*, yet the mode also changes without one. It is now printed on any divergence from what the server settings record.
  • An unrecognised answer in the routing menu fell back to the default mode silently. It now prints a warning naming the mode it picked; an empty answer still counts as a deliberate choice of the default and raises no warning.

5.33.0 - 2026-09-11

v5.33.0 - the I1 concealment packet takes the shape of a DNS reply instead of the random bytes that kept the handshake from completing on some cellular networks, and the protocol generation for a new install is now set by a flag.

Added

  • The installer's --protocol=2.0|3.1 flag. Sets the protocol generation for a NEW install; the default is 2.0, as before. The spaced form (--protocol 3.1) is accepted too, because that is how the way out is written in the refusal messages. On an already configured server a flag naming a DIFFERENT generation ends the install and explains why: the generation of a running install does not change in place - that is reissuing every client profile and handing them out again; a flag matching the installed generation is accepted quietly. This installer version does not emit 3.1 yet - the request is declined with a named reason, one per case: an old kernel, an unsuitable architecture, awg tools without support for the parameters, an undetermined architecture. The refusal also says when the machine may not be the problem at all: the installer carries no third-line generator on any architecture yet.

Fixed

  • The I1 concealment packet now has a meaningful shape. The generator emitted I1 = <r N> - 32 to 256 random bytes. Some cellular networks drop such a packet together with the handshake packet that travels in the same opening burst: the tunnel never comes up, while the server shows the peer and its received counter grows, so the failure is easily mistaken for a working link. A measurement on a live carrier separated size from shape: two 128-byte profiles, the random one never completed a handshake, the DNS-reply-shaped one completed in 40 seconds. The generator now writes a DNS-shaped packet of 70-128 bytes using tags both implementations understand (<b>, <r>, <rc>); the transaction id and the name label are expanded afresh in every packet, so this is a structure rather than a fixed block of bytes. Existing installations keep their previous value - ADVANCED.en.md, section "The handshake never completes on cellular", explains how to fix them without reinstalling.
  • An older installation now hears about itself. The packet-shape change applies to new installations only: a reinstall over a live server deliberately keeps the parameters already set, because regenerating them would change a working server's settings without asking and would cut off every client config handed out so far. There is therefore nothing to migrate silently - but nothing to be silent about either: when the settings carry an I1 of random bytes, the installer says so and points at the documentation section about the handshake on cellular networks. Installations with the new packet, and installations without I1 at all (the --no-cps flag), see no message.
  • The I1 generator no longer emits a half-built packet silently. The function had no failure signal at all: its exit status is that of the last printf, which succeeds whatever the content is. With the randomness source degraded it produced a packet with an empty count in a tag and zero answer records, and such a value travelled into the settings file, into the server config and into every client profile, failing only at step 7 when the interface comes up. The packet is now checked before it is handed out, and an installation stops with a clear message on an invalid value, having written nothing.
  • The carrier diagnostic no longer scolds our own default. manage_amneziawg.sh diagnose compares the installed I1 against a carrier profile, and where the profile expects a random packet only the <r N> form was accepted. A shaped packet fell into "unusual format" and pushed the user back towards random bytes, that is towards exactly what was measured as not working on cellular. A structured I1 is now accepted, and the carrier profile is honestly marked as confirmed on the earlier form.

5.32.0 - 2026-09-08

v5.32.0 - the machine-readable interface reports client expiry, an installation records its protocol generation, and client routes can be set at creation time.

Added

  • Per-client AllowedIPs at creation: manage add --allowed-ips=LIST. Until now add only took --expires and --psk, and a new client always received the server-wide routing mode - custom routes could only be patched in afterwards with modify <name> AllowedIPs <value>. JSON-interface consumers (bots, scripts, panels) had to run two calls - add, then modify - deliver files only after the modify, and compensate for its possible failure with a warning (the awgram Telegram bot still works this way; Issue #253). Now a single call creates the client with the right routes from the start: the list of IPv4/IPv6 CIDRs is validated and normalized before the first client is created (an invalid or EMPTY value aborts the command creating nothing - an empty one does not silently widen the routes to the server-wide mode) and applies to every name in the batch, like --expires. When a full tunnel spells out IPv6 without ::/0, add warns right away - by the same condition regen uses (one shared predicate). The value follows the same rules as the global mode: a full-tunnel IPv4 list gets ::/0 added next to it (the iOS rule), a split list never does, and a list with explicit IPv6 tokens is written as is. Under the hood it is the CLIENT_ALLOWED_IPS env contract in generate_client/render_client_config, modeled after CLIENT_PSK; regen clears it and per-client routes of existing clients are unaffected. The list validation moved out of the modify dispatcher into the shared helper awg_validate_allowed_ips_list, called from both places - copies of such checks have drifted apart before. created entries in add --json results[] gained an allowed_ips field - the applied routes from the created .conf (with the ::/0 mirroring); for a bot this removes the verification call after creation.
  • Protocol generation marker AWG_PROTOCOL in awgsetup_cfg.init. The installer records the installation's generation in the settings file and, on every re-run that reaches configuration (--force / AWG_FORCE_REINSTALL=1, with or without a preset change, --no-cps or manual --jc*, and any run while the service is not active: resume after a reboot, the ARM module repair), reads it back from there and writes it out unchanged; a run without --force on top of a working server stops earlier and does not touch the file. Behaviour does not change: today the value is always 2.0, and a file without the field also reads as 2.0 - that is what every earlier install looks like. This prepares the ground for a second profile: an existing server must never change generation silently by any path, and tests pin it: the marker gets its value in exactly one place (after a hard reset), nothing else in the installer or the management script sets it, the settings file carries it through unchanged, and it cannot arrive from the environment. A corrupt marker (an invalid value, or a line that cannot be parsed) stops the installer with a clear message instead of being replaced by a default. restore warns before the service is stopped when the generation in the backup differs from the current one, when the marker cannot be read, or when the backup has no settings file. An installation marked as generation 3.1 is not reinstalled by this installer version: it has no 3.1 profile yet, and silently substituting the second generation would be exactly what the marker guards against.
  • list --json now reports client expiry (expires_at). The field is present in every record and mirrors the semantics of the add success record: unix time as a number for a client created with --expires, null for a permanent one. Previously only add returned it in results[], so an external interface (GUI, bot, monitoring) could not tell a temporary client from a permanent one without reading /root/awg/expiry/ on the server. The -v flag still does not affect machine output - it only changes the human-readable table. Requested in Issue #250.
  • A broken expiry marker no longer looks like the absence of one. Every record carries an expires_at_error flag: null in the normal case, and "unreadable" when the marker exists on the server but no value could be obtained from it - the file is empty, corrupted, unreadable, or a directory sits in its place. On its own, expires_at: null would mean "permanent by design", while an expiry was in fact set for that client. The reason it failed 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. Both fields are always present and express "not applicable" as null.

Fixed

  • An expiry marker with a leading zero caused a client to be deleted silently. The expiry check accepted any sequence of digits, and the comparison reads such a value as OCTAL: the marker 01750000000 became 262144000, that is 1978, the condition fired, and the client was removed along with its config and keys on a date nobody had set. A value containing an 8 or a 9 instead made the comparison fail with value too great for base, which evaluated false and kept the client - one and the same corruption behaving in two different ways. The expiry check, list --json and add --json now use one form: a canonical decimal of at most 15 digits. A leading zero, an empty marker, junk and an over-long number are all treated the same way - the expiry is undetermined, the client is not removed, and a warning goes to the log. The expiry check also no longer decides on data from a failed read: the exit status is captured, and on failure the client is left alone.
  • The client name from the server config is validated before it is used in a path. list read the expiry marker at /root/awg/expiry/<name> without validating the name, although the expiry check next to it does. A name containing ../ made it read a file outside the marker directory and publish its contents in the output. Such a name cannot come from add, but the server config is also edited by hand.

Documentation

  • ADVANCED: a warning about RandomTrailers. The flag itself does not cost speed: on a bare config the measurement was 11.94 MB/s both with and without it. Combined with the rest of the obfuscation, throughput came out at 0.09-0.14 MB/s, that is 85 to 133 times lower. The measurements are given per parameter combination, together with a local-network run (44.22 against 0.09 MB/s) showing the route is not involved. Do not enable it until a fix ships.
  • ADVANCED: why the DNS-mimicking I1 needs the <r 2> prefix. Those two random bytes are the DNS transaction ID. Without them the payload shifts and the packet stops parsing as DNS at all: the flags declare a query instead of a reply, the additional-records counter grows to 1641, and the first name label claims more bytes than remain. The second reason is that without the random prefix all 42 bytes are constant, so the packet is identical for everyone who copied the recipe from the documentation.

5.31.0 - 2026-09-02

v5.31.0 - the default install no longer lets IPv6 out around the tunnel.

In the "Amnezia" routing mode, which is the default, all IPv4 goes through the tunnel while the device's IPv6 kept going out directly with its real address. A blocked site with an AAAA record therefore stayed blocked with the VPN on, and on home Wi-Fi without IPv6 the very same profile behaved perfectly - hence the contradictory reports of the "does not work on mobile data" kind. The IPv6 of such clients now goes into the tunnel and dies there, and the device falls back to IPv4 on its own.

Fixed

  • The client's IPv6 went around the tunnel on a default install. The question "is this a full tunnel?" was answered by comparing the AllowedIPs string with 0.0.0.0/0. The "All traffic" mode matched and got its ::/0; the default "Amnezia" mode is written as a subnet list and never matched, even though it is a full tunnel by meaning: the list covers all public IPv4 and is spelled out as a list only to dodge an iOS bug (Issue #42). A full tunnel is now decided by route coverage, so the default install gets ::/0 just like "All traffic" does.
  • Re-issuing a profile now delivers the fix to already issued clients. The same fork sat on the regen path, which is why "update your profile" did not help before - the re-issued config still had no ::/0. To roll the fix out, run sudo bash /root/awg/manage_amneziawg.sh regen and re-import the profiles on the devices. Individual AllowedIPs set through modify are still preserved. Clients issued with --allow-ipv6-tunnel will not receive ::/0 from a plain re-issue: their IPv6 part is kept as an individual setting, and a warning now says so and names regen --reset-routes.

Changed

  • A full tunnel is decided by route coverage, not by string equality. One predicate replaces the three literal comparisons in awg_common.sh: a list counts as a full tunnel when it covers all public IPv4. Private and special-purpose ranges may be missing from it - those are meant to stay outside the tunnel. Your own network list (--route-custom) usually does not cover the whole public space and gets no ::/0, exactly as before; a list that does cover it counts as a full tunnel.
  • modify says so when it drops ::/0. The command still writes exactly what it was asked for: dropping ::/0 through it is how people get their IPv6 back. But when the given list is a full tunnel without ::/0, it names the consequence and the command that restores the route. This matters for ready-made recipes shaped like modify <name> AllowedIPs "$ALLOWED_IPS": the list in awgsetup_cfg.init is IPv4-only, so such a substitution takes the IPv6 route away from the client.
  • The price, worth knowing up front. While the VPN is on, resources reachable ONLY over IPv6 become unreachable, and the local network's IPv6 goes into the tunnel and dies there (the LAN stays reachable over IPv4). If you want working IPv6 through the VPN, use --allow-ipv6-tunnel on a server with native IPv6. If you want the local IPv6 left alone, use your own route list via --route-custom.

Added

  • tests/test_v5310_ipv6_full_tunnel.bats: 48 checks of the predicate and of all three places that call it, including client re-issue and a second re-issue (::/0 is not doubled), a multi-line list, a config with CRLF, and the edges of the non-public range table.

5.30.0 - 2026-09-01

v5.30.0 - install commands that do not go stale, and a guard for oversized masking parameters.

The install and update commands no longer name a version, so a line copied from an article or from a search answer installs the current release rather than the one that was current when the text was written. Diagnostics warn about oversized masking parameters before the interface read hangs on them. And the documentation answers the question people actually arrive with: which generation of the protocol a given client speaks.

Added

  • A dated facts block at the top of README.md and README.en.md: installer version, release date, recommended and end-of-life systems, architectures, where the kernel module comes from, and the profile of the generated configs. The block is built by scripts/update-facts-block.sh from repository data (SCRIPT_VERSION, the CHANGELOG heading, docs/support-matrix.json, the arm-build.yml matrix) instead of being written by hand, so it cannot drift away from its sources, and both language versions are rendered from the same values.
  • scripts/check-docs-consistency.sh: a check that the block stored in the READMEs matches what the generator produces right now. Runs in CI and in preflight.
  • tests/test_facts_block.bats: mutation tests for the guard, including the case where a third-line parameter reaches a config template while the block still promises the 2.0 profile.
  • A client generation matrix in ADVANCED.md and ADVANCED.en.md. The documentation could say whether a client supports AmneziaWG 2.0, but not which generation of the protocol it speaks, and that is what people ask now that the vendor app marks second-line configs in yellow. Four answers sit next to the table: why a config is marked yellow, where to get a third-line client on Android, whether a given router speaks the third line, and whether a third-line config can be converted back to the second. Two rows are deliberately weaker than the rest and say so: the light client on Google Play publishes no version number on its page, and WG Tunnel ships its engine as a prebuilt artifact, so the generation cannot be read from the manifest. For the third-party projects the table states plainly that their generation comes from their own documentation and code rather than from verified behaviour: it does not vouch for another project's implementation
  • A warning about oversized I1-I5. A total decoded size of masking packets above a kilobyte hangs the interface read: the dump stops making progress and repeats forever, the reading side grows in memory, and on a router it takes the device down. Our generator never produces that much (256 bytes at most), but configs are also edited by hand, and nothing warned about it anywhere. diagnose computes the size offline, parsing the configuration file before the first call into the interface: a check placed after that call would never print in exactly the case it was written for. If the size is judged unsafe the remaining interface calls are skipped, and the ones that stay are bounded by a timeout that reports itself rather than showing zero peers. An unrecognised tag in the config produces its own warning instead of an understated number: "could not compute" must not look like "computed, all good"

Changed

  • Install and update commands point at releases/latest/download. Thirty-two commands named a specific tag: ten each in README.md and README.en.md, two in each installation guide and eight more in ADVANCED.md and ADVANCED.en.md. A copied line went stale the day the next release shipped, and search answers held on to it for months: a measurement on 31 August 2026 was still handing out v5.14.1 from 19 May, thirty-seven releases back, with neither the critical package protection nor the /etc/fstab fix. The version-less address does not age in a cache or in training data, and it serves a signed release asset: .minisig files and KEYS.txt sit beside it, which a branch address cannot offer. Four pinned addresses stay on purpose: the ADVANCED section explains how the installer fetches its own helper scripts, and it fetches them by tag, so a version-less address there would make the section lie about the code it documents; CASCADE pins cascade/ru.zone, which is not a release asset at all. The guard in check-docs-consistency.sh is positive by construction: it requires at least the expected number of commands on the new address and zero on the old one, because a check that only looks for the bad form cannot tell a fixed file from an empty one

Fixed

  • No command reports an unread state as a measured one. When diagnostics cannot read the interface they say so, instead of the previous "I1 absent" - which was printed in exactly the case the guard exists for, an oversized I1. The carrier profile comparison is skipped entirely in that state: a green "I1 absent" tick would certify the very condition that made the check refuse to look. In the same situation list leaves clients at "No data", and no_data under --json, rather than declaring everyone without a handshake
  • Every interface call is bounded in time. check, stats, show, list, the diagnostics and the --diagnostic report can no longer wait forever, and a timeout is named separately from other errors so the cause is easy to find. That matters most for --diagnostic: the documentation asks people to attach its output to a report, and without a bound someone hitting the defect could not collect a report about it. The installer's own verification on a repeat run over a working server is bounded the same way
  • Sizing the masking packets no longer reports the unparsed as zero. A string with no recognisable tag, an unterminated bracket, junk between tags, content inside <t> or <c> where none belongs, and a count too large for shell arithmetic all used to return zero with a success status - and zero means "there are no masking packets", which switches the protection off. Each of those is now marked as "could not compute" and said out loud by the diagnostics. A tag preceded by a stray angle bracket also stopped being counted twice
  • Two stale claims about why third-line features stay out. The section pointed at the pre-release status of the Android client and at release 2.0.2 of the Windows client, when both have had full third-line releases for a while, and it promised that ContentPaddingAddition was waiting for an upstream fix, when that fix shipped on 5 August 2026 and has been verified on a test box. What is written there now is mechanism, which does not go stale when someone else ships: HeaderProtectionKey is two-sided and fails silently on a mismatch, with no handshake and no error from either end, so enabling it would cut off devices that update from an app store
  • A dead assertion in tests/test_diagnostic_report_secrets.bats. The assertion about masking secrets in the report was not actually checking the Russian installer: a negation inside a loop does not fail a bats test, so a defect in the Russian version passed as success. The assertion now takes a form that does not depend on how set -e behaves, confirmed by mutation: the old one went green on a defect the new one catches

5.29.0 - 2026-08-30

v5.29.0 - signed releases and one source for supported systems.

Releases are now signed and published atomically, the list of supported systems became machine readable, and the SHA pin gate checks what the installer actually downloads.

Added

  • Release signing. The six executable scripts are signed with minisign on the maintainer's machine; the private key never reaches GitHub Actions. Every release carries six .minisig files and KEYS.txt with the public key, and the workflow refuses to publish if any signature fails to verify. A downloaded file can now be checked independently: minisign -Vm install_amneziawg.sh -P <key from KEYS.txt>. The trusted comment names both the tag and the file, so a signature from a previous release does not fit a new one and one file cannot be swapped for another inside the same release. Design and threat model: docs/SIGNING_DESIGN.md
  • Releases are assembled atomically. A release used to be created first and filled with files afterwards, and in between releases/latest/download/<file> answered 404 - the very address the documentation tells people to use. Now the release is created as a draft, receives every asset, has its asset count checked against the expected number, and only then becomes visible. An interrupted run can be repeated with the same tag: a draft means "resume", an already published release means "do nothing". Title and notes are rewritten on a repeat, otherwise a corrected changelog would never reach the release
  • docs/support-matrix.json, a machine readable list of supported systems. The same list of operating systems lived in thirty nine files and drifted quietly, with nothing to arbitrate between the copies. It now has a single source, from which the documentation checker derives the set of systems. A separate check recomputes each system's support state from the vendor dates, which catches a class that comparing documents with each other cannot catch at all: the external fact changed while the documents still agree with one another. That is exactly how the end of Debian 12 regular support went unnoticed
  • scripts/sign-release.sh, signing a release in one command. The trusted comment has to read exactly amneziawg-installer <tag> <file>, and a typo in it produces a signature that verifies cryptographically yet is rejected by the release, which is discovered only after the tag is pushed. The script composes that comment itself, validates the tag format before asking for the password, asks for the password once for all six signatures, and finishes by running verify-signatures.sh, so "signed 6" never rests on minisign merely not crashing. It also refuses to run without a terminal: the manual loop in such an environment died at the password read, silently, having signed nothing and said nothing
  • A commit hygiene check in CI. Co-authorship trailers and mentions of outside tooling in commit messages are now caught by a workflow of their own, not only by the local preflight

Changed

  • Advice on choosing an operating system distinguishes three states rather than two. The installer and the documentation used to list five systems as equals. They now say it plainly: for a new server pick Ubuntu 24.04 LTS or Debian 13, and Ubuntu 26.04 will do as well; Ubuntu 25.10 and Debian 12 install and work, but their regular support has ended and they are not a good pick for a new server. The wording is identical in Russian and English - until now the Russian guide warned about Ubuntu 25.10 while the English one offered it as an ordinary choice
  • The AmneziaWG versus WireGuard comparison table no longer makes undated absolute claims. Two cells asserted what the project cannot back up
  • Recovery diagnostics no longer suggest installing systemd-resolved on a hunch. On Debian with a static resolver or with resolvconf that package may never have been installed, yet the recovery command named it unconditionally

Fixed

  • The SHA pin gate compares each pin against what the installer really downloads. It used to compare a pin with the helper file in the working tree, while the installer fetches the helper from the tag of its own version. That was wrong in both directions: red on main after a helper was touched without a version bump, although every published installer was correct, and green in the one genuinely dangerous state, where the pins had been recomputed from the tree while the tag already existed - an installer shipped with such pins would refuse the download it is supposed to accept. scripts/update-sha-pins.sh now applies the same criterion, since two gates answering differently about one state would make the preflight green and red at once. Both tools state out loud which comparison they made
  • Private issue tracker references were removed from the public tree. One hundred and fourteen mentions that meant nothing to an outside reader

5.28.1 - 2026-08-27

v5.28.1 - the step 1 system upgrade can no longer remove an installed package.

Fixed

  • The step 1 system upgrade is no longer allowed to remove installed packages. The v5.28.0 guard caught the loss after the fact and tried to put the packages back. The two logs posted in issue #223 show two different failures. On one server apt full-upgrade SUCCEEDED and still dropped six of the eight protected packages: the manual mark does not forbid removal, it only clears the "no longer required" status. On the other the same command failed while resolving (netplan.io : Depends: udev but it is not going to be installed), removed nothing, and the repair never ran at all. What they share is the command itself: full-upgrade is by definition free to choose removal when that makes dependency resolution easier. Step 1 now runs apt-get upgrade --with-new-pkgs, which has no such right at all: a package that cannot be upgraded without removing a neighbour simply stays at its current version. The --with-new-pkgs flag keeps the only reason full-upgrade was needed here: a new kernel arrives as a package with a new name (linux-image-6.8.0-NNN-generic), and a plain upgrade refuses to install new names. The trade is deliberate: a VPN server does not need the freshest environment, it needs a guarantee that it boots. The pre-reboot verification remains as the second line of defence. Both installer versions affected
  • Package repair now runs as a single transaction, with the per-package pass kept as a fallback. What differs is WHAT the resolver treats as a goal. Installed one at a time, the other lost names are not goals and apt is free to leave them absent; in one command they all become goals and it looks for a version set that suits the group as a whole. In issue #223 the per-package pass produced five refusals, among them udev : Breaks: systemd (< 255.4-1ubuntu8.17) but 255.4-1ubuntu8.12 is to be installed, and a single transaction was never tried there. One transaction with every lost name now runs first, and only then, for whatever is still missing, the per-package pass. It is kept on purpose: a single name with no installation candidate aborts the whole transaction, and then nothing is restored, including packages that install perfectly well. A single transaction is not a guarantee either: if the old version is pinned by a package that is not in the lost list (in issue #223 that was systemd-resolved, pinning systemd to 8.12 through a dependency on an exact version), it does not become a goal there either. The final verdict still comes from the full-set check before the reboot
  • The fatal message about a failed upgrade no longer names a cause it did not observe. The previous wording blamed a busy dpkg lock unconditionally, including when the fuser check two lines above had found nothing. On the fourth server in issue #223 the cause was a resolver refusal (netplan.io : Depends: udev but it is not going to be installed), it was on screen, and the message sent the investigation elsewhere. There are now four outcomes and each is named for what it is. The lock: printed only when a holder is actually present, and measured again inside the diagnosis itself, because dpkg --configure -a and a second attempt run between the first check and this point and the process found earlier may be long gone. Dependencies do not resolve: apt's real answer is shown, obtained from a dry run under timeout. The dry run succeeded: its happy output is NOT passed off as the reason, the message says plainly that dependencies are not the cause and lists the usual suspects instead. A fourth and nastier case is closed separately: the dry run failed but printed nothing at all, for instance because timeout killed it. The previous logic filed that under "dependencies resolve", turning an unknown into a confident wrong diagnosis. It now prints the exit code and says plainly that the check did not answer. All of this reasoning moved into its own function: while it lived inside step 1 the tests could only grep it, and an outside review showed that deleting the second measurement, inverting the condition, dropping -s and dropping timeout all went unnoticed by the whole suite
  • The manual recovery advice is corrected both in the log and in ADVANCED. Both told the user to install the lost packages one at a time, which is exactly the approach that does not work in this scenario. The recommendation is now a single command naming all of them, without -y, so that the list of proposed removals can be read before confirming
  • The installer says out loud when not everything was upgraded. The flip side of moving to apt-get upgrade: a package whose upgrade would require removing a neighbour now stays at its current version, and apt returns zero while doing so. That is safe for booting the server and exactly what was wanted, but staying silent about it is not acceptable: with full-upgrade this outcome was rare, with upgrade it is routine, and without a dedicated line it would pass entirely unnoticed. The list of packages left behind is printed after the upgrade. The wording deliberately does not present itself as exhaustive: a package can be held back by the new mode, by Ubuntu's phased rollout, by an explicit hold, or by a stuck dependency chain, and this line cannot tell them apart. So it does not declare the remainder safe, it points at what to run to find the reason

5.28.0 - 2026-08-26

v5.28.0 - the step 1 system upgrade can no longer take udev with it and leave the server unable to boot.

Fixed

  • The step 1 system upgrade could remove udev and leave the server unable to boot. The package cleanup purges its own list, and apt takes the ubuntu-server meta-package along as a reverse dependency. On images where that meta-package was the only manual root, everything hanging under it becomes "no longer required", udev and initramfs-tools included: on a stock cloud image both are marked auto-installed. The apt full-upgrade that runs about a minute later is free to drop such a package instead of upgrading it, and on a system months behind on updates that is exactly what it does. In issue #223 a single transaction removed 34 packages, udev among them. Without it there is no /dev/disk/by-label, so systemd never finds the partitions that fstab refers to by label, waits the default 90 seconds for them (both in parallel, not one after the other) and drops into emergency mode, on the very reboot that step 1 triggers. The server stays powered and healthy while being unreachable over the network. Now the still-installed members of a boot-critical set are marked manual again before the upgrade, and after it the set is compared against the snapshot: anything missing is reinstalled, and if that fails the installer stops while the server is still reachable instead of rebooting into a system that cannot come back. Manual rather than hold on purpose: a hold would block the upgrade of those very packages, which is not what anyone wants, while manual only clears the "no longer required" status. The pre-existing apt-mark hold inside the cleanup did not cover this: its list names neither udev nor initramfs-tools, and it is released before the upgrade, which is why netplan.io is on that list and was removed anyway. Both installer versions are affected
  • The protected package list survives an installer restart. The installer itself asks the user to run it again after a failure, and by then a package may already be gone: apt can remove udev and then fail, in which case the check at the end of the step never runs at all. A snapshot recomputed from scratch on the next run will not see the removed package, and the check will compare the system against an impoverished baseline, staying silent about exactly the state it was written for. That is what the issue #223 case looked like: the user re-ran the installer six times. The list now lives in /root/awg/boot-critical.pkgs and is merged with the previous one on every run, so it can only grow. Only known names are read back from the file: a corrupted or substituted file must not turn into a list of arbitrary packages to install
  • The snapshot and the verdict answer different questions and now use different criteria. A package left unpacked or half-configured counts as present for the snapshot: it is there and has to be protected. For the verdict before the reboot it does not: initramfs-tools left unpacked means postinst never ran and no initramfs was built for the new kernel, so the server will not boot even though the package formally exists. Both questions used to be answered by one predicate, which in that case said everything was fine
  • Step 2 is checked before its own reboot, just like step 1. It installs packages and reboots the machine as well, so it needs the same line of defence
  • The swap entry in /etc/fstab could be glued onto the preceding line. optimize_swap appended /swapfile none swap sw 0 0 with >> without checking whether the file ended with a newline. When it did not, and some hoster images ship it that way, the entry merged into the last fstab record, and instead of two valid lines the file got a single one of 11 fields instead of six. Worse than the damage itself was how quiet it was: fstab is read on the next boot, so the failure surfaces only after the connection to the server is already gone. The last byte of the file is now checked before writing, and a newline is added when needed. On the stock Ubuntu image the defect did not trigger, because its fstab is properly terminated. Both installer variants fixed
  • The diagnostic report printed PresharedKey in clear text. The report is meant to be pasted into a public issue, our own template asks for exactly that, and it masked precisely one line: PrivateKey. Yet PresharedKey lives in that same server config, written there whenever a client is created with --psk. So a user who had enabled the extra layer on top of AWG 2.0 published the preshared keys of every peer, destroying the one thing that layer was there for. Key values are now stripped by one shared function applied at two points: at the report boundary, and separately to the server config as the riskiest raw input. That is not a second source of truth, whereas the previous masking inside a single section did drift apart from the rest, which is how this defect appeared. The filter is case insensitive because config parsing in amneziawg-tools is case insensitive too (config.c, get_value through strncasecmp), which makes presharedkey = ... a valid line. An exhaustive search across all 168 issues and pull requests and all 55 discussions of this repository found no exposed PresharedKey or PrivateKey value. That does not amount to "nothing leaked": deleted and edited comments, attachments and anything outside this repository are not covered by such a search
  • A key value could reach the report by two more routes, both through the journal. On an unknown key awg prints the line itself to stderr (Line unrecognized: ...), already stripped of whitespace and wrapped in a backtick, and on a malformed key it prints Key is not the correct length or format:, which carries no key name at all. Both messages land in the journal, which the report prints as a section. Each is now covered by its own filter expression
  • A failed filter or a failed write is now fatal. Previously the failure was logged and execution continued, so the --diagnostic branch ended with exit code 0 and the last line on stdout was a cheerful "Report: ". The error went to stderr, so anyone redirecting output or checking the exit code saw success only, while the user got an empty file and attached it to an issue. It is now a die. Note that this path could never produce an UNFILTERED report: the filter is the only writer of the file
  • The report file is created with mode 600 rather than repaired afterwards. --diagnostic runs before the installer narrows the permissions on /root/awg, so the window in which the file sat with default permissions was reachable. A umask 077 is now set for the duration, the same idiom already used for keys
  • The server config section now has a fallback line. If awg0.conf cannot be read or filtered, the report states so explicitly. Previously the section was simply empty, which is indistinguishable from an empty config and sent triage in the wrong direction
  • A value split by whitespace is masked in full. Config parsing strips whitespace before parsing (config.c, config_read_line), so a record like PrivateKey = AA BB= is valid. Cutting at the first space would have left the tail of the key in the report, so the value is stripped to end of line
  • awg show prints HeaderProtectionKey in clear text, and it reached the report too. The private and preshared keys are hidden by awg show itself, but the header protection key is printed as is (upstream src/show.c: key() instead of masked_key()). The installer does not configure third-line parameters, but nothing stops anyone from adding them by hand, so the filter covers that spelling as well

Changed

  • The report header and the issue template no longer create a false sense of security. Both listed only IP addresses, ports and routes as sensitive, from which a reader reasonably concluded that everything else had already been taken care of. Both now name what is stripped (key values) and what is kept on purpose: IP addresses, ports, routes, obfuscation parameters, client names and public keys. Names and public keys are left unmasked deliberately: a public key is not a secret by construction, and without it the report cannot be used to correlate peers, which is what it is read for

Added

  • ADVANCED.en.md: two new blocks in the troubleshooting section. The first covers a server that stops answering after the reboot in step 1: how to reach the provider's console, how to tell a broken network from emergency mode, how to read three diagnostic blocks, and three repair paths including the nofail workaround for when udev cannot be restored. The second explains the new installer stop that lists missing packages: what /root/awg/boot-critical.pkgs is, why the installer refuses to reboot, and three causes in order of how often they apply. Prompted by issue #223, where a user lost access to a server for eleven days while the documentation said nothing about this case
  • tests/test_fstab_trailing_newline.bats (9 checks): static ones proving the guard is present in both installer variants, and functional ones that lift that block out of the shipped script and run it against a scratch file. A copy of the logic living in the test would have stayed green after the guard was removed from the installer, so there is none.
  • tests/test_diagnostic_report_secrets.bats (14 checks): static ones proving the filter is present in both variants and that the report really goes through it, and functional ones that lift the filter out of the shipped installer and run fourteen spellings through it: plain, lowercase, uppercase without spaces, commented out, a value split by whitespace, an empty value, a CRLF line, HeaderProtectionKey in the config, two journal forms (bare and with a systemctl status prefix), the malformed-key message, and the three awg show forms. Every sample line carries its own marker, so a failure names the exact form. A separate check pins down what is NOT masked: public keys, a client name, a path such as PrivateKeyFile = /path, obfuscation parameters, and a server name, which may itself contain the text PrivateKey = ... - that is where the unanchored version of the filter broke
  • tests/test_boot_critical_packages.bats (47 checks): asserts the order, not just the presence. apt-mark manual must come before the upgrade and the comparison after it but before the reboot; presence alone would not be enough, since the pre-existing apt-mark hold was present too and still did not help. The functional part lifts the real blocks out of the shipped installer rather than reimplementing them. Checked by feeding it deliberately broken installer versions: removing any of the four guards, moving the check off its last-action slot, downgrading its die to a warning, dropping a package from the list or loosening the dpkg status parsing all turn the suite red

5.27.1 - 2026-08-23

v5.27.1 - manage regen no longer collapses the AllowedIPs and DNS lists, and configs damaged by earlier versions are repaired by running the same command again.

Fixed

  • regen silently collapsed the lists. The installer writes AllowedIPs and DNS with a space after each comma, while regenerate_client read the current values from the client .conf through tr -d '[:space:]', stripping the spaces together with the carriage return, and wrote what it had read straight back. The very first regen turned 1.0.0.0/8, 2.0.0.0/7 into 1.0.0.0/8,2.0.0.0/7, and that form then survived every later reissue. The difference is invisible to the eye, and in the default "Amnezia List+DNS" mode it covers 34 entries. The values are now normalised to the canonical a, b, c form, so a repeated regen not only preserves the format but also restores it in configs damaged by earlier versions. Verified on a test server: after regen the damaged config matched the original byte for byte. Thanks to @humowns for the find and for checking the routes on his side (discussion #38)
  • manage modify stored the list exactly as it was typed. The value was validated element by element but written verbatim, so modify NAME AllowedIPs "a,b" left a collapsed variant of the same list in the config. AllowedIPs and DNS are now normalised here as well, and the entered value and the normalised one are logged separately
  • Repeated AllowedIPs and DNS lines are no longer lost. wg allows those keys to be given on several lines, and the values add up. The old code glued them into a plainly invalid address, so awg-quick refused to bring the interface up and the error was loud. The lines are now joined into one canonical list, and the join itself is reported, so a hand edit cannot disappear unnoticed
  • modify can no longer wipe a setting. The post-write check required only the Key = prefix, so an empty value passed as success while the backup was deleted. The value must now be non-empty. A separate check makes sure awg_common.sh is not older than the script: the version gate between the two halves deliberately tolerates a patch-level drift, the normaliser arrived in exactly such a patch, and on a half-updated server a call to the missing function would have wiped the list of routes
  • Whitespace inside a list element is cleaned again. A value like 1.1.1. 1 used to be cleaned by the old tr by luck, and trimming edges only would have preserved the typo. Whitespace is now stripped inside the element, the same way the modify validator does it

Clarified

  • The vpn:// link did not and does not damage this format. The client config is inlined into it from the file as it is, so a collapsed embedded config always has the same cause: a .conf damaged by an earlier regen. The separate structured allowed_ips field is deliberately built from the compact form, because it becomes a JSON array and a space would end up inside its elements. While preparing this release the normalisation was mistakenly applied there as well: on the test server that put a leading space into 33 of the 34 entries. The change was reverted, and both the code and the tests now record why spaces are stripped in that one place

Added

  • tests/test_allowedips_dns_spacing.bats (19 checks): the behaviour of the normaliser, three functional regenerate_client runs, two functional manage modify runs and two decodes of a real vpn:// link asserting that no array element starts with a space. The earlier revision checked some of these properties by grepping the source, and such a check stayed green when the defect was reintroduced. The expectation in tests/test_issue170_route_mode_change.bats was corrected too: it had pinned the collapsed list down as the norm

5.27.0 - 2026-08-14

v5.27.0 - the installer no longer wipes system packages silently: it prints the list, warns about the snaps and asks for consent, and there is now a dedicated flag to opt out.

Added

  • Consent before removing system packages. The server is set up as single-purpose, so the installer clears out what a VPN does not need: snapd, modemmanager, networkd-dispatcher, unattended-upgrades, packagekit, udisks2, lxd-agent-loader, plus cloud-init when it does not manage the network. The removal was a purge followed by wiping the /snap, /var/snap and /var/lib/snapd directories, all without a single question: that snapd had taken the installed snaps and their data with it was something you found out afterwards, and rm -rf does not roll back. Now, at step 0 and together with the other questions, the installer prints the packages that are actually present, warns about the snaps on a separate line, and names the snaps you installed yourself. A separate line mentions /etc/cloud and /var/lib/cloud when cloud-init is due for removal. The question belongs to step 0, where the other questions live: everything after that should run without a human present. The answer is stored in 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. Thanks to @kemko for raising it (#213)
  • Any failed check means "do not remove". No terminal available (ssh host bash ... without -t, ansible, user-data, cron), dpkg not answering, an unreadable snap directory, a mangled value in the config - each of those used to collapse into a quiet zero and lead to removal. All of them now mean keep, plus a log line saying the check did not happen. The answer parsing changed with them: when there is something to lose, removal takes an explicit y, yes or да and nothing else, whereas the earlier version tested the answer for the Latin letter n, so "no thanks", a stray key or any answer in another language meant REMOVE, on a prompt that said [y/N]
  • The default answer depends on whether there is anything to lose. When snaps you installed yourself are found, keeping the packages is offered and an empty answer means exactly that. With no snaps of your own the default is unchanged, that is remove. The base snapd, core*, bare and lxd do not count as yours: they ship with the image and by themselves do not mean anything is at stake. The snap list is read from the files in /var/lib/snapd/snaps rather than from snap list, because the snap binary may already be gone from an earlier run and the list would silently come back empty
  • New --keep-packages flag: leave the packages alone, keep everything else. Until now the only way to opt out was --no-tweaks, which also turns off UFW, Fail2Ban, the sysctl hardening and the optimization, so the price of opting out was out of proportion. The new flag skips the system cleanup only. It is also what an unattended install needs: with --yes no question is asked and the packages are removed as before, so keeping them without a human present takes an explicit --keep-packages

Fixed

  • The --help text hid the fact that --no-tweaks also cancels the package removal. It read "skip optional hardening/optimization (UFW, Fail2Ban)", which implied the cleanup happens either way. It did not: the flag suppressed that too, so the only available way to save the packages was described in a way that made it impossible to find. The help text is corrected in both installers, and so is its copy in ADVANCED.md and ADVANCED.en.md

5.26.0 - 2026-08-13

v5.26.0 - cascade: two simultaneous runs of the routing script no longer break each other, your own addresses can be routed through the Russian leg, and the diagnostic report finally shows whether the split is applied.

Fixed

  • Two simultaneous runs of awg-routing.sh broke each other, and one of them died halfway through. The script from CASCADE.en.md built the set of Russian networks through a temporary ru_tmp with a fixed name and no locking at all. With two instances running, the one finishing first destroyed ru_tmp from under the second, and the second failed with ipset v7.17: The set with the given name does not exist. Where the second run comes from: the awg-routing unit starts at boot, a reinstall reboots the machine, loading ~8600 networks goes one ipset call per network and takes seconds on a single core, so a manual run right after logging in over SSH overlaps the one still going from boot. The script now takes a lock on /root/awg/awg-routing.lock, and the second instance WAITS up to 5 minutes instead of breaking. Verified on a Debian 12 box with ipset 7.17, the same combination the user hit it on: without the lock one of the two instances failed in 3 runs out of 3, with the lock both reach the end in 3 out of 3, and the wall time roughly doubles, which is the visible sign that the runs queued up. 🔴 There turned out to be two races, and the second one only surfaced on the test box. Both instances wrote the list into a shared ru.zone.tmp and moved it into place with mv, while the trap of the first deleted the file of the second: in one run out of three the failure looked like mv: cannot stat '/root/awg/ru.zone.tmp': No such file or directory, a different message on a different step. The lock closes both, because it covers downloading the list, working with ipset and applying the rules. Thanks to @aya2work for the report (#212)
  • The downloaded list of Russian networks is now validated by content, not just for being non-empty. curl -f only rejects codes 400 and above, so a provider stub page, a DPI page or a captcha served with code 200 passed as a valid list: the working file was overwritten with garbage, the first malformed address then killed the script before the routes were applied, and every subsequent run failed the same way until someone deleted the file by hand. The script no longer replaces the working list when the response has fewer than 1000 CIDR-shaped lines or contains anything else. Verified on a test box with a server serving HTML under code 200: both URLs were rejected, the previous list stayed intact and the split kept working. --retry-max-time was added as well: it forbids starting a new attempt past the deadline, so an unreachable source no longer stretches a run to a minute and a half instead of predictable seconds
  • The final line of the script no longer says OK when some of your addresses did not make it. Failed EXTRA_RU_NETS entries are counted, and with a non-zero count a separate line reports how many were not added and states plainly that those addresses will go through the foreign leg. Previously there was only a warning further up the output while the last line reported success, so skimming the output bottom-up missed the failure entirely. ⚠️ The exit code is still zero and the diagnostic report still does not show rejected entries: that is left for later on purpose, because changing the exit code of a script tied to Requires= needs its own change and its own verification

Added

  • The diagnostic report gained a cascade section, so the report now shows whether the traffic split is applied. The cascade lives outside awg0.conf - it has its own routing table, mark, ipset set and mangle rules - while the report only showed the main routing table. Because of that a submitted report could not tell a working split from a broken one, and triage turned into a conversation. The report now includes the state of the awg-routing unit, the ip rule by mark, the contents of table 100, the list of ipset set names with the entry count for ru, the age and size of the ru.zone file, the cascade rules from mangle and the NAT rule on awg1. That last one matters most: the script applies MASQUERADE on awg1 LAST, so a run cut short leaves every other sign in place, and without that line the report would show a healthy cascade while the foreign leg is in fact dead. When there is no sign of a cascade, the section is a single line and the report does not grow for nothing. The ru set is deliberately not printed in full: that is 8600+ lines which would make the report unreadable, and the diagnostic value is in the count alone, where zero entries mean a silently broken split
  • Your own addresses can be routed through the Russian leg: the script gained an EXTRA_RU_NETS variable. It exists for the case an address list cannot fix: a site in the .ru zone behind a foreign CDN looks foreign by destination address and leaves through the far leg. Addresses from EXTRA_RU_NETS are added to the same temporary set as the downloaded list and land in the working set together with it. There is also an explicit note on why appending an address to the live ru set is pointless: the next run of the script rebuilds the set and wipes the additions

Documentation

  • The Verification section now says what the message The set with the given name does not exist means. The ipset list ru command was already there, but what it prints when the set is missing, and what follows from that, was not. It now says it plainly: the set is gone, so the split does not work, but the command itself only reports the set missing right now and not the reason (a run cut short, or a unit that never started after a reboot). The cost of a cut-short run is spelled out separately: the ipset block runs before the routing part, so in that run neither the table nor the marking nor the NAT were applied. The same place removes a trap that has already caught people: ip rule and ip route show table 100 can look correct at that moment, because they persist in the kernel from an earlier successful run and say nothing about the current one

5.25.0 - 2026-08-08

v5.25.0 - reversibility before AmneziaWG 3.0: a lost-access warning, a removed parameter no longer disappears silently, and the module version comes from the loaded one.

Added

  • A warning before any operation that restarts the interface: you can cut yourself off from the server. The installer offers routing modes where all traffic goes into the tunnel, its audience is people without Linux experience, and some of them reach the server over SSH through that very VPN. Such a person ran manage restart, lost access and had no idea why: there was no warning in any of the six scripts. The script now determines whether the current session goes through the tunnel and says so plainly, naming the console or VNC in the provider's panel as the fallback, since those work independently of the VPN. Detection uses two paths, because $SSH_CONNECTION alone is not enough: the script runs through sudo, sudo does env_reset by default, and that variable is not in the Debian/Ubuntu env_keep list - so the second path is who, matched against the script's own tty. There are exactly three states rather than two: "through the tunnel", "not through the tunnel" and "could not determine" (for instance who returning a hostname instead of an address with UseDNS yes); the last one prints the generic warning, because presenting a guess as a fact is worse than saying "unknown". The warning is printed BEFORE the confirmation prompt, so it is visible with --yes too: a non-interactive run cuts you off just as effectively. 🔴 The tunnel subnet is NOT guessed: it comes from the live interface (ip -4 -o addr show awg0), failing that from the Address line of the server config, and only then from the environment variable; if nothing is found the verdict is "unknown". That matters because manage restart does not load awgsetup_cfg.init: with a substituted literal 10.9.9.1/24 anyone who installed with --subnet would get a confident "you are not through the tunnel" in exactly the scenario the check exists for. A substituted default turns "there is no data" into "there is data, and it says this". Warnings were also added to the two apply_config branches where a restart happens UNEXPECTEDLY - when awg-quick strip or awg syncconf fails during a routine add/remove: there the operator is not expecting a drop and gets one, along with the loss of their own SSH session
  • A removed interface parameter no longer disappears silently: it is reported. Measured on module 3.0.20260731-04: Jc, S4, H1, I1, ContentPaddingAddition and RekeyAfterTime that had been set stayed on the live interface after applying a config without them. For AWG parameters setconf and syncconf only ever add, so the operation "drop the line from awg0.conf and apply" silently did nothing: the file changed, the interface did not, and nothing caught that divergence. It affected nobody so far - our add/remove/regen only touch [Peer] sections, and the single hint about deleting an I1 line already advises a restart - but the trap would have opened silently as soon as 3.0 parameter management arrived. The scripts now remember the set of parameter NAMES applied last time, and if a name has disappeared they print a warning together with the command that carries the removal through to the live interface. 🔴 The restart is NOT performed automatically, and that deliberately replaces the first revision of this change. An automatic restart closed the trap but opened a worse one: it drops EVERY client connection, and the snapshot can fall behind through no fault of the user - if someone drops the line and applies it with manage restart, the interface is already recreated and the parameter already cleared, yet the next ordinary add would see the "removal" a second time and cut everyone off again. A false warning costs a log line, a false restart costs everyone's connection, so the decision stays with the human. The snapshot is refreshed after a SUCCESSFUL apply, and also after manage restart, manage restore and an install - after everything that recreates the interface, because otherwise a parameter added that way would make a future removal invisible (the comparison would read "absent against absent"). If the apply failed, the snapshot stays as it was and the warning repeats: refreshing it on failure would permanently mute the reminder while the interface had not changed. Names are compared, not values: syncconf applies values correctly. The set is deliberately not compared against the live interface - awg showconf prints neutral values too (S4 = 0, H1 = 1), so that comparison would fire on every apply. An empty set against a non-empty previous one does not count as removal: our generator always writes Jc/S/H, so emptiness speaks of a file being rewritten at that moment. The snapshot write is atomic (temp plus mv), because a truncated write would read as "the parameters were removed"

Documentation

  • Documented a layer that was missing from every guide: a site can learn a visitor's address from a script on the page rather than from the connection. Such a script asks a third-party IP lookup service, and the request to that service goes to a different address, so it may leave through a different leg than the site itself - and then the site holds two different addresses. ADVANCED.md/ADVANCED.en.md gain a section, "What a site can see when traffic is split by destination", covering the consequence for each of the three schemes: a client-side AllowedIPs list with networks deliberately excluded exposes the home address, the cascade exposes only your own servers (as long as the device's IPv6 is not going around the tunnel), and with the WARP scheme the script reports exactly the datacenter address the scheme was routing around. There is also a plain answer to "just add those services to the exception list", an unreliable path: 24 lookup-service domains were resolved through two resolvers (dig @8.8.8.8 and @77.88.8.8) and run against the cascade/ru.zone snapshot (8626 networks, checked on 7 August 2026) - two domains landed inside it, and they are one and the same Yandex service under two names, while the other 22 sit outside Russian networks; a name in the .ru zone says nothing about the address itself; and api.ipify.org handed out three different addresses within a single day, being anycast behind Cloudflare. CASCADE.md and WARP-RU.md (both languages) gain per-scheme bullets under Troubleshooting and Limitations. What the measurement does NOT prove is stated too: the mismatch does not always appear (land the service on the same leg as the site and there is one address), it is no proof of a VPN on its own (a dual-stack visitor can open a site over IPv6 while the service answers over IPv4), how often sites use the trick was not measured, and the numbers come from the cascade's snapshot - the WARP scheme has a list of its own. Thanks to kuza2000 for raising it in the discussion under the Habr article
  • Separated two levels of IPv6 that the guides had run together. "IPv6 is off, that is how the installer sets it up" always meant IPv6 on the server; IPv6 on the client device is something the installer never touches, and the traffic split does not apply to it (neither the cascade's list of Russian networks nor the WARP scheme's BGP feed carries IPv6 entries). Whether the device's IPv6 goes around the tunnel is decided not by the scheme but by whether AllowedIPs carries a ::/0 route: a full tunnel without --allow-ipv6-tunnel (the default) gets 0.0.0.0/0, ::/0, so IPv6 goes into the tunnel and dies there; split routing never adds ::/0, so IPv6 goes around with its real address and the site sees it directly, no scripts needed; and with --allow-ipv6-tunnel enabled the rules depend on the server's native IPv6 and live in the dual-stack section, which remains their only source. Readers get a one-command check: curl -6 ifconfig.co from a connected device - looking at the address itself rather than at the fact an answer arrived, since with IPv6 working inside the tunnel one arrives too. An inaccuracy in the IPv6 tunnel section is fixed along the way: it said the device's IPv6 in IPv4-only mode always goes out directly, outside the VPN, which is not true for a full tunnel
  • Stated plainly that the pinned 2.0 line receives no updates, and that this is a 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; the same applies to the prebuilt ARM packages. So on Debian 12 with its stock 6.1 kernel, and on ARM with a matching prebuilt, updates simply do not arrive, and there is no "move 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. The docs also say that nothing breaks meanwhile, and name the single supported route to the third version: a kernel 6.7 or newer (on Debian 12, from backports) plus a reinstall. And separately for ARM: the prebuilt package is tried first, so if one exists for the new kernel, the pinned 2.0 arrives again

Changed

  • The README showcase now says AmneziaWG 3.0. On x86 with kernel 6.7 or newer the installer ships the third version from the PPA, the README documents that in its own section and the header badge reads "2.0 | 3.0" - yet the logo, the heading, the subtitle and the alt text were still on 2.0. The logo is replaced (1200x675 instead of 1280x640, JPEG quality 95, no chroma subsampling: over the red "3.0" text, moving to 4:2:0 drops PSNR from 41.4 to 31.4 dB while saving only 19% of the file size, which is why 4:4:4 stayed). The heading, the subtitle and the alt text in both languages now read "AmneziaWG 2.0 / 3.0": the precise information sits next to the image, because 2.0 remains what Debian 12 and ARM with the prebuilt module get. The "what it does" line under the install command is fixed along the way: it promised AmneziaWG 2.0 unconditionally even though modern kernels get 3.0, and it now names no version, pointing at the section that covers it instead

Fixed

  • On ARM with kernel 6.7 or newer, the 3.0 module from the PPA could land next to the prebuilt 2.0 one. The chain: apt-mark hold was set only on the pinned path, that is with a kernel older than 6.7, while the branch for newer kernels ran apt-mark unhold; the ARM block with the prebuilt package runs AFTER that gate and does not depend on it. The prebuilt package is named amneziawg-kmod-<KERNEL_ID> and declares no Provides: amneziawg-modules, so it does not satisfy the alternative in amneziawg-tools' Recommends, and amneziawg-modules itself is absent from the PPA (verified against the live Packages.gz metadata for noble, amd64 and arm64). The tools are installed via apt install -y with recommends, so apt pulled amneziawg-dkms - leaving two trees carrying a module of the same name on the system: the pinned 2.0 in extra/ and 3.0 in updates/dkms/. That is exactly the state the hold exists to prevent. The scenario is reachable: the prebuilt packages for ubuntu-2510-arm64 and debian-trixie-arm64 are built against kernels 6.7+. The hold is now set on this path as well, regardless of kernel version, and its success is verified - if the package cannot be pinned, the install stops with an explanation instead of quietly installing a second tree. The POSTCONDITION is checked too: the hold is a precondition, and nobody was looking at the result, even though step3_check_module reads vermagic and the modinfo output and would therefore pass on the "wrong" module as well. 🔴 How that state actually ends was measured on a Debian 13 ARM64 bench rather than guessed: as soon as kernel headers appear, DKMS builds, displaces the prebuilt file from extra/ (leaving the directory empty), and after a reboot it is the one that loads - the server silently moves to the other protocol line while the tunnel keeps working and dpkg still believes the prebuilt package is installed. The earlier wording "which of the two trees loads cannot be predicted" is replaced by the measured outcome. The hold itself was verified live: an explicit apt-get install -y amneziawg-dkms on an installed bench is refused (Held packages were changed), so no second tree appears at all
  • check and diagnose could report a module version that is not in the kernel. Both commands asked modinfo amneziawg, and modinfo reads the metadata of whichever .ko was selected on disk via modules.dep, not of the object loaded into the kernel. Normally these are the same, which is why the divergence never surfaced. But in the scenario above, with two trees, modinfo names the module that won the search order while a different one may be loaded - the previous one, before a reboot, for instance. For a command we ourselves offer as the answer to "what do I have", that is bad. The version now comes from /sys/module/amneziawg/version, that is from the loaded module; MODULE_VERSION is declared in src/main.c in both lines, so the file exists for the pinned 2.0 as well as for 3.0. modinfo remains the second path for when the module is not loaded. The command in README and ADVANCED (both language versions) was updated in step with the code

5.24.0 - 2026-08-04

v5.24.0 - AmneziaWG 3.0 documented, plus module version in diagnostics.

Added

  • An "AmneziaWG 3.0 for self-hosted servers" section in README and ADVANCED, both languages. Until now the third version was mentioned three times in ADVANCED.en.md and not once in either README, even though on x86 with kernel 6.7 or newer the installer ships exactly that - people were getting 3.0 with no way to learn it from our own material. README now carries a short block: what you get, how to check it, and why the configs you handed out keep working. ADVANCED.en.md carries the reference write-up: which module line each host ends up with (PPA 3.0 on x86 >= 6.7, the pinned 2.0 on older kernels, the prebuilt 2.0 on ARM) and why the 6.7 threshold is deliberate; what 3.0 changes on the wire and what it does not; the seven new config keys with a column for whether the value has to match between server and client; why S1-S4 have to match (the receiver reads the message type at exactly that offset) and why Jc/I1-I5 do not. There is also an explicit list of what the installer does not enable yet: HeaderProtectionKey needs every client to understand it at once, and ContentPaddingAddition waits on an upstream fix
  • A "Rotating the server key" section in ADVANCED.md and ADVANCED.en.md. The key files were documented, the procedure for replacing them was not, even though it is the first thing needed when a server private key leaks. The whole order is spelled out: backup, new pair, substitution into awg0.conf, service restart and regen. Three easy mistakes are called out separately: skipping /root/awg/server_private.key (a --force reinstall then brings the old key back), skipping regen (existing clients keep .png, .vpnuri and .vpnuri.png carrying the old public key), and restoring an old archive from /root/awg/backups/ later, which silently undoes the rotation. The section opens with the fact that previously recorded traffic cannot be decrypted, since the first reaction is usually worse than the facts
  • check and diagnose report the kernel module version. Now that modern kernels get AmneziaWG 3.0 while older ones and ARM stay on 2.0, the first question about any server is which module line it runs, and answering it used to need a separate modinfo. diagnose additionally names AmneziaWG 3.0 when the version starts with 3.; otherwise the raw value is printed rather than a derived protocol number, because upstream tag names have never tracked the protocol version. check --json gains a module.version field (string or null) - adding a key keeps the existing envelope readable by current consumers

Fixed

  • The second S-padding size collision is gone: S3 can no longer equal S2 + 28. The final packet size is the message size plus its own S padding, and two message types ending up the same size gives DPI a stable marker - exactly the one the paddings exist to remove. Message sizes of 148 (init), 92 (response) and 64 (cookie reply) allow three such collisions, and only S2 = S1 + 56 was checked. S3 = S1 + 84 is unreachable with our ranges, but S3 = S2 + 28 is not: it came up in roughly 0.17% of installs, about one in six hundred. S3 is the value regenerated, so the already-checked condition on S2 still holds. Tests were added for all three collisions and for the S1-S4 ranges. ⚠️ This applies to NEW installations: the obfuscation parameters are generated once and a normal reinstall does not recreate them, since that would break every config already handed out. To check an existing server: awg showconf awg0 | grep -E '^S[23] ' - if S3 is exactly 28 more than S2, you are in that 0.17%. Fixing it means regenerating the whole set (--preset on reinstall) followed by regen and redistributing configs, which for most people is not worth the trouble
  • Cascade guide: Persistent=true is dropped from the timer, it was doing nothing there. The recipe suggested it so that a firing missed while the server was off would be caught up. There is nothing to catch up: the awg-routing unit is enabled at boot, so the list of Russian networks refreshes on every startup before any timer is involved, and Persistent only added an unscheduled run right after it. The guide now also covers the case where it would matter (timer enabled but the service not in autostart) and the hard Requires= on both tunnels, which neither the timer nor Persistent can work around. Thanks to @MrSokol (Discussion #120)

Documentation

  • Answer to "how do I update without redoing everyone's connections" (#201). The FAQ in both READMEs, the update section of INSTALL_VPS.md and the limitations list in CASCADE.md/CASCADE.en.md now say it outright: with --force the server keys, the [Peer] blocks and the obfuscation parameters are taken from the existing setup, so configs and QR codes handed out earlier stay valid and every client is preserved, defaults included. The single exception is --preset/--jc/--jmin/--jmax, which regenerate the whole Jc/S/H/I1 set and do force a re-issue via regen. For the cascade it is now stated that its logic lives in awg-routing.sh and survives a reinstall, but the routing script has to be run once more afterwards
  • The update section of INSTALL_VPS.md now says what the reinstall itself looks like: with --force the installer walks the state machine from the top, so the box reboots twice (after the system-update step and after the kernel-module step) and the command has to be run again after each reboot; step 1 is replayed along the way - package cleanup, sysctl, swap, BBR. Verified on a stand
  • Fixed a wrong line in the FAQ of both READMEs: it claimed that re-running the installer recreates the default my_phone and my_laptop clients. That described long-gone behaviour - the creation loop skips clients already present in the config and re-issues nothing

Changed

  • Installer messages about the pinned 2.0 module no longer call the 3.0 build impossible. On 31 July upstream fixed nla_put_uint (issue #204, version v3.0.20260731-04), and the PPA 3.0 module now builds on Debian 12 as well - verified on kernel 6.1.0-51. The 6.7 threshold in the installer is unchanged, but the reasoning behind it is different: on older kernels we deliberately keep the module we have tested rather than route around one that cannot build. Messages in both installers, the code comments, ADVANCED.md/ADVANCED.en.md, INSTALL_VPS.md and the ARM build pin are brought in line, and the v5.23.0 entry is marked as describing release day. Install behaviour is unchanged

5.23.0 - 2026-07-31

v5.23.0 - installs keep working on Debian 12 and other pre-6.7 kernels now that upstream shipped AmneziaWG 3.0 and switched the PPA to it.

Update, 1 August 2026: what follows describes the state on release day. Upstream fixed the pre-6.7 build of the 3.0 module the same evening (v3.0.20260731-04), and the PPA package builds on Debian 12 again - verified on kernel 6.1.0-51. The pinned path stays on purpose: in its first days the 3.0 line managed to break and then fix the build on old kernels specifically, so 2.0 stays there until it is validated separately.

Added

  • On kernels older than 6.7 the installer builds a pinned AmneziaWG 2.0 module from source instead of taking it from the PPA. On 30 July upstream merged AmneziaWG 3.0 into the amneziawg-linux-kernel-module default branch, and the ppa:amnezia/ppa PPA switched to it the same night. The 3.0 module uses the kernel function nla_put_uint, which only appeared in kernel 6.7, so on Debian 12 (kernel 6.1) the PPA module build fails with implicit declaration of function 'nla_put_uint' and a fresh install dies at step 2. The installer now checks the kernel version before installing packages: if it is older than 6.7, the PPA amneziawg-dkms package is not installed at all, and the last 2.0-line module (v1.0.20260725) is cloned from source, verified against its immutable commit hash, and built via DKMS. The amneziawg-tools userland still comes from the PPA: the 3.0 tools detect the module's protocol generation and work correctly with a 2.0 module. A module built this way is picked up by the existing auto-rebuild mechanism on kernel upgrades (AUTOINSTALL=yes), so it needs no separate maintenance. On kernels 6.7 and newer (Ubuntu 24.04/25.10/26.04, Debian 13) the behaviour is unchanged - the module is installed from the PPA. Verified on a clean Debian 12: the 3.0 build failure was reproduced and the full install/uninstall cycle on the pinned 2.0 module passed. The logic is mirrored in both installers (RU and EN)
  • The amneziawg-dkms package is put on hold on the pinned path. amneziawg-tools recommends amneziawg-dkms, and apt installs recommends by default, so without an explicit hold installing the tools would pull the 3.0 module back in and fail on the build again. The hold is set before installing packages and also protects against the 3.0 module being pulled in by a later apt upgrade. Uninstall (--uninstall) clears the hold, deregisters the DKMS module properly, and removes the source from /usr/src

Fixed

  • ARM builds pinned to the last AmneziaWG 2.0-line tag. scripts/arm-module-version.txt pointed at the upstream default-branch HEAD, so before 30 July the prebuilt ARM packages were built from the latest 2.0, and once the branch became 3.0 they would have been built from 3.0 - and debian-bookworm-arm64 (kernel 6.1) already stopped compiling for the same reason as on x86. The pin now points at the tag v1.0.20260725 so the ARM prebuilts keep implementing protocol 2.0; a test locks this value in, so changing it is now a deliberate edit

5.22.0 - 2026-07-31

v5.22.0 - manage now catches an awgsetup_cfg.init edit that never reached clients, plus a new Cloudflare WARP guide for the Russian segment.

Added

  • A warning when awgsetup_cfg.init disagrees with the live config. manage regen and manage check compare the obfuscation parameters in /root/awg/awgsetup_cfg.init against the [Interface] section of the live /etc/amnezia/amneziawg/awg0.conf and name the keys that disagree. Prompted by #196: someone edited I1 in the init file, ran regen, and the client kept its old value. That is by design - after the install the obfuscation parameters come from awg0.conf alone, and the init file is read for them only during a first install, which is what keeps the server and the client configs from drifting apart (#38). The fair part of the report was different: the edit was ignored in silence, even though the file is named like the installation config. The check is gated on modification time and so stays quiet on the supported path: editing awg0.conf and regenerating (the recommended way to tune) also makes the two files disagree, but nothing rewrites the init file after the install, so there it stays older than the live config and the check says nothing. It speaks only when the init file was touched later, which is exactly the case of an edit to init that changed nothing. The installer deliberately never calls it: on step 6 the init file is legitimately newer than an awg0.conf that has not been rewritten yet

Documentation

  • New guide WARP-RU.md / WARP-RU.en.md: selectively routing the Russian segment through Cloudflare WARP via the antifilter BGP feed, for people running a single server. Russian sites see a Cloudflare address instead of a data-center one, all other traffic keeps the existing direct exit, and the network list arrives over BGP with nothing to maintain by hand. The scheme was contributed by @alexfiu4-cyber in #103; it was reproduced end to end on a test server, so the guide also covers what the original recipe did not: the point where the published bird.conf fails to parse, the fail-closed mode with a live interface and a dead WARP peer, how the setup behaves across a reboot and an abrupt BIRD crash, and why the extra MSS clamp is redundant on top of this installer. Cross-links added to README and the ADVANCED FAQ (RU+EN)
  • Corrected a wrong claim about which obfuscation parameters have to match (ADVANCED RU+EN, five places per language). Only S1-S4 and H1-H4 must match on the server and the client: the receiver strips that padding and checks those headers. Jc/Jmin/Jmax and I1-I5 do not have to match, being separate decoy packets that the far side discards (checked against the amneziawg-go sources: concealment packets are read on send and never validated on receive). The worst instance was in the troubleshooting section, where someone whose tunnel would not come up was sent hunting for an I1 mismatch that cannot be the cause; the same place now notes that H1-H4 are ranges in 2.0, so it is the ranges that must be identical
  • Finished the correction about which obfuscation parameters have to match (ADVANCED RU+EN, two places per language). #198 fixed the numbered item in the troubleshooting section but left the intro paragraph of the same block three lines above it, which still said that a mismatch in any of the parameters, Jc and I1-I5 included, breaks the handshake. One collapsible block held two contradictory statements, the wrong one came first, and it sent anyone with a tunnel that would not come up down exactly the false trail #198 was meant to remove. The paragraph now says what the item says: parsing an incoming packet relies on S1-S4 and H1-H4 alone. Separately, the rationale in the --no-cps FAQ is corrected: clients have to be reissued after CPS is turned off not because a client that still has I1 "will not match" a server without it (measured on a test box: the handshake completes with any I1 mismatch, including the parameter being present on one side only), but because the client config still carries I1 and that is what the desktop app chokes on.
  • CASCADE: the Russian network list refresh can now be driven by a systemd timer (RU+EN). Cron stays the primary path and nobody who already set it up has to change anything. The variant was suggested by @MrSokol in #120, and it is documented together with the condition without which it silently does nothing: a timer starts its unit with start, systemd treats start on an already active unit as done, so with RemainAfterExit=yes the timer fires into the void, and invisibly - systemctl list-timers shows a fresh LAST while the journal holds not one line from the moment it fired (checked on systemd 255). Added to the original snippet: Persistent=true, to catch up a run missed while the server was off, and RandomizedDelaySec, so that everyone following the guide does not hit the list source in the same second. It is also spelled out that with RemainAfterExit=no, systemctl status awg-routing shows inactive (dead) between runs and that is normal
  • The code comment about I1-I5 having to match is now in line with the facts (awg_common.sh / awg_common_en.sh, the regen path). #198 removed the claim that the values must match the server side from ADVANCED, but it survived in the code, and a comment is what the next person to touch that block reads. Measured on a test box (kernel module 1.0.20260611, amneziawg-tools v1.0.20260618-2): the handshake completes with any I1 mismatch, including the parameter being present on one side only. The accurate half of the line is kept - regen really is what distributes the values to clients
  • The ADVANCED FAQ answer on where the AWG 2.0 parameters are stored no longer points at the init file (RU+EN). It used to open with /root/awg/awgsetup_cfg.init, which, judging by #196, is where the expectation that editing it would work came from. The answer now opens with awg0.conf as the source of truth, explains what the init file is for, and points to the steps in the neighbouring question

5.21.2 - 2026-07-22

v5.21.2 - validate values from the hand-edited config and from kernel output before they reach the client config, JSON, and arithmetic; plus a temp-directory leak fix on interrupted backup/restore.

Fixed

  • add and regen no longer write a client .conf with a broken port: the server port (from the live awg0.conf, else awgsetup_cfg.init) went into the Endpoint = IP:PORT line unchecked, so a bad edit produced a silently broken profile - carried onto a device and debugged blind, while the vpn:// URI right next door already refused the same value. Both commands now sanitize the port and refuse to write a config when it is unusable, with an explicit error. The sanitizer moved to the shared library so check, diagnose, add and regen use one copy
  • stats --json no longer breaks on non-numeric counters: rx/tx/last_handshake from awg show dump went into arithmetic and JSON unchecked (the same class as the port in v5.21.1). A theoretically broken field produced invalid JSON and opened the same command substitution in arithmetic. A non-numeric value is now collapsed to 0
  • Temp directories no longer leak on interrupted backup/restore: the directory was registered in a subshell, so the signal (INT/TERM) cleanup never saw it, and an interrupted operation left /tmp/tmp.XXXX behind. Registration moved to the parent process

5.21.1 - 2026-07-20

v5.21.1 - validated port handling in check: the output stays parseable whatever the config holds, and a corrupt config no longer looks healthy.

Fixed

  • check --json no longer breaks on a non-numeric AWG_PORT: the value from awgsetup_cfg.init went into the port.number field unchecked, so after a bad hand-edit of the config the output stopped being valid JSON - against the v5.21.0 promise of one parseable document on every exit path. The port is now validated as it is read and normalised to a number: 1-65535 stays a port, anything else becomes 0
  • A corrupt port in the config is now an error, not a silent warning: check with AWG_PORT=abc used to finish with ok=true, so monitoring saw nothing wrong. A non-empty value that is not a port now yields ok=false and exit code 1. A missing setting stays a warning, as before
  • The same unchecked value also reached the [[ -eq ]] arithmetic comparison, where bash performs command substitution, and the UFW rule regex in diagnose, where .* matched anything. Both now get an already-validated number

5.21.0 - 2026-07-20

v5.21.0 - JSON output for management commands and a strict confirmation mode for automation.

Added

  • --json for management commands: add, remove, regen, modify, backup, restore, check/status, restart, repair-module. Each prints exactly one JSON document to stdout on any outcome - including errors, confirmation refusals, signals and early option-parsing failures (an emergency {"command","ok":false,"error","rc"} object via an EXIT trap). list --json and stats --json are unchanged (plain arrays) and are now pinned byte-for-byte by tests. Compatibility is additive: new fields may be added, existing ones are never renamed and never change type. Aliases are canonicalized in the response (status -> check, repair -> repair-module). Requested by bot and integration authors in D#130
  • AWG_STRICT_CONFIRM=1 (opt-in, per-run ENV): a non-interactive run of a destructive command without an explicit --yes/AWG_YES=1 is refused instead of silently proceeding - protection for cron, CI and bots. Default behavior is unchanged
  • check got more informative: it now checks the kernel module (a missing module on userspace installs is a warning, not an error), verifies the UFW rule strictly (specifically ALLOW, not any mention of the port; an inactive UFW no longer masquerades as "rule not found"), and reports the interface MTU and addresses plus a client counter - both in the human output and in JSON

Fixed

  • json_escape hardened: C0 control characters are escaped as \u00XX, invalid UTF-8 is replaced with U+FFFD - closes a latent way to break list --json/stats --json with garbage bytes (same class as the vpn:// ESC bug from v5.20.0)
  • remove with partially missing names now exits with code 1 (previously 0) - symmetry with add/regen for automation (#175)
  • An --apply-mode error now honors a --json placed later in argv; usage errors with --json no longer pollute stdout with help text
  • Stale artifacts of a same-named client (.png/.vpnuri) are cleaned up before add, so the file report is always honest
  • depmod stdout is silenced during module repair (keeps repair-module --json clean)

Documentation

  • ADVANCED RU+EN: new "JSON interface for automation" section (contract, response shapes, a bot recipe); README RU+EN: a scripts-and-bots block, a FAQ entry on managing the server from your own scripts or a Telegram bot, a version-guard note in the update FAQ; INSTALL_VPS: an install-steps diagram, an automation example, --mobile as the primary mobile-network advice
  • The port range is corrected to the actual 1-65535 across all documents (low ports, including 443/udp for mobile DPI, have been open since v5.18.1 - the docs lagged behind); --no-cps and --reset-routes added to the CLI references; the autotest counter updated

Tests

  • Around 60 new bats tests: the strict-confirm matrix, json_escape hardening (ESC/BEL/broken UTF-8), the JSON contract on failure paths, behavioral envelope checks for every command, RU/EN schema parity, the list/stats format freeze. 1135 in the suite total

5.20.1 - 2026-07-18

v5.20.1 - version guard: manage and awg_common now check each other's versions and tell you how to update, plus a precise path traversal check on restore.

Fixed

  • A version mismatch between manage_amneziawg.sh and awg_common.sh is now caught immediately with a clear message. When only one file of the pair got updated, the breakage surfaced as a cryptic _valid_cidr: command not found and a bogus "Invalid CIDR format" on a perfectly valid 0.0.0.0/0, ::/0 (the missing function's exit code 127 was inverted by an if ! check). The library now declares its version (AWG_COMMON_VERSION) and manage compares it against its own by MAJOR.MINOR right after sourcing; on a mismatch - or a library too old to carry the variable - it stops with a direct explanation and ready-to-paste wget commands to update both halves. A patch-level difference is tolerated (#183)
  • The path traversal check on restore no longer rejects legitimate names containing ... The old check dropped any archive file with the .. substring - including innocent names like my..backup.conf or v1..2. Now .. counts as a threat only when it is a complete path component (../x, a/../b, a/..); dots inside a filename pass. The protection itself is not weakened: every real traversal path is rejected exactly as before

5.20.0 - 2026-07-17

v5.20.0 - explicit client isolation (contributed by @ekuraev), the --mobile flag, a configurable server name in the vpn:// link, and early container detection.

Added

  • Explicit client isolation setting (--isolation=on|off, an interactive question on first install). Previously isolation was an implicit side effect of the routing mode: split modes isolated clients only because a client had no route to its neighbors, the 0.0.0.0/0 mode did not isolate at all, and dual-stack clients in split modes remained reachable to each other over the tunnel's IPv6 subnet. Now, with isolation enabled (the default), the server honestly blocks traffic between clients in all modes (FORWARD awg0→awg0 DROP; with the IPv6 tunnel enabled - a matching ip6tables rule); with it disabled, the tunnel subnet is added to clients' AllowedIPs so devices can see each other. The setting is persisted in awgsetup_cfg.init (the CLIENT_ISOLATION key) and changed by reinstalling; existing clients need manage regen --reset-routes after a change (a hint is printed). Isolation rules are removed by PostDown and cleaned up explicitly on an on-off reinstall. When the tunnel subnet changes, the previous route added by the installer is automatically cleaned up from AllowedIPs (the CLIENT_ISOLATION_NET key). Both keys are validated on config load: a non-0|1 CLIENT_ISOLATION and anything but a single canonical CIDR in CLIENT_ISOLATION_NET produce a warning and a safe default (#178)
  • --mobile flag - the mobile setup in one option: --preset=mobile plus port 443/udp at once. Field testing showed the main mobile problem is the port (on MTS the default 39743/udp is dead while 443/udp works), but --preset=mobile only tuned the obfuscation: two users installed with the preset and stayed on the dead port. An explicit --port=N wins over 443; a contradicting --preset is an error; --preset=mobile itself keeps working as before (D#38)
  • Early container detection - an install inside LXC/OpenVZ/Docker/WSL now stops at step 0 with an explanation (a kernel module cannot be loaded in a container - the kernel is shared with the host) and a pointer to the userspace amneziawg-go path in ADVANCED. Previously the install would reach step 3 and die with a raw modprobe: FATAL with no explanation
  • Configurable server name in the vpn:// link (--server-name=NAME, a question on first install). The field used to be hardcoded to AWG Server: every server looked identical in the Amnezia app, and a manual edit was lost on every config regeneration. The name is now set at install time, persisted in awgsetup_cfg.init (the AWG_SERVER_NAME key) and survives reinstalls; old configs without the key keep getting AWG Server. The value is validated (no quotes, backslash or control characters, at most 128 bytes) and escaped when embedded into JSON. On a name change, a hint is printed to reissue vpn:// links via manage regen (D#180)

5.19.2 - 2026-07-15

v5.19.2 - stale UFW rule cleanup on a port change (contributed by @ekuraev), config regeneration after an interrupted install, and an honest repair-module exit code.

Fixed

  • Resuming an interrupted install (a stale setup_state=7/99 after a step 7 failure) with configuration CLI flags (--port, --subnet, --route-*, --endpoint, --ssh-port, obfuscation flags) no longer skips steps 4-6: previously the new values were written to awgsetup_cfg.init while awg0.conf, client configs and UFW rules silently kept the old ones. The installer now rolls the state back to step 4 and regenerates the firewall and the server config. Existing client configs are deliberately left alone by step 6 - on a port change the installer warns that their Endpoint still holds the old port and points at manage regen (#175)
  • Reinstalling with a new --port deletes the old port's UFW rule: previously the old UDP port stayed open forever - the only ufw delete lives in uninstall and reads the already rewritten config, so even --uninstall never removed it. The old port is persisted in awgsetup_cfg.init (the PREV_AWG_PORT key) and survives the step 1-2 reboots; the key is removed only after the rule is successfully deleted, so a failed attempt retries on the next run. SSH limit rules are deliberately left alone (auto-removal on a misdetected port would cut off server access) (#175)
  • manage repair-module no longer reports "service is active" with exit 0 while the service is down: ensure_amneziawg_kernel_module full now returns a distinct code 2 for "module OK but awg-quick@awg0 did not start", and repair-module turns it into an explicit error with diagnostics hints. add/remove keep their previous behavior (warning + config written; apply_config reports the apply failure itself) (#175)
  • manage add exits non-zero when a requested client already exists (no-op): parity with remove/regen, automation can distinguish "created" from "nothing was done" (#175)
  • The NO_CPS key was added to the safe_load_config whitelist in awg_common.sh - aligned with the installer's copy of the function (#175)

5.19.1 - 2026-07-13

v5.19.1 - changing the routing mode after install works again (contributed by @ekuraev), an early check for a too-old kernel, and subnet-guard hardening.

Added

  • Early kernel-version check: on kernels older than 5.15 (for example Ubuntu 20.04 with kernel 5.4) the installer warns clearly and up front that the AmneziaWG 2.0 module usually will not build on such a kernel and suggests reinstalling the VPS on a supported OS, instead of an opaque package-install failure at the module build step (#163).

  • manage regen --reset-routes flag. A regular regen deliberately preserves the client's individual AllowedIPs (modify customizations), so a new global routing mode never reached existing clients. With --reset-routes regenerated clients get the AllowedIPs of the current global mode from awgsetup_cfg.init (DNS and PersistentKeepalive are still preserved). After a mode change on reinstall the installer prints a hint with this command (#170)

Fixed

  • Changing the routing mode after install works again: --route-all / --route-amnezia on reinstall (--force) changed only ALLOWED_IPS_MODE while the ALLOWED_IPS list kept the old value from awgsetup_cfg.init - the flag silently had no effect, and new clients still got the old routes. An explicit CLI mode now clears the list so it is recomputed for the new mode (#170)
  • The live-peer subnet-change guard now picks the IPv4 element from a dual-stack Address line in any order. Previously it took the first comma field, so an Address with IPv6 first could trigger a false install block.

5.19.0 - 2026-07-11

v5.19.0 - full CIDR /16-/30 tunnel subnets (contributed by @ekuraev), a --no-cps switch for the macOS desktop client, and more robust network-interface detection.

Added

  • Full CIDR for the tunnel subnet. --subnet and the interactive prompt now accept /16-/30 masks (previously /24 only). The server address is the first host (network+1); input is in network or network+1 form. The 10.9.9.1/24 default and existing installs are unaffected. The IPv6 tunnel maps clients by host offset (no collisions on masks wider than /24).
  • Subnet-change guard. A reinstall (--force) with a different tunnel subnet aborts when awg0.conf already contains peers: their addresses were issued in the old subnet (old IPv4s can fall outside the new range, IPv6 suffixes can collide). Remove the clients or run --uninstall and reinstall from scratch before changing the subnet. When peers exist but the Address line is unreadable, the install also aborts (fail-closed).
  • IPv6-only host warning. When the server has no IPv4 egress (no default IPv4 route and no global IPv4 on the interface), the installer warns that IPv4 client traffic will not leave the host: the tunnel is IPv4 and NATed via MASQUERADE, so a host with an IPv4 address (dual-stack) or NAT64 is required. Does not block the install (#166)
  • --no-cps flag (disable CPS for the desktop AmneziaVPN on macOS). Drops the I1 parameter from the server config and clients: the desktop AmneziaVPN app on macOS does not support CPS yet and hangs on connect (mobile and CLI clients are unaffected). The rest of the obfuscation (Jc/S1-S4/H1-H4) is kept. On a reinstall --force --no-cps drops I1 without touching the other parameters; re-enable CPS with a reinstall using --preset/--jc/--jmin/--jmax. After disabling on a live server, reissue clients with manage regen (#159)

Fixed

  • Network-interface detection no longer aborts the install on hosts where the 1.1.1.1 probe returns no interface - the provider blocks or null-routes that address, policy-routing, or IPv6-only egress (seen on Ubuntu 26.04 / Timeweb). get_main_nic now tries a chain: ip route get, the default IPv4 route, the first global-IPv4 interface, the default IPv6 route; on total failure it prints the available interfaces and a hint. The interface can be set manually with AWG_MAIN_NIC=<iface> (an invalid value is now rejected with a log warning instead of silently). The interface fallback skips tunnel and virtual interfaces (awg0/wg*/docker0/br-* etc.) - otherwise a reinstall on an IPv6-only host could NAT into the tunnel itself. Previously step 6 aborted with "Failed to detect network interface" (#166)
  • The vpn:// QR is now generated with -8 (single 8-bit byte mode). Large configs with I1-I5/CPS parameters failed with "Input data too large" even though the data itself (the URI is around 2929 bytes) fit the QR capacity: qrencode's optimizer split the base64 into alternating segments and the mode-switch overhead pushed the stream past the v40-L limit (2953 bytes). If a config still does not fit a single QR, the error now suggests importing the vpn:// from the .vpnuri file manually

Documentation

  • New ADVANCED section "Connecting a Linux machine as a client": installing the AmneziaWG module and tools or userspace amneziawg-go, bringing it up with awg-quick, and a warning about losing SSH on a full tunnel to a remote machine (#165)
  • ADVANCED FAQ: why the AmneziaVPN client says "this server does not support split tunneling" and how to enable the feature (full tunnel 0.0.0.0/0, ::/0, no docker needed)

5.18.4 - 2026-07-06

v5.18.4 - cascade reliability and documentation. Cascade split-routing now survives ipdeny being unreachable: a snapshot of the Russian network list (cascade/ru.zone) is bundled in the repository, and the awg-routing.sh script uses it when www.ipdeny.com is blocked by the provider and no local copy exists yet - previously a fresh server in that situation would not bring the cascade up at all. The script also gained a workaround for the route error on VPSes whose default gateway sits outside the server subnet (Hetzner Cloud and similar with a /32 interface). The installer itself is functionally unchanged, so existing installs do not need updating. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.

Documentation

  • Cascade: bundled a snapshot of Russian networks cascade/ru.zone (ipdeny aggregated zone, snapshot 2026-07-06, 8626 networks) as a fallback source. The order in awg-routing.sh is now: ipdeny (the live list) -> the repo snapshot over raw.githubusercontent.com -> the previous local list; it aborts only if all three are unavailable. This brings the cascade up on a fresh server even when ipdeny is blocked (from Discussion #120)
  • Cascade: awg-routing.sh retries adding the route to the exit server with the onlink flag when the default gateway sits outside the server subnet - otherwise on VPSes like Hetzner (real public IP on a /32 interface, private gateway) the route failed with Error: Nexthop has invalid gateway (#158, Discussion #120)
  • Added a cascade/README.md note for the snapshot (source, date, how to refresh) and an Ask DeepWiki badge in both READMEs for quick code navigation (#161)

Infrastructure

  • CI: bumped docker/setup-qemu-action from 4.1 to 4.2 in the ARM package build (#157)

5.18.3 - 2026-07-02

v5.18.3 - convenience and documentation. Confirmation prompts (for example, when removing a client or enabling UFW) now accept not only y but also yes in any case and with stray surrounding whitespace. UFW is enabled more reliably via ufw --force enable, so on some systems the firewall is no longer left disabled. The docs gain the T-Mobile (Moscow) mobile carrier and a Keenetic Speedster router note. Existing installs are unaffected. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.

Fixed

  • Confirmation prompts accept yes, not only y. Previously confirm_action (in manage) and five installer prompts (reboot, unsupported OS, low disk space, UFW enable, backup on removal) matched a bare y only, and answering yes read as "no". They now accept y/Y/yes/YES with any surrounding whitespace (issue #154)
  • UFW is enabled via ufw --force enable. The old approach (feeding y into ufw enable) silently left the firewall off on some systems - only --force helped, and the installer now uses it directly (issue #154)

Documentation

  • Added T-Mobile (Moscow) to the mobile carrier table: a narrow app-style profile (Jc=6, Jmin=10, Jmax=50 + DNS-mimic I1 + full tunnel); the diagnose --carrier=tmobile_us profile checks Jc/Jmin/Jmax and that I1 is binary-shaped (Discussion #45)
  • Added a Keenetic Speedster (firmware 5.0.6) router note: older firmware does not parse H1-H4 as ranges (invalid H1), needs concrete values and calmer junk, with a userspace AWG Manager workaround (Discussion #81)
  • Added a diagnostics tip: the ByeByeVPN scanner helps tell whether a carrier blocks the server IP itself, separating an obfuscation issue from an AS/IP-level block

5.18.2 - 2026-07-01

v5.18.2 - installation robustness. On fresh Ubuntu servers the built-in unattended-upgrades often holds the dpkg lock for several minutes on first boot, which made the installer fail at the system-update step with an apt full-upgrade error. The installer now waits for the lock to be released instead of failing immediately. Existing installations are unaffected. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.

Fixed

  • A fresh-server install no longer aborts when the dpkg lock is busy. At the start of step 1 the installer sets DPkg::Lock::Timeout (via /etc/apt/apt.conf.d), so every apt call waits for the lock that unattended-upgrades / apt-daily hold on first boot. If apt full-upgrade still does not go through, the installer logs the process holding the lock, runs dpkg --configure -a and retries, and on a repeated failure exits with a clear message and recovery steps (issue #150)

5.18.1 - 2026-06-27

v5.18.1 - bug-fix release. Fixes --port on reinstall: install --force --port=N now actually changes the server port (previously the new port was silently ignored). Full-tunnel clients now get 0.0.0.0/0, ::/0 so AmneziaVPN on iOS accepts the "all traffic" mode. The default DNS is now a resolver pair 1.1.1.1, 1.0.0.1. --port now accepts any port 1-65535, including 443 (useful for mobile carriers that drop a non-standard high UDP port). Plus documentation fixes. Behaviour of existing installs and connected clients is unchanged; the improvements apply to new and re-issued (manage regen) configs. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.

Fixed

  • install --force --port=N now changes the server port. Previously the new port was silently ignored: when rendering the server config, the port was re-read from the old awg0.conf, overwriting the value passed via --port. The port for the new config is now taken from the saved install parameters (the user's intent), which correctly survives the reboots during --force. Client regen is unaffected - it still reads the port from the live awg0.conf
  • Full-tunnel clients (AllowedIPs = 0.0.0.0/0) now get 0.0.0.0/0, ::/0. With a bare 0.0.0.0/0, AmneziaVPN on iOS treated the config as incomplete split tunneling and refused to bring the tunnel up. ::/0 sends IPv6 into the tunnel (and drops it if the server has no native IPv6), so nothing leaks past the VPN. Split tunneling (a custom route list) is not affected

Changed

  • The default DNS in client configs is now a pair 1.1.1.1, 1.0.0.1 instead of a single resolver (a fallback DNS in case the first one is unreachable)
  • --port now accepts ports 1-65535 (previously only 1024-65535). Low ports like 443/80/53 help with DPI evasion on mobile carriers: MTS, for example, drops a non-standard high UDP port but passes 443/udp. The VPN service runs as root, so privileged ports bind fine

Documentation

  • ADVANCED: fixed the S3/S4 range in the minimal-config example (S3: 0-64, S4: 0-32 instead of the wrong 0-127); removed the stale link of the active-probing section to the already-closed issue #71; added notes on the mobile port and on direct IPv6 traffic bypassing the tunnel
  • README: added Ubuntu 26.04 to the subtitle; clarified the --force gate in the re-run FAQ

Tests

  • Added tests/test_v5181_bugfix.bats - the port from the saved parameters wins over the old awg0.conf on render; a full-tunnel client gets ::/0, a split one does not; the default DNS pair; RU/EN parity of all three fixes

5.18.0 - 2026-06-26

v5.18.0 - the special-junk params I2-I5 now reach clients. Previously only I1 made it into the client config; I2-I5 were hard-coded empty in the vpn:// URI and never rendered into the .conf, so even if the admin set them in awg0.conf they could not reach the client. Now all five CPS params are carried into the client .conf, the QR code, and the vpn:// link. Workflow: set I2-I5 in the [Interface] of awg0.conf, restart the service, and distribute to clients with manage regen. No new install flags; when I2-I5 are unset, behavior is identical to before. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM (issue #71).

Added

  • Pass-through of I1-I5 (not just I1) from the server awg0.conf into client configs on generation and regen: the live-config parser reads I2-I5, the client .conf render appends the set values, and the previously hard-coded empty I2-I5 in the vpn:// URI are replaced with the real ones. Values are carried verbatim; the admin picks the tag format (<r N>, <b 0xHEX>, <c>, <t>) - see the VoidWaifu list. I2-I5 are optional and independent: unset ones are simply not emitted
  • The params inherit the same preservation semantics as the rest of the obfuscation: --force without --preset/--jc/--jmin/--jmax reads I2-I5 from the live awg0.conf and keeps them; --preset/--jc regenerate the whole obfuscation set, which drops manually set I2-I5 (same as H1-H4/S1-S4/I1)

Tests

  • Added tests/test_v5180_i2i5_passthrough.bats - parsing I2-I5 from awg0.conf, anti-stale protection across calls, client .conf render (present when set, absent when empty), and a real vpn:// decode (values present in both the structured fields and the embedded raw config), plus RU/EN parity of every touched site

5.17.0 - 2026-06-24

v5.17.0 - the server firewall now clamps the TCP MSS to the tunnel MTU so large pages and downloads no longer hang on paths that filter ICMP (mobile carriers, double-NAT, two-server cascade). The install behavior is otherwise unchanged; update an existing server by re-running the installer. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.

Added

  • MSS/PMTU clamp in the server config PostUp/PostDown: a TCPMSS --set-mss rule in the mangle table's FORWARD chain caps the advertised TCP MSS to a tunnel-safe value in both directions (-o %i and -i %i). This removes the PMTU blackhole - when ICMP "Fragmentation needed" is filtered along the path, oversized TCP segments with DF set are silently dropped at the 1280 tunnel and the page or download stalls. A common complaint on mobile carriers, behind double-NAT, and in cascade setups
  • The MSS value is derived from AWG_MTU when the config is generated (IPv4: MTU-40 = 1240, IPv6: MTU-60 = 1220); a manual MTU change is picked up by re-running the installer with --force. IPv6 rules are only added when the IPv6 tunnel is enabled. It complements the existing conservative MTU = 1280 rather than replacing it

Tests

  • Added tests/test_v5170_mss_clamp.bats - asserts the rules in both directions, the mirrored -D in PostDown, IPv6 gating, and MSS auto-derivation from AWG_MTU

5.16.1 - 2026-06-16

v5.16.1 - hotfix: iOS tunnel drop on the default routing mode (Issue #42). The default install behavior is otherwise unchanged. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.

Fixed

  • iOS: on the default routing mode (mode 2, "Amnezia List + DNS") the tunnel came up but traffic stopped after ~10 seconds (easy to mistake for DPI). The AllowedIPs list 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. 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 split-tunnel is preserved. Traced and fixed by @LiaNdrY (Issue #42)
  • The --expires error message now lists 30d among the valid formats (matching --help)

Documentation

  • README and ADVANCED (RU + EN): added an FAQ entry about the iOS drop with instructions for already-installed servers (before v5.16.1)
  • Cascade (CASCADE.md / CASCADE.en.md): the client config example now uses this installer's default Endpoint port 39743 (was 51820 from upstream WireGuard); the troubleshooting section no longer references a nonexistent after-awg1.conf drop-in - the autostart check now points at the awg-routing.service unit; removed a duplicated paragraph in step 1
  • Full documentation audit: fixed minor inaccuracies (a duplicated pointer line in ADVANCED.en.md, an install-link anchor mismatch between README RU and EN, the import-files wording in INSTALL_VPS.md)

CI

  • The ShellCheck workflow ("Lint and syntax check") is no longer path-filtered: the required status check now runs on docs-only PRs too, so it no longer blocks them (previously needed an admin override)

Tests

  • Added tests/test_v5161_ios_allowedips.bats - a structural guard against the mode-2 list reverting to 0.0.0.0/5

5.16.0 - 2026-06-12

v5.16.0 - security and reliability hardening from a full code audit. This release closes the findings of a full review of all six scripts of the AmneziaWG 2.0 VPN installer for Ubuntu and Debian: it eliminates the peer-loss window on --force reinstall, removes secrets from process argv, pins the PPA GPG key by full fingerprint, makes the AWG parameter validator check H1-H4 non-overlap, and stops expired orphan clients from looping the cron job forever. The default install behavior is unchanged. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.

Security

  • The temp-file registry moved from world-writable /tmp into $AWG_DIR (0700): a predictable name in /tmp would let a local user plant a list of arbitrary paths that cleanup would then delete as root; registry reads are additionally guarded against symlink swaps
  • Private keys (client and server) are now born with 0600 permissions via umask 077 - the brief world-readable window between write and chmod is gone; the keys/ directory is created with 0700
  • The client private key and PSK are passed to the perl vpn:// URI generator via environment variables instead of argv (a process command line is visible to all users in /proc/<pid>/cmdline)
  • The Amnezia PPA GPG key is requested by its full 40-character fingerprint instead of the short 32-bit ID (short IDs have known evil32 collisions) and is verified against the pin after download
  • uninstall no longer purges the fail2ban package if it was installed before our installer (the .fail2ban_installed_by_installer marker, symmetric to the UFW marker); a user-owned fail2ban is restarted without our jail
  • uninstall removes only the exact lines written by legacy versions of this installer from /etc/sysctl.conf instead of any line containing disable_ipv6 (it could previously wipe user-added settings)
  • Library functions generate_client / add_peer_to_server / remove_peer_from_server / regenerate_client now validate the client name themselves (defense-in-depth for cron and third-party scripts sourcing the library)

Fixed

  • Step 6: existing [Peer] blocks are now carried over inside a single atomic write of the server config (render_server_config appends peers before the mv). Previously a crash between render and the separate append left the config without peers, and a re-run of the step backed up the already peer-less file - all clients were lost on --force reinstall
  • remove_peer_from_server checks the awk exit code and the presence of [Interface] in the result before replacing the server config - an awk failure on a full disk can no longer atomically replace a working config with a broken one
  • An expired client whose peer was already removed from the config manually or by a restore (an orphan expiry label) is now cleaned up properly: previously cron retried the failing removal every 5 minutes forever and the client artifacts were never reclaimed
  • regen recovers the PresharedKey from the server config when the client .conf is lost - previously the recreated config came out without a PSK while the server peer still had one, silently breaking the handshake
  • The public IP detection cache actually works now (file-based, survives subshells): previously the assignment inside $(...) was lost, so regen over all clients performed a full curl round (up to 6 services, 5 s each) per client
  • validate_awg_config checks pairwise non-overlap of the H1-H4 ranges (the key AWG 2.0 invariant) and parses parameters the same way the loader does: arbitrary whitespace around =, last-wins on duplicates - a hand-edited Jc=4 no longer produces a false "parameter not found"
  • H1-H4 generation guarantees a strict gap between ranges (boundary touching is excluded) and a lower bound >= 5 (values 1-4 are reserved by the WireGuard protocol)
  • --apply-mode is validated at argument parse time: a typo like --apply-mode=restrat used to silently behave as syncconf
  • --diagnostic checks for root before doing anything (previously it failed on every log write under a regular user and exited with a false success)
  • Debian 12: the PPA suite-mismatch repair now also covers the traditional .list format (previously only DEB822 .sources was handled)
  • Step 2: the early apt update is now tolerant to Amnezia PPA errors - a leftover PPA file with a broken suite (404 Release) no longer kills the install BEFORE the repair logic runs (live repro on Debian 12); base repository errors remain fatal
  • check: when the port cannot be determined, the UFW rule check is skipped (previously it printed a meaningless warning about 0/udp)
  • list: a corrupted expiry file no longer causes a bash arithmetic error in the table
  • generate_vpn_uri verifies the port is numeric before generating the URI (an empty port produced syntactically broken JSON that Amnezia Client silently refused to import)
  • The public-IP detection error message in manage add no longer suggests the --endpoint flag that manage does not have

Improved

  • Installer steps 1-2 no longer re-run apt-get update without a sources change (saves 10-60 seconds per run on slow mirrors, up to two redundant runs per install)
  • backup additionally takes the config lock: a parallel add/remove can no longer produce a desynchronized file set inside the backup
  • Interactive port and subnet prompts during install re-ask on a typo instead of aborting the whole installation
  • Reinstalling with --preset/--jc/--jmin/--jmax prints an explicit warning that the ENTIRE obfuscation parameter set is regenerated and existing client configs will need regen (the idempotency-guard message was clarified too)
  • setup_fail2ban warns when UFW is inactive (bans with banaction=ufw are effectively inert in that case)
  • The apt-get update error classification ignores known informational lines W: Target ... is configured multiple times and W: ... legacy trusted.gpg - they no longer turn a tolerable failure into a false fatal
  • diagnose makes one awg show call instead of four; stats no longer spawns date per peer; list --json does not read expiry files (the field is not part of the JSON output)
  • The expiry cron job runs the temp-file cleanup (fixes a registry-file leak on every firing)
  • The random number generator fallback (when /dev/urandom is unavailable) covers the full 31-bit range instead of 30 bits
  • manage help: regen documents accepting multiple names, repair-module lists its repair alias; the client-creation message mentions .png only when the QR was actually created
  • Dead code removed: the unreachable help) dispatcher branch in manage, the unreachable return after request_reboot in the installer, two redundant AWG_APPLY_MODE re-exports, stale shellcheck directives
  • CI/release scripts: update-sha-pins.sh preserves file permissions on rewrite; build-arm-deb.sh surfaces xz stderr on failure; build-release-notes.sh validates the version format before interpolating it into awk

5.15.6 - 2026-06-08

v5.15.6 - input validation, atomicity and a JSON status field. This release continues the code-audit hardening cycle: it sharpens input validation in manage, makes client-artifact writes atomic, refines interrupt handling, and adds a stable machine-readable status_code to list/stats --json. The default install is unchanged. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.

Added

  • list --json and stats --json now emit an extra status_code field with a stable, language-independent value (active | recent | inactive | no_handshake | key_error | no_data). The existing localized status field is kept - the change is additive and backward-compatible, and convenient for jq and automation (the contract is documented in ADVANCED.en.md)

Improved

  • manage modify <client> validates each DNS entry as a real IPv4/IPv6 address (via the shared _valid_ipv4/_valid_ipv6 helpers) rather than a character set, so values like abc or 999.999.999.999 are no longer accepted. The same octet-range check (0-255) now applies to the auto-detected server public IP
  • --expires duration is validated once before the client is created: an invalid value aborts the command up front without changing anything. A failure to write the expiry after creation is now reflected in a non-zero result and the log
  • --psk is treated as an explicit request for a key: if the PSK cannot be generated, the command fails instead of silently degrading to a PSK-less client, and no artifacts are created
  • .vpnuri and the temporary QR PNGs are written through the shared safe-temp mechanism with an atomic mv, so an interrupted write cannot leave an empty or truncated file over a working one. Client removal (manual and automatic on expiry) uses one shared helper that covers every client artifact
  • The expired-client auto-removal cron is compared by content and refreshed when the working directory changes (for example after a restore), not only when the file is absent
  • Restore checks for the server config before stopping the service (an interruption no longer touches a working system), and an empty clients directory is treated as a valid case
  • Ctrl+C or a termination signal now aborts the script with the correct exit code (130/143) instead of continuing past the interrupted operation; an interrupted restore still rolls back

Documentation

  • Installer built-in help and the option tables in ADVANCED.en.md: --endpoint accepts an FQDN, IPv4 or [IPv6]; clarified that --no-tweaks still applies the minimal forwarding sysctl
  • The command tables in README.en.md now include diagnose and repair-module
  • ARM support for Ubuntu 26.04 (built via DKMS) is documented; the OS / architecture / prebuilt-package matrix is now checked automatically for consistency

Tests

  • Expanded automated coverage (bats): input validators, file-operation atomicity, signal handling, and the status_code contract

5.15.5 - 2026-06-07

v5.15.5 - fail2ban bugfix on Ubuntu 24.04 (thanks @stereomonk).

Fixed

  • fail2ban failed to start on minimal Ubuntu 24.04 without rsyslog ("Have not found any log file for sshd jail"): the sshd jail now uses backend = systemd on Ubuntu as well, matching Debian since v5.7.12 (PR #106, thanks @stereomonk)
  • fail2ban status after restart is now checked honestly: systemctl restart returns 0 even when the service dies right after start, so the installer used to report success on a crashed service. The state is now verified via systemctl is-active (PR #106)

5.15.4 - 2026-06-06

v5.15.4 - a companion release: a new documentation section on a host being unreachable from Russia (autonomous-system blocking) and the I1/CPS workaround, plus housekeeping. The default install behavior and the support matrix are unchanged.

Documentation

  • ADVANCED: a new section on host blocking by autonomous system (AS). It covers the symptom (the handshake completes, then traffic stalls), the AS-level cut on the operator's network, and the workaround via I1/CPS QUIC mimicry with an allowlisted SNI. The section includes field observations across several operators and links to the relevant discussion (Issue #71).

Internal

  • Two bats test names were converted to ASCII. The full suite of 790 checks now also runs under bats on Windows (two tests with non-ASCII names were previously skipped there); the Linux CI always ran the full suite.

5.15.3 - 2026-06-04

v5.15.3 - a hardening release following four rounds of external code and documentation audits. No new features: it tightens the installer's input validators, fixes a number of correctness issues in manage, and hardens file-operation atomicity and the release process. A default install is unchanged. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.

Fixed

  • Early installer validators now match the canonical ones. The step-0 checks for port, subnet, endpoint and CIDR lists (before the shared library is downloaded) now reject ports with leading zeros (0080 is no longer accepted via octal interpretation), numbers longer than 5 digits (64-bit overflow guard), non-canonical subnet octets, edge-case bracketed IPv6 endpoints ([:::], double ::), and newlines or empty items in route lists.
  • --route-custom is validated on the first run regardless of where the value came from: an invalid route list stops the install before the configuration is written.
  • An unknown command-line argument exits the installer with code 1 (it used to print help and exit 0); an explicit --help still returns 0.
  • manage restore is robust against incomplete backups: restoring from an empty key set or a truncated backup no longer removes live clients.
  • Re-running the installer keeps all existing peers and the default clients when regenerating the server config.
  • manage modify validates IP and CIDR values by range, not just by shape.
  • File-operation atomicity: config temp files are created on the destination filesystem (so mv is actually atomic), the client QR PNG is written via temp + mv, and temp cleanup, comma validation in lists, and vpn:// URI QR generation were tightened.

Internal

  • Reproducible ARM builds: the upstream module is pinned to an explicit ref, and a build manifest is published alongside the packages.
  • release.yml builds bilingual release notes (RU + EN from both changelogs) and a descriptive release title automatically.
  • Preflight and CI hardened: correct bats verdict, full OS/arch test matrix, the docs checker runs on path triggers, new guards against stale IPv6 wording and version placeholders.
  • The documentation consistency check is about 8x faster (single-pass Unicode heading slugger) and now covers the ROADMAP.
  • The test suite grew to 790 checks (new scenarios for the validators, restore, atomicity, the slugger and release notes).

Documentation

  • Corrected descriptions of IPv6 routing, native IPv6 detection, --json scope and the supported OS count; the CLI reference in ADVANCED is synced with the actual flags.
  • README and INSTALL_VPS point at --ssh-port for changing the SSH port; mobile carrier advice and the script integrity note were refreshed; the ROADMAP was brought up to date.
  • The release process is documented without contradictions (release-notes source, neutral review gate, checklist sync); Code of Conduct reports go to a private channel; blank issues are disabled - questions go to Discussions.

Verification

  • Live runs on clean VPSes: Ubuntu 24.04 (x86_64, full cycle: negative validator tests, install with 2 reboot-resumes, the whole manage CRUD), Ubuntu 26.04 (ARM64: PPA questing -> noble remap, DKMS module build), Debian 13 (custom routes via --route-custom, kernel headers fallback, DKMS against a kernel upgraded during the install). Real cross-continent AWG 2.0 handshakes and DNS through the tunnel confirmed.

5.15.2 - 2026-06-02

v5.15.2 - a small maintenance release. It points the install and update commands in the documentation at the current tag. No installer or management code changed, behavior is unchanged. Support matrix is unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.

Fixed

  • Install and update commands in the documentation point at the current release. The wget / curl examples in README, INSTALL_VPS and ADVANCED were pinned to the previous tag, so copying a command downloaded the previous version. They now point at this release, so following the instructions installs it.

Internal

  • The documentation consistency check now verifies that pinned raw.githubusercontent.com links match the current version, so install commands do not fall behind the release in the future.

5.15.1 - 2026-06-02

v5.15.1 - a maintenance release after a round of external code and documentation audits. No new features: it hardens the v5.15.0 dual-stack IPv6 work, tightens several management commands, and fixes a number of correctness and robustness issues. A default install is unchanged. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.

Changed

  • Split tunnel + IPv6 now mirrors the IPv4 intent (behavior change). When a client uses a custom ALLOWED_IPS (split tunnel), the dual-stack config keeps that IPv4 split list and adds only the tunnel ULA for IPv6 - it no longer forces a full ::/0 IPv6 route. A full tunnel (ALLOWED_IPS=0.0.0.0/0) still gets ::/0 with native IPv6, or the tunnel ULA without it. This affects only clients created with both a split tunnel and --allow-ipv6-tunnel. If you relied on the old behavior of IPv6 going full-tunnel while IPv4 stayed split, recreate the client to pick up the new routing.

Fixed

  • vpn:// URI now carries the server's real MTU, keepalive, and DNS instead of fixed values, so a config imported from the URI matches the .conf even after the server settings or a manage modify changed them.
  • Stricter native-IPv6 detection. The server is treated as natively IPv6-capable only when it has a global (non-ULA) IPv6 address and a default IPv6 route, so a client gets a full ::/0 route only when it will actually work.
  • The IPv6 host stack is re-enabled before detection when the tunnel needs it, so a server that had IPv6 disabled via sysctl still gets a working dual-stack tunnel.
  • install fails fast if apt update errors out, instead of continuing a multi-step install against a stale package cache.
  • manage modify validates the Endpoint and AllowedIPs it is given (host:port, CIDR list) before writing them.
  • manage add is safe against name reuse: it refuses a name that already exists and cleans up partial key or config artifacts if creation fails partway.
  • manage restore prunes stale clients and keys that are absent from the backup, so a restore yields exactly the backed-up state.
  • manage help exits 0 for an explicit help request and 1 only for a real usage error.
  • manage --json keeps stdout pure JSON by routing info and debug logging to stderr.
  • Log messages no longer double percent signs ("95%" stays "95%").
  • manage restore recreates the expiry directory before restoring expiry timestamps.

Documentation

  • --subnet help states the /24-only requirement; install --help lists Ubuntu 26.04.
  • manage --help and the README document list --json (including the client_ipv6 field).
  • Restored the changelog compare-links for 5.11.0-5.15.0, fixed several broken in-page anchors, refreshed SECURITY.md and CONTRIBUTING.md, and added docs/RELEASE_PROCESS.md.

Internal

  • Tagging a release now runs a full preflight gate (syntax, shellcheck, tests, punctuation, version and SHA-pin consistency, documentation checks) before the GitHub Release is published, plus a lightweight documentation-consistency workflow. The signing design doc was aligned with the asset-upload draft.

5.15.0 - 2026-06-01

v5.15.0 - optional dual-stack IPv6 inside the tunnel. Requested by users in #24: the new --allow-ipv6-tunnel flag hands clients an IPv6 address from the private ULA subnet fddd:2c4:2c4:2c4::/64 alongside the usual IPv4. Off by default - without the flag the install is identical to v5.14.x. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.

Highlights

  • 🆕 --allow-ipv6-tunnel flag in install_amneziawg.sh (opt-in, off by default). Enables dual-stack IPv6 inside the VPN tunnel: the server and clients get addresses from the ULA subnet fddd:2c4:2c4:2c4::/64 next to IPv4. The subnet can be overridden via IPV6_SUBNET= in awgsetup_cfg.init. This is separate from the existing --allow-ipv6 (host-level IPv6 via sysctl); the behavior of --allow-ipv6 / --disallow-ipv6 is unchanged.
  • 🌐 Dual-stack server and client configuration. With the flag on, the server awg0.conf gets IPv6 in Address and AllowedIPs, an ip6tables MASQUERADE rule on the public NIC is set up, and the client .conf plus vpn:// URI become dual-stack. The installer detects native IPv6 on the server (ip -6 addr show scope global): with native IPv6 the client gets a full ::/0 route, without native IPv6 only the tunnel subnet, so IPv6 traffic does not vanish into a black hole (a warning is logged in that case).
  • 🔧 manage list and manage regen are dual-stack aware. The client list shows a mixed state (new dual-stack clients next to legacy IPv4-only ones), and config regeneration correctly preserves the IPv6 address.

Migration

  • Existing clients are unaffected. After upgrading and enabling IPv6, already-issued IPv4-only configs keep working as before - the server does nothing to them automatically.
  • To add IPv6 to a client that already existed, after enabling --allow-ipv6-tunnel (re-run the installer with the flag or set ALLOW_IPV6_TUNNEL=1 in awgsetup_cfg.init) recreate that client: manage remove <name>, then manage add <name>. Only recreation allocates an IPv6 for the client on the server and issues a dual-stack config. A plain regen keeps the client IPv4-only, because its [Peer] entry on the server has no IPv6 yet. The new .conf must be re-imported on the device.
  • Details and troubleshooting are in ADVANCED.en.md.

Tests

  • New file tests/test_v515_sha_pins_lockstep.bats (4 tests): checks the real sha256sum of the four helper scripts against the COMMON_SCRIPT_SHA256 / MANAGE_SCRIPT_SHA256 pins in both installers, so a partial bump cannot ship an installer with drifted pins.
  • New helper scripts/update-sha-pins.sh (with a --verify flag) to recompute and verify the SHA pins, and scripts/preflight-check.sh for a single pre-tag check run (syntax, shellcheck, bats, punctuation, version consistency, SHA pins).

Verification

  • The automated test suite (bats) was extended with dual-stack coverage: server and client config rendering, IPv6 allocation, dual-stack vpn:// URI, manage list/regen, and the no-native-IPv6 path. The no-flag regression is identical to v5.14.x.
  • Verified on clean servers: the happy path on ARM64 / Ubuntu 26.04 with native IPv6 (the client gets a full ::/0 route, ip6tables MASQUERADE, IPv6 internet egress), and the warning path on x86_64 / Debian 13 without native IPv6 (the client gets the tunnel subnet only). A cross-host tunnel was confirmed live: both IPv4 and IPv6 pass through it (loss-free ping6 to the server's in-tunnel address).

5.14.5 - 2026-05-25

v5.14.5 - the installer now detects the real SSH port and opens exactly that one in UFW. Previously, with SSH on a non-standard port, the firewall opened only port 22, and access to the server could be lost once it was enabled on the final step. No architectural changes, support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.

Highlights

  • 🔧 Automatic SSH port detection for the UFW rule in install_amneziawg.sh. As observed by @userosos on Debian 13 (#91): with SSH on a non-standard port, the installer opened only the default port 22 in UFW, and access was lost once the firewall was enabled (ufw enable). Added real-port detection: first the --ssh-port flag, then the effective sshd -T config (the Port directive and ListenAddress host:port, honouring drop-in files in sshd_config.d), then the real sshd listening sockets via ss, then sshd_config parsing, and port 22 as the default. UFW now opens every detected SSH port, the pre-enable warning names them explicitly, and the --yes mode also detects the port instead of assuming 22.
  • 🆕 --ssh-port=PORT flag to set the SSH port manually (comma-separated list allowed) for non-standard setups where auto-detection is not desired.

Tests

  • New file tests/test_ssh_port_detect.bats (18 tests): port detection from --ssh-port, from sshd -T (the Port directive and ListenAddress, IPv4 and bracketed IPv6), union with ss sockets, port normalisation and deduplication, UFW rule application in both branches (fresh setup and update), and parity between the RU and EN branches.

Verification

  • Verified on clean servers with SSH on a non-standard port and a --yes install: Ubuntu 24.04 (x86_64) and Ubuntu 26.04 (ARM64). UFW opens the right port and server access is preserved.

5.14.4 - 2026-05-24

v5.14.4 - small installer refinement: declining UFW activation during an interactive install (answering N to "Enable UFW?") now correctly continues the installation. A minor user-choice handling improvement, no architectural changes. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.

Highlights

  • 🔧 Declining UFW now continues the install correctly in install_amneziawg.sh. As observed by @jay0x on Ubuntu 24.04 (#89): answering N to the interactive "Enable UFW?" prompt stopped the installer instead of continuing. Adjusted the decline handling: the UFW rules stay configured (deny incoming, SSH rate limit, VPN port allow, routing) but the firewall is not activated, the install proceeds, and a hint is logged that the server is running without a firewall, along with the command to enable it later (sudo ufw enable). With the --yes flag the behaviour is unchanged - UFW is enabled automatically. Affects only the interactive path where the user declines the firewall themselves.

Tests

  • New file tests/test_v5144_ufw_optional.bats (6 tests): declining UFW continues the install and does not call ufw enable; accepting enables UFW; --yes mode enables automatically without reading input; structural parity between the RU and EN branches.

5.14.3 - 2026-05-21

v5.14.3 - patch release with one fix: cleanup_system() no longer calls apt-get autoremove after purging cloud-init, which on clean Ubuntu 26.04 server in VirtualBox (subiquity, no cloud-init network management) could remove netplan-generator as a transitive dependency and leave the server unable to obtain an IP via DHCP after reboot. No architectural changes. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.

Highlights

  • 🛡️ Network stack protection in cleanup_system in install_amneziawg.sh. Reported in #84 by @jay0x on a clean Ubuntu 26.04 server in VirtualBox: after the installer the server did not obtain an IP via DHCP. Root cause: an aggressive apt-get autoremove after apt-get purge cloud-init cascaded into removing netplan-generator as a transitive cloud-init dependency. Without netplan-generator the /etc/netplan/00-installer-config.yaml file (subiquity creates it on ISO installs) was no longer translated into /run/systemd/network/*.network, and systemd-networkd started with empty configuration. Changes in cleanup_system(): the apt-get autoremove call is dropped; before any apt-get purge an apt-mark hold is applied to critical network stack packages (netplan.io, netplan-generator, systemd-resolved, netcfg, ifupdown) - we first snapshot the user's existing holds via apt-mark showhold and only add our own hold on packages the user has not already locked (and on unhold we release strictly the ones we placed); a default-route snapshot is taken before and after cleanup - if the route is lost, the installer attempts recovery (netplan.io is reinstalled unconditionally, netplan-generator only when it is available in the archive via apt-cache show, so Debian 12 - which does not ship that package - does not abort the apt transaction), restart systemd-networkd, netplan apply, then a wait loop polling the route every 1-5 seconds for up to ~26 seconds; then on failure a last-ditch interface bring-up via ip link set up, first networkctl renew with a route re-check, then dhclient -4 if needed, and only then the installer stops with a hint to restore the network from the console (sudo dhclient -4 <iface>) and retry the installer with --no-tweaks. Orphan packages now stay in the system after purge (~50-200 MB) - acceptable trade-off for stability; users can manually run apt-get autoremove --no-install-recommends after installation.
  • 🪟 Ubuntu 26.04 whitelisted in check_os_version. Previously 26.04 fell into the warning branch with an interactive prompt (passed automatically with --yes). Now it is recognised as a supported OS alongside 24.04 / 25.10. The release is tested on 26.04 server in VirtualBox after the Issue #84 fix.

Tests

+14 new bats (532 planned in bats tests/, up from 518 on v5.14.2):

  • test_v5143_cleanup_no_autoremove.bats (+14) - functional checks via mocks of dpkg-query, apt-get, apt-mark, apt-cache, ip, systemctl, netplan, networkctl, dhclient, sleep: apt-get autoremove is never invoked; apt-mark hold fires for the netplan and systemd-resolved packages before any purge (without systemd-networkd - that is not a standalone package on Ubuntu 24+, the binary lives inside systemd); pre-existing user apt-mark holds are left alone (our hold/unhold cycle skips them); recovery path when default route is lost (install netplan.io unconditionally, netplan-generator only behind an apt-cache show gate, netplan apply + wait loop); last-ditch path after primary recovery fails (ip link set up + networkctl renew with a route re-check, then dhclient -4 as fallback); die path on total failure with the --no-tweaks hint; existing cloud-init guard (three marker checks) preserved. Structural checks: RU/EN line parity of cleanup_system, presence of apt-mark hold / unhold / die in both files, absence of a real apt-get autoremove line (rationale comments are excluded).

Compatibility

  • Backward compatible with v5.14.x. Behaviour on cloud images that carry cloud-init markers (Hetzner, Oracle Cloud) is unchanged. ISO installs of Ubuntu 26.04 in VirtualBox now correctly handle the absence of cloud-init netplan markers.
  • The --no-tweaks workaround still works but is no longer required for the @jay0x scenario.

Upgrade

From v5.14.2 to v5.14.3:

wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.14.3/install_amneziawg_en.sh
sudo bash ./install_amneziawg_en.sh --force --yes

Step 5 of the installer pulls the latest manage_amneziawg.sh and awg_common.sh with SHA256 verification.

Thanks to @jay0x for the detailed repro with dpkg, journalctl, and ls /etc/netplan/ output - the root cause would have taken longer to pin without it.

Full list of changes since v5.14.2


5.14.2 - 2026-05-21

v5.14.2 - patch release with two small fixes: the .vpnuri.png QR is now scannable with a phone camera from a computer screen (long URIs with PSK used to trigger error 900 in AmneziaVPN on iOS), and the ARM .deb build script no longer silently picks the "first" /lib/modules/*/build directory on hosts with multiple installed kernels. No architectural changes. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM.

Highlights

  • 📱 .vpnuri.png QR is now scannable off a screen. awg_common.sh:generate_qr_vpnuri now invokes qrencode with an explicit -s 6 (the previous default was 3). This is the real fix: at the default scale the PNG modules were too small for the iPhone camera to resolve when scanning off a computer screen, which produced error 900 ImportInvalidConfigError in AmneziaVPN on iOS for @haritos90 in issue #72 (Debian 12 + AmneziaVPN iOS 4.8.15.4). Increasing the module scale does not change QR data capacity - it makes each module physically larger so the camera can distinguish black/white blocks reliably. The current qrencode defaults are also pinned explicitly: -l L (lowest error-correction level) and -m 4 (standard quiet zone) - so future default changes in libqrencode cannot regress this fix. Pasting the text of .vpnuri into the app already worked; the fix restores the primary camera-scan path.
  • 🛠️ scripts/build-arm-deb.sh: explicit KERNEL_VERSION + fail on ambiguity. The ARM .deb builder used to pick the first matching /lib/modules/*/build directory through a simple loop; on developer hosts with several installed kernels this could build against an unintended target. An external code review on 8 May raised the risk. Version resolution has been extracted into a _resolve_kernel_version helper with three paths: when KERNEL_VERSION is set, the helper validates /lib/modules/$KERNEL_VERSION/build and uses it; otherwise it counts candidates - zero is an error (unchanged), exactly one is the unambiguous choice (unchanged), two or more produce an explicit failure that lists every found version and asks the caller to set KERNEL_VERSION. The AmneziaWG CI matrix is unaffected because each QEMU container installs exactly one headers package; the defensive behaviour is needed when the script runs on user hosts.

Tests

+18 new bats (528 total, up from 510 on v5.14.1):

  • test_v5142_qr_high_density.bats (+7) - asserts that qrencode receives -l L, -s 6, -m 4, that -t png is still passed, regression check that the PNG file is still produced from the .vpnuri payload, and byte-identical parity of the invocation line between RU and EN.
  • test_v5142_build_arm_deb.bats (+11) - functional tests for _resolve_kernel_version: exactly one candidate, zero candidates, multiple candidates with an explicit list on stderr, directories without a build/ subdir ignored; KERNEL_VERSION env path (valid, used to disambiguate, missing target dir, empty string falls back to auto-detect); structural checks that the function exists, the source-guard allows safe source from tests, and the old inline detection loop is gone from the main body.

Compatibility

  • Backward compatible with v5.14.x. Default behaviour is unchanged: qrencode still produces .vpnuri.png, and on CI hosts with exactly one installed kernel _resolve_kernel_version returns the same result as the previous loop.
  • The workaround previously required for long QRs (manually copying .vpnuri contents into the app) is no longer needed.

Upgrade

From v5.14.1 to v5.14.2:

wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.14.2/install_amneziawg_en.sh
sudo bash ./install_amneziawg_en.sh --force --yes

Step 5 of the installer pulls the latest manage_amneziawg.sh and awg_common.sh with SHA256 verification.

Full list of changes since v5.14.1


5.14.1 - 2026-05-19

v5.14.1 - patch release: manage regen now picks up the MTU from the server awg0.conf when regenerating client configs and no longer hardcodes 1280 in the client file. No architectural changes; default behaviour (MTU = 1280 on the server) is unchanged. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM (Raspberry Pi, Oracle Ampere, Hetzner CAX).

Highlights

  • 📐 MTU sync between server and client configs on regen. Before v5.14.1 both awg_common.sh:render_client_config and render_server_config hardcoded MTU = 1280. If the user hand-edited MTU in /etc/amnezia/amneziawg/awg0.conf, manage_amneziawg.sh regen still wrote the stale 1280 into the new client .conf. MTU resolution is now ordered: value from the [Interface] section of server awg0.conf (the source of truth for a running server), then AWG_MTU from awgsetup_cfg.init, then 1280 as fallback. The live-config parser (load_awg_params for AWG parameters from awg0.conf) also reads the MTU = ... line now and exports AWG_MTU. Values outside the sane range 576..9100 at any stage fall back to 1280. Reported in Discussion #38 by @E-lmedano.
  • 🔧 Installer: AWG_MTU variable in awgsetup_cfg.init. Fresh installs write AWG_MTU=1280 into the config file; the user can override via environment before running the installer (AWG_MTU=1380 sudo bash install_amneziawg.sh ...) and the value is preserved. The variable is also added to the safe_load_config whitelist.

Tests

+18 new bats (510 in the matrix, was 492 on v5.14.0):

  • test_v5141_mtu_resolution.bats (+18) - functional tests for _extract_mtu_from_server_conf (valid MTU from [Interface], whitespace around =, no MTU, ignored MTU in [Peer], last-wins on duplicates, missing server file, non-numeric value); functional tests for _validate_mtu (accepts 1280, boundary 576 and 9100, rejects 0 / -1 / 9101 / 575 / abc / empty); structural checks on render_client_config (no MTU = 1280 hardcode, uses ${mtu} substitution), render_server_config (uses ${AWG_MTU:-1280}); safe_load_config whitelist contains AWG_MTU in all 4 files; installer writes AWG_MTU to awgsetup_cfg.init (RU + EN); byte-identical _extract_mtu_from_server_conf between RU and EN.

Compatibility

  • Backwards-compatible with v5.13.x and v5.14.0. Default scenario behaviour is unchanged: with MTU = 1280 on the server, regen still produces 1280 in the client config.
  • The previous workaround (hand-editing /root/awg/<name>.conf after regen) is no longer needed - regen picks up whatever is in awg0.conf.

Updating

From v5.13.x / v5.14.0 to v5.14.1:

wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.14.1/install_amneziawg_en.sh
sudo bash ./install_amneziawg_en.sh --force --yes

Step 5 of the installer pulls fresh manage_amneziawg.sh and awg_common.sh with SHA256 verification.

Full diff against v5.14.0


5.14.0 - 2026-05-19

v5.14.0 - small feature release: more reliable public IP detection (extra fallback services for AWS / NAT'd cloud) plus a new manage diagnose subcommand for one-line self-troubleshooting. Backwards-compatible with v5.13.x installs; no architectural changes. Support matrix unchanged: Ubuntu 24.04 / 25.10 / 26.04, Debian 12 / 13, x86_64 + ARM (Raspberry Pi, Oracle Ampere, Hetzner CAX).

Highlights

  • 🌐 Extended public IP detection in awg_common.sh:get_server_public_ip. The fallback cascade grew from 4 to 6 services - api.ipify.org, checkip.amazonaws.com, icanhazip.com, ifconfig.io, ifconfig.me, ipinfo.io/ip (alphabetical order, deterministic for diffs and tests). checkip.amazonaws.com is reachable from AWS / GCP / OCI private subnets behind a NAT Gateway where ifconfig.me can rate-limit; ifconfig.io is a backup for ifconfig.me downtime. First-wins behaviour preserved: when a service returns a valid IPv4, the rest are skipped. Successful detection is now traced into /root/awg/install_amneziawg.log (or the manage log file) - written directly to file, never to stdout, so the function's $() capture contract is preserved and the generated client Endpoint = line is not corrupted.
  • 🩺 manage diagnose [--carrier=NAME] - new subcommand for one-line server self-troubleshooting. Without arguments it runs 6 health checks (kernel module loaded / service active / interface UP / sysctl ip_forward / BBR / UFW + AWG port + peer count). With --carrier=NAME it additionally compares the current AWG 2.0 obfuscation parameters (Jc / Jmin / Jmax / I1) against a known carrier profile and prints OK/WARN/FAIL per check with a Fix: hint. Seven confirmed carriers from ADVANCED.en.md operator matrix: beeline_msk (default preset); yota_msk, tele2_msk, tattelecom (mobile preset, random I1); tele2_krasnoyarsk, megafon_regions (mobile preset, I1 must be absent); tmobile_us (binary I1, from Discussion #45). Exit code 1 only on FAIL or unknown carrier; WARN does not change the rc. Bilingual RU + EN.
  • 🔒 Release signing design (docs/SIGNING_DESIGN.md, planning only). Threat model, tool choice (minisign over cosign / GPG), signing flow with trusted-comment binding to tag + filename for rollback protection, and a draft release-sign.yml workflow that uploads pre-generated .minisig files. Activation is gated on the maintainer generating an offline keypair and committing KEYS.txt to the repository root; until then the section in SECURITY.md describes the planned path.

Tests

+37 new bats (492 in the matrix, was 455 on v5.13.0):

  • test_v5140_public_ip_services.bats (+11) - structural RU/EN parity on the 6 endpoints, byte-identical service list across RU and EN, alphabetical-order assertions (first = api.ipify.org, last = ipinfo.io/ip), functional fallthrough (first service success / first fails-second succeeds / all 6 fail / invalid IP format skip / last-in-list success / cache short-circuit).
  • test_v5140_diagnose.bats (+16) - structural RU/EN parity on diagnose_server and the _diagnose_carrier_known, _diagnose_carrier_list, _diag_line helpers; CLI parser accepts --carrier=NAME; command dispatcher wires up diagnose; usage help mentions diagnose; functional checks on the carrier map (beeline_msk row matches default-preset shape, tele2_krasnoyarsk has i1=absent, tmobile_us has i1=binary + Jc=6; unknown carrier returns 1); carrier list has 7 distinct confirmed entries; previously-included unconfirmed mts_msk + megafon_msk are intentionally removed.

Compatibility

  • OS: Ubuntu 24.04 LTS, 25.10, 26.04 (with noble fallback). Debian 12 (bookworm), 13 (trixie).
  • Arch: amd64, arm64 (Raspberry Pi 4/5, Oracle Cloud Ampere, Hetzner CAX, AWS Graviton, other ARM VPS).
  • Russian carriers matrix in ADVANCED.en.md. The new diagnose --carrier=NAME recognises 7 confirmed rows; "🔄 testing" rows (Megafon Moscow, MTS Moscow) intentionally excluded until the operator range is confirmed and locked.

Out of scope

  • v5.14.1+: minor cleanups uncovered by post-release feedback.
  • v5.15.x: minisign signature activation (after maintainer keypair generation), per-client CPS profiles (Issue #71), --preset=mobile-awg1 for I1=none carriers.

Full diff against v5.13.0


5.13.0 — 2026-05-12

v5.13.0 — AmneziaWG 2.0 VPN installer release with Ubuntu 25.10 (questing) and 26.04 support, plus a --force safety guard against accidental re-install on a configured server. Ubuntu 24.04, Debian 12 / 13, x86_64 + ARM (Raspberry Pi, Oracle Ampere, Hetzner CAX) support — unchanged.

Highlights

  • 🛡️ PPA noble fallback for Ubuntu 25.10 / 26.04 (Issue #46). The Amnezia PPA does not yet publish packages for questing (25.10) or upcoming Ubuntu codenames. The installer now auto-detects the 404 on dists/<codename>/Release, remaps the suite to noble in /etc/apt/sources.list.d/amnezia-ppa.sources and re-runs apt update. If the server has leftover kernel headers from a previous 24.04 install (typical after do-release-upgrade), the installer also pulls in gcc-13 from questing/universe so the DKMS autoinstall succeeds for every kernel. No manual sources.list edits, no DKMS surprises. The script also repairs a "sticky" .sources file from a previous (≤ v5.12.1) run that left Suites: questing behind after an apt failure.
  • 🔒 --force safety guard (Issue #78). Re-running the installer on a server with AmneziaWG already configured now requires an explicit --force (or AWG_FORCE_REINSTALL=1). Without it, the script early-exits with a clear "already installed and running" message. Server keys, peer configs, and obfuscation parameters survive a re-run, but Step 1 re-tunes sysctl/swap/BBR, apt-get upgrade may pull a new kernel (and require another reboot), and Step 7 restarts awg-quick@awg0 — handshakes drop for a few seconds. The guard removes that foot-gun.
  • 🧹 manage_amneziawg.sh logging: WARN → stderr. In v5.12.1 manage_amneziawg.sh:log_msg routed only ERROR to stderr and leaked WARN to stdout — broke CI/automation parsing (stdout = "data", stderr = "diagnostics"). WARN and ERROR now both go to stderr, symmetric with install_amneziawg.sh:log_msg.
  • 💾 Precise /swapfile check in /etc/fstab. The old substring check grep -q '/swapfile' matched commented lines and partial-name hits (/swapfile.bak); on re-run the installer could mistakenly skip adding a valid entry — swap then failed to mount on reboot. Switched to an anchored field-aware awk check: !/^[[:space:]]*#/ && $1 == "/swapfile" && $3 == "swap". Idempotent and comment-resistant.

Installation

wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.13.0/install_amneziawg_en.sh
chmod +x install_amneziawg_en.sh
sudo bash ./install_amneziawg_en.sh

3 commands → ~20 minutes → a working VPN server with traffic obfuscation. Full guide — README → Installation.

Upgrading an existing server

Run the latest install_amneziawg.sh with --force (if AmneziaWG is already running) — Step 5 fetches the fresh manage_amneziawg.sh and awg_common.sh with SHA256 verification. Full commands — ADVANCED.en.md → Updating the scripts.

Tests

+68 new bats (455 in the matrix, was 387 on v5.12.1):

  • test_v5130_ppa_noble_fallback.bats (+33) — RU/EN structural greps on the pre-check block and suite-mismatch detection, parity counts, functional tests with mocked curl (404 → noble, timeout → noble, success → questing), LTS whitelist (noble/jammy/focal skip pre-check), suite mismatch deletes file on mismatch and preserves on match, corrupt .sources (missing Suites:) gets recreated, legacy .sources mismatch is removed, gcc-13 pre-install fires when stale headers are detected.
  • test_v5130_force_guard.bats (+19) — RU/EN structural greps on the --force|-f CLI flag, the AWG_FORCE_REINSTALL=1 env bridge, the idempotency guard [[ -f $SERVER_CONF_FILE ]] && systemctl is-active --quiet awg-quick@awg0, the help-section mention of -f, --force, RU/EN parity by FORCE_REINSTALL occurrence count; functional matrix of 6 cases (clean install / configured+active+no-force / configured+inactive+no-force → repair flow / configured+active+--force / env bridge / strict =1 env vs yes).
  • test_v5130_bundled_fixes.bats (+16) - warn-stderr: RU/EN log_msg routes WARN to stderr (structural + functional, INFO still on stdout); fstab-swap: awk check on /swapfile correctly detects a valid entry, rejects commented lines, rejects partial-name matches (/swapfile.bak), handles indented lines and an empty fstab.

Compatibility

  • OS: Ubuntu 24.04 LTS, 25.10, 26.04 (with noble fallback). Debian 12 (bookworm), 13 (trixie).
  • Arch: amd64, arm64 (Raspberry Pi 4/5, Oracle Cloud Ampere, Hetzner CAX, AWS Graviton, other ARM VPS)
  • Russian carriers: --preset=mobile works on Yota, Beeline, MTS, Tattelecom, Tele2 (Moscow), Megafon (Moscow). See the operator matrix in README.

Out of scope

  • v5.13.1: external review fixes (kernel ambiguity in build-arm-deb.sh), backlog refinements.
  • v5.14.0: --preset=mobile-awg1 (I1=none fallback for Tele2 Krasnoyarsk / Megafon regions).

Full roadmap — Issue #79.


5.12.1 — 2026-05-08

v5.12.1 — patch release of the AmneziaWG 2.0 VPN installer: three small fixes for issues found in the first 48 hours after v5.12.0. No new features, no architectural changes. Support matrix unchanged: Ubuntu 24.04 / 25.10, Debian 12 / 13, x86_64 + ARM (Raspberry Pi, Oracle Ampere, Hetzner CAX).

Highlights

  • 🔧 AWG_SKIP_APPLY=1 works again in manage add / manage remove. v5.12.0 added an unconditional pre-call to ensure_amneziawg_kernel_module before both actions to make awg syncconf reliable. Side effect: it broke the offline edit-only flow on dev / CI machines without the kernel module loaded, where AWG_SKIP_APPLY=1 accumulates changes for batch apply (see ADVANCED.en.md — environment variables section). The pre-call is now wrapped in if [[ "${AWG_SKIP_APPLY:-0}" != "1" ]]. manage restart is intentionally NOT gated — it is an explicit apply, AWG_SKIP_APPLY has no meaningful semantics for it. Only the literal 1 is honoured; yes, true, any other string — same behaviour as unset (apply happens).
  • ☁ linux-headers-cloud-${arch} in the repair-module fallback on Debian. awg_common.sh:_install_kernel_headers (used by manage repair-module) on Debian only tried linux-headers-${kernel_ver} and linux-headers-${arch}. On AWS / Azure / GCP / cloud-Hetzner (kernel name contains -cloud-) the exact-version package can disappear from the mirror after a kernel upgrade, while the cloud meta linux-headers-cloud-${arch} stays available. The installer's step 2 already knew about cloud-headers via smart detection; now repair-module knows too and tries the cloud meta before the generic one. Standard kernels (no -cloud- in the name) — behaviour unchanged.
  • 📦 ARM prebuilt packages now decompress correctly with the in-tree kernel decoder (Issue #76). scripts/build-arm-deb.sh used xz -9 (CRC64 check, 64 MiB dictionary) — the userspace xz -t tool considered the stream valid, but the in-tree Linux decoder on Debian 13 trixie kernel 6.12.85+deb13-arm64 (build 2026-04-30) returned decompression failed with status 6. Switched to a kernel-compatible preset xz --check=crc32 --lzma2=dict=1MiB — matches mainline scripts/Makefile.modinst. Plus a build-time sanity gate: after compression, xz -t + xz -d -c round-trip; if anything fails — exit 1, no broken prebuilt ships to the arm-packages release. On the v5.12.1 tag push, CI workflow arm-build.yml re-publishes all 14 ARM prebuilt packages with the new xz flags.

Install

wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.12.1/install_amneziawg_en.sh
chmod +x install_amneziawg_en.sh
sudo bash ./install_amneziawg_en.sh

3 commands → ~20 minutes → a ready-to-use VPN server with traffic obfuscation. Details — README → Install.

Upgrading an existing server

Run a fresh install_amneziawg_en.sh — at step 5, manage_amneziawg.sh and awg_common.sh are updated automatically (with SHA256 verification). Full commands — ADVANCED.en.md → How to update the scripts.

Tests

+30 new bats tests (387 in the matrix, was 357 in v5.12.0):

  • test_v5121_skip_apply_regression.bats (+14) — RU/EN structural greps on add / remove / restart blocks, plus runtime semantics of the gate for every documented value (unset / 0 / 1 / yes / true / YES); confirms repair-module keeps the AWG_ALLOW_APT_IN_ENSURE=1 ensure_amneziawg_kernel_module full invocation; bash -n syntax sanity.
  • test_v5121_cloud_headers.bats (+9) — functional test of _install_kernel_headers via mocked apt-get and dpkg: cloud kernel gets the cloud meta in candidates before the generic one, standard kernel does not, Ubuntu codepath untouched; the EN mirror awg_common_en.sh is verified separately.
  • test_v5121_xz_kernel_compat.bats (+7) — structural greps for the new xz flags in build-arm-deb.sh, fail-fast on sanity-stage failure, local round-trip with the same flags (toolchain smoke test; kernel-decompressor compatibility itself is best validated on a real kernel boot — VPS or QEMU with the target kernel).
  • test_v5115_regen_multiarg.bats — version assertion bumped from 5.12.0 to 5.12.1.

Compatibility and dependencies

  • Fully backwards-compatible. All three fixes change behaviour only in narrow regression cases (offline edit / cloud-kernel repair / ARM prebuilt on Debian 13 trixie). The standard install + client flow is unchanged.
  • No new dependencies.

Full diff against v5.12.0


5.12.0 — 2026-05-06

v5.12.0 — feature release of the AmneziaWG 2.0 VPN installer: one big feature — automatic DKMS module recovery on kernel upgrade. No architectural changes: compatible with all v5.11.x installs — the apt hook, systemd unit, and helper are deployed on the next run of install_amneziawg_en.sh. Support matrix unchanged: Ubuntu 24.04 / 25.10, Debian 12 / 13, x86_64 + ARM (Raspberry Pi, Oracle Ampere, Hetzner CAX).

Highlights

  • 🛡 DKMS auto-repair on kernel upgrade — three layers of safety net. Before v5.12.0, after apt upgrade of the kernel, DKMS did not always rebuild the amneziawg module by the next reboot — awg-quick@awg0 failed with modprobe: FATAL: Module amneziawg not found, and the VPN was down until manual recovery. Now three safety nets work transparently:
    • apt hook (/etc/apt/apt.conf.d/99-amneziawg-post-kernel) — DPkg::Post-Invoke runs /usr/local/sbin/amneziawg-ensure-module --hook, which iterates /lib/modules/*/build, rebuilds DKMS for every target kernel with installed headers, then depmod -a. Log at /var/log/amneziawg-ensure-module.log (logrotate weekly, 4 copies). The stamp file /var/lib/amneziawg/ensure-module.stamp silences the hook on routine apt operations unrelated to the kernel.
    • systemd unit (amneziawg-ensure-module.service) — Type=oneshot, Before=awg-quick@awg0.service, After=systemd-modules-load.service local-fs.target. At boot, before awg-quick, it iterates kernels with already-installed headers (/lib/modules/*/build), rebuilds DKMS, runs modprobe amneziawg, and verifies via lsmod. If headers are missing, it logs a WARN and exits successfully — installing the headers themselves is the job of step 2 of the installer or of manage repair-module. Logs in journal (journalctl -u amneziawg-ensure-module.service). ConditionPathExists=/usr/local/sbin/amneziawg-ensure-module — the unit will not fail if the helper has been removed.
    • manage repair-module — explicit fallback for interactive recovery: sudo bash /root/awg/manage_amneziawg.sh repair-module. Sets AWG_ALLOW_APT_IN_ENSURE=1 (apt-get install of kernel-headers is allowed only in this context — the apt hook and systemd unit do not use it to avoid blocking on dpkg-lock).
  • 🧠 Smart kernel-headers meta-package detection. Step 2 of the installer now installs a meta-package matched to your kernel rather than pinning to linux-headers-$(uname -r): on Ubuntu it extracts the flavor from uname -r (aws/azure/gcp/oracle/kvm/lowlatency/raspi) with a fallback to linux-headers-generic; on Debian — linux-headers-cloud-${arch} for cloud kernels, otherwise linux-headers-${arch}. This protects against the case where apt-get upgrade raised the kernel, but the new module fails to build without matching headers.

Other

  • 🛠 manage_amneziawg.sh pre-calls ensure_amneziawg_kernel_module in add / remove / restart. If the module is unloaded or doesn't match the current kernel, it tries to recover before the operation — this removes half of the "add a client doesn't work after apt upgrade" reports.
  • 🧹 step_uninstall cleans up the auto-repair components. On --uninstall the systemd unit is disabled, and the apt hook, helper, logrotate config, stamp directory /var/lib/amneziawg/, and rotated logs /var/log/amneziawg-ensure-module.log* are removed. Idempotent — installs from before v5.12.0 are unaffected.

Install

wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.12.0/install_amneziawg_en.sh
chmod +x install_amneziawg_en.sh
sudo bash ./install_amneziawg_en.sh

3 commands → ~20 minutes → a working VPN server with traffic obfuscation. Details — README → Install.

Upgrading an existing server

Run the fresh install_amneziawg_en.sh — at step 5 manage_amneziawg.sh and awg_common.sh are refreshed automatically (with SHA256 verification), and at step 2 the apt hook, systemd unit, helper, and logrotate config are deployed. For an existing server this is a safe re-run. Full commands — ADVANCED.en.md → Updating the scripts.

Tests

+32 new bats (357 total, was 325 in v5.11.5):

  • test_v512_dkms_repair.bats (+32) — structural coverage of the DKMS auto-repair deployment phases: 4 functions in awg_common.sh+_en.sh, ≥3 pre-calls in manage (add/remove/restart), the manage repair-module|repair command, the AWG_ALLOW_APT_IN_ENSURE gate; smart kernel-headers candidate-loop (Ubuntu flavor extraction, Debian cloud detection, RPi guard, fallback ordering: flavor BEFORE generic + cloud BEFORE arch); helper --hook|--systemd modes, stamp fast-path gated to --hook, modprobe+lsmod in --systemd plus 2 exit-1 paths, helper does not use apt-get; systemd unit 12 directives, atomic deploy, daemon-reload+enable; byte-identical RU/EN for helper, hook, logrotate, unit; atomic deploy cleanup-on-failure for all 4 staging vars; helper body parses with bash -n. Mock-based runtime tests (kernel upgrade simulation) are run on VPS Ubuntu 24.04 + Debian 13 as part of the release test.

Compatibility and dependencies

  • Fully backwards-compatible. v5.11.x installs continue to work as before; auto-repair components are deployed on the next run of install_amneziawg_en.sh — re-run is safe. Step 2 now installs a meta-package for headers, which protects against kernel-upgrade-without-headers; no extra packages need to be installed manually.
  • No new dependencies. The helper uses dkms, depmod, modprobe, systemctl — all from the base install. The apt hook is a POSIX-sh inner command (not bash). The systemd unit is Type=oneshot, no timers needed.

5.11.5 — 2026-05-05

v5.11.5 — bug-fix release of the AmneziaWG 2.0 VPN installer: two small fixes after v5.11.4, no architectural changes. Support matrix unchanged: Ubuntu 24.04 / 25.10, Debian 12 / 13, x86_64 + ARM (Raspberry Pi, Oracle Ampere, Hetzner CAX).

Highlights

  • 🔁 manage regen c1 c2 c3 now regenerates all three clients, not just the first. Until v5.11.5 the regen case picked up only the first argument from the list — the rest were silently dropped. add and remove already had the loop over arguments, but regen was missing it. Behaviour is now consistent: each name is validated and processed individually, a missing client logs a warning and contributes rc=1, valid ones still get regenerated. A summary at the end prints Processed: N of M. The no-args path (manage regen regenerates all clients) is unchanged. (#70, @Barmem)
  • 🛡 Step 2: a hard apt-get update error is no longer masked. In v5.11.4 I downgraded the apt-get update check at step 2 to a warning, so that a brief Launchpad PPA outage (issue #68) would not break the install. Side effect: real apt errors — DNS, GPG mismatch, dpkg lock contention on the base mirror — let the install continue on a stale apt-cache and fail later with a less actionable error. The logic now distinguishes the two scenarios: errors only on the Amnezia PPA (issue #68) — continue, apt_wait_for_ppa_package retries; any other non-source error — die with concrete pointers (DNS / /etc/apt/keyrings / dpkg lock). The OOM / silent-crash edge case is also covered — apt failures with no classifiable lines are no longer swallowed even if a PPA URL appears in the output. (post-merge review on PR #69)

Other

  • 📚 Docs: AWG 2.0 vs AWG 1.0 (S3/S4). Added a FAQ entry to ADVANCED.en.md noting that an AmneziaWG 2.0 server with S3>0 or S4>0 is not compatible with AWG 1.0 clients (see upstream issue amnezia-vpn/amneziawg-linux-kernel-module#168). My installer always generates S3=8..55 and S4=4..27 — both >0 — so in the typical scenario (Amnezia VPN client + clients generated by manage) this does not surface. Risk only when manually importing the server preset into a WireGuard/AWG 1.0 client.

Install

wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.11.5/install_amneziawg_en.sh
chmod +x install_amneziawg_en.sh
sudo bash ./install_amneziawg_en.sh

3 commands → ~20 minutes → a working VPN server with traffic obfuscation. Details — README → Install.

Upgrading an existing server

Run the fresh install_amneziawg_en.sh — at step 5 manage_amneziawg.sh and awg_common.sh are refreshed automatically (with SHA256 verification). Full commands — ADVANCED.en.md → Updating the scripts.

Tests

+13 new bats (325 total, was 312 in v5.11.4):

  • test_v5115_regen_multiarg.bats (+13) — RU/EN regen case iterates ARGS[@] (the headline fix for #70); _regen_count counter and the summary line Обработано: N из M / Processed: N of M; no regression: single-arg regen <name> and no-args regen behave as before; missing/invalid names yield a warning + rc=1 and the batch keeps going; RU/EN structural parity on the regen branch control-flow tokens; apt_update_tolerant accepts the --ppa-amnezia-tolerant flag with local ppa_tolerant=0 declared in both installers; step 2 calls the helper with that flag and dies on a hard error; the OOM / silent-crash guard via raw_had_non_src_errors is present in both installers; SCRIPT_VERSION="5.11.5" is bumped in all six files.

Compatibility and dependencies

  • Fully backwards-compatible. Single-arg manage regen <name> and no-args manage regen are unchanged; only the multi-arg path is affected — names that used to be silently dropped are now processed. Step 2 behaviour during a normal install is unchanged too — strict mode only triggers on a real hard apt error that would have blocked the install anyway.
  • No new dependencies.

5.11.4 — 2026-05-04

v5.11.4 — bug-fix release of the AmneziaWG 2.0 VPN installer: two fixes for issues reported on top of v5.11.3, no architectural changes. Support matrix unchanged: Ubuntu 24.04 / 25.10, Debian 12 / 13, x86_64 + ARM (Raspberry Pi, Oracle Ampere, Hetzner CAX).

Highlights

  • 🔑 vpn:// import into the Amnezia VPN app now carries the PSK. With manage add --psk the PresharedKey was correctly written to the server [Peer] and the client .conf since v5.11.1, but the vpn:// URI was missing the psk_key field that the AmneziaVPN parser reads — clients silently came up without the preshared key, the server (which had it) rejected the handshake, and awg show transfer stayed at «never». Also tightened trailing CR / space stripping for PresharedKey = and AllowedIPs = so CRLF configs edited on Windows no longer leak \r into the JSON. (#67, @haritos90)
  • 🔁 Install survives a brief Launchpad PPA outage. When ppa.launchpadcontent.net is briefly unreachable (as on May 3rd per #68), the installer now waits for amneziawg-dkms to show up in apt-cache for up to 3 attempts with 30 s and 60 s backoff (and a fresh apt-get update between retries). Checking apt-cache matters: apt-get update itself is tolerant to an unreachable InRelease (returns 0 even when the PPA never downloaded), so a plain rc-based retry would not catch this case. After three failures a friendly message points the user at the issue and explains this is a Launchpad infrastructure outage, not a script bug. (#68, @saligin / @baikov)

Install

wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.11.4/install_amneziawg_en.sh
chmod +x install_amneziawg_en.sh
sudo bash ./install_amneziawg_en.sh

3 commands → ~20 minutes → a working VPN server with traffic obfuscation. Details — README → Install.

Upgrading an existing server

Run the fresh install_amneziawg_en.sh — at step 5 manage_amneziawg.sh and awg_common.sh are refreshed automatically (with SHA256 verification). Full commands — ADVANCED.en.md → Updating the scripts.

Tests

+16 new bats (312 total, was 296 in v5.11.3):

  • test_v5114_psk_uri.bats (+5) — happy path with PSK; no PSK → no psk_key field; indented PresharedKey; CRLF-edited config does not leak \r into the JSON; empty PresharedKey = line does not turn into psk_key:"" (which would mismatch a server with a real PSK anyway).
  • test_v5114_ppa_retry.bats (+11) — success on first attempt; retry until success; exhausting max attempts; exponential backoff doubling; 1800 s cap against arithmetic overflow; RU/EN structural parity of the helper; issue #68 link present in both installers.

Compatibility and dependencies

  • Fully backwards-compatible. On a stable network the retry helper adds zero delay — the first attempt passes and the rest of the run is unchanged. manage add --psk without vpn:// import behaves the same as before (the PSK has always been written to the .conf correctly).
  • No new dependencies. Just bash arithmetic and sleep — both standard.

5.11.3 — 2026-04-28

v5.11.3 is a UX release for the AmneziaWG 2.0 VPN installer: five improvements driven by recent issues and discussions, no architectural changes. Ubuntu 24.04 / 25.10, Debian 12 / 13, x86_64 + ARM (Raspberry Pi, Oracle Ampere, Hetzner CAX) — supported as before.

Highlights

  • 🍎 Shadowrocket on iOS / macOS now connects out of the box. The --psk flag for manage add shipped in v5.11.1 but was not visible in the README — it is now in the Quick Reference and has its own FAQ entry. (#62, @andreykorobko)
  • 📡 Ping inside the tunnel — server ↔ clients — a step-by-step recipe for UFW + /etc/ufw/before.rules in the FAQ. Explicit warning: ufw allow ... proto icmp does not work (UFW only supports tcp/udp/esp/ah/gre/ipv6 via the proto flag). (#63, @PavelVVrn)
  • 🌐 Mobile carrier → I1 map extended. Megafon (regions) and Tele2 (Krasnoyarsk) updated to I1=absent — the AWG 1.0 fallback for carriers where CPS packets themselves trigger DPI blocks. Exact commands below the table (systemctl restart awg-quick@awg0 + manage regen <name>). (#42, @alkorrnd)
  • 🤖 Auto-scripts for cron / Ansible / Proxmox. manage --yes (flag) or AWG_YES=1 (env) skip the confirm prompt in remove, restore, restart. Default behavior is unchanged (opt-in).
  • 🗂️ Backups without collisions. Millisecond suffix in filenames (awg_backup_2026-04-28_15-53-50.123.tar.gz) protects against overwrite when two backups land in the same second (e.g., regen → backup → modify → backup). Legacy filenames (no .NNN) keep working.

Install

wget https://raw.githubusercontent.com/bivlked/amneziawg-installer/v5.11.3/install_amneziawg_en.sh
chmod +x install_amneziawg_en.sh
sudo bash ./install_amneziawg_en.sh

3 commands → ~20 minutes → a working VPN server with traffic obfuscation. More — README.en.md → Installation.

Upgrading an existing server

Re-run the latest install_amneziawg_en.sh — step 5 refreshes manage_amneziawg.sh and awg_common.sh automatically (with SHA256 verification). Full commands — ADVANCED.en.md → How to Update Scripts.

Tests

+34 new bats (295 total, up from 261 on v5.11.2):

  • test_yes_flag.bats (+11) — extract confirm_action via awk + eval. Verifies that non-"1" values of AWG_YES ("yes", "true", "0") do not match the bypass branch under forced-interactive mode.
  • test_backup_collision.bats (+8) — date +%3N produces distinct values under rapid fire; find pattern and sort -r correctly handle both legacy and ms-suffix names in the same directory.
  • test_v5113_docs.bats (+15) — invariants for the ICMP FAQ, the carrier table, the --psk highlight; protection against RU/EN cross-link drift in README → ADVANCED.

Compatibility and dependencies

  • Fully backwards-compatible. --yes is opt-in, default behavior unchanged. Backup ms-suffix is designed to coexist with legacy filenames. Downgrading to v5.11.2 is safe.
  • No new dependencies. date +%3N (milliseconds) is part of standard GNU coreutils, available out of the box on Ubuntu/Debian.

5.11.2 — 2026-04-24

UX patch on top of v5.11.1. A second per-client QR code — rendered from the vpn:// URI — for one-tap import into the flagship Amnezia VPN app (Android / iOS / Desktop). The existing <name>.png (scan of .conf) is unchanged and keeps working with WireGuard-compatible clients (AmneziaWG Windows, wireguard-apple, wg-quick).

Added

  • New generate_qr_vpnuri helper in awg_common.sh / awg_common_en.sh. It reads /root/awg/<name>.vpnuri (the URI that generate_vpn_uri has been producing for a while now — full Amnezia envelope: zlib-JSON with containers/defaultContainer/hostName/dns/mtu/protocol_version=2 plus all AWG 2.0 params), pipes it into qrencode -t png, writes /root/awg/<name>.vpnuri.png with mode 600. Writes are atomic: first into <name>.vpnuri.png.tmp.$$ in the same directory, chmod 600, then mv -f to the target path — if qrencode or chmod fails, the old file stays intact and the orphan .tmp.* is cleaned up.
  • Hooked into generate_client and regenerate_client. After a successful generate_vpn_uri, generate_qr_vpnuri runs. If the URI itself cannot be built (missing perl modules, params not loaded), the vpn:// QR is silently skipped. The wg-quick QR and vpn:// QR are independent best-effort artifacts — a failure in one does not prevent the other.
  • Hooked into manage regen and manage remove. regen now refreshes both QR codes (conf and vpn://) together. remove cleans up <name>.vpnuri.png alongside <name>.conf / .png / .vpnuri and keys.
  • Backup / restore picks up .vpnuri.png automatically — no new code paths needed: the existing *.png glob in _backup_configs_nolock and chmod 600 *.png in restore_backup already cover the new artifact.

Why

The flagship Amnezia VPN app (Android / iOS / Desktop) supports one-tap import by scanning a QR that encodes vpn://{base64url(zlib(json))}. I have been generating that URI for a while, but only as a plain text .vpnuri file that users had to copy over manually. Now you can point the phone camera at the second QR code instead of copying a file. The first QR (from .conf) remains the right one for classic WireGuard clients.

Tests

  • +10 new bats (261 total, up from 251 on v5.11.1):
    • test_qr_vpnuri.bats (+10) — happy path (stdin → PNG), missing .vpnuri → error, qrencode non-zero exit → error, atomic write (pre-existing .vpnuri.png is preserved on failure, no orphan .tmp.* left behind), chmod 600 on Linux/Darwin, RU/EN structural parity of the helper (qrencode call + .vpnuri.png target + command -v guard), hooks in generate_client / regenerate_client / manage regen / cleanup in manage remove.

Breaking changes

None. Existing client .conf / .png / .vpnuri files keep working. The new .vpnuri.png is only generated for clients created or regenerated on v5.11.2 — for older clients, a single manage regen <name> is enough. Downgrading to v5.11.1 is safe (stale .vpnuri.png files just sit in /root/awg/ and are ignored).

Dependencies

No new ones: qrencode was already in the installer step-2 required list (used by generate_qr for .conf), and perl + Compress::Zlib + MIME::Base64 — already for generate_vpn_uri.


5.11.1 — 2026-04-23

UX patch. Three small improvements for manage on manual (non-installer) setups — e.g. amneziawg-go userspace in LXC. Credit to @Akh-commits for the detailed live-test in Issue #51 on 2026-04-22, which is where all three fixes came from.

Fixed / Added

  • manage add and regen now work without the server_public.key cache. A new _ensure_server_public_key helper computes the server public key from the [Interface] PrivateKey in awg0.conf via awg pubkey if /root/awg/server_public.key is missing (typical for installs made outside my installer — that cache is only populated there). The result is written atomically (tmp + mv) with mode 600. The awk extractor tolerates leading whitespace before PrivateKey = (hand-edited configs).
  • Endpoint fallback chain for egress-restricted setups. Previously manage add inside LXC without access to external IP services died with "Failed to determine server public IP". Now, after curl to ifconfig.me/ipify/icanhazip/ipinfo fails, I try the first non-loopback IPv4 on a global-scope interface (ip -4 -o addr show scope global). The user gets a log_warn suggesting they hand-edit Endpoint in the client .conf if the server sits behind NAT.
  • New manage add --psk flag. Optionally enables PresharedKey in the client .conf and in the server [Peer]. Generates a 32-byte key via awg genpsk for every client in batch mode (distinct PSK per client). Off by default — AWG 2.0 obfuscation is sufficient in most scenarios, and PSK is an extra layer for the paranoid or for compatibility with classic WireGuard deployments. Documented in ADVANCED.md / ADVANCED.en.md manage CLI section.

Tests

  • +19 new bats (249 total, up from 230 on v5.11.0):
    • test_server_pubkey_autogen.bats (+7) — no-op when cache exists, reconstruct from awg0.conf, edge cases (missing file, missing PrivateKey, ignore PrivateKey in [Peer] sections, indented PrivateKey, RU/EN parity).
    • test_endpoint_fallback.bats (+5) — returns IPv4 on a global-scope interface, empty output when no global scope, skips loopback, picks first of many interfaces, RU/EN parity.
    • test_psk_flag.bats (+7) — PresharedKey absent without flag, written when CLIENT_PSK is set, correct ordering inside [Peer] blocks, CLIENT_PSK="auto" resolution in generate_client, --psk parsing in RU+EN manage, help mention.

Breaking changes

None. All three changes are additive — the existing install flow is unchanged, and without the --psk flag manage add behaves identically to v5.11.0.


5.11.0 — 2026-04-22

Robustness bundle — I closed a batch of scenarios where install or manage could leave the system in a half-configured state on failure: running install twice without reboot, a helpers download being interrupted, a kill during restore, a failed backup before a destructive modify, a race between concurrent regen calls. The CI ARM matrix now also ships prebuilt packages for Ubuntu 25.10 and Debian 13. Upgrading is recommended but not required — v5.10.2 remains working, no blocking bugs there.

Fixed — install_amneziawg.sh

  • Running install twice without reboot no longer breaks DKMS. request_reboot now saves /proc/sys/kernel/random/boot_id to $AWG_DIR/.boot_id_before_step2 before the step 1→2 reboot. On entry to step 2 the installer compares the saved boot_id against the current one: if they match, the installer dies with "a reboot was expected before step 2". Previously re-running without the reboot attempted to build amneziawg-dkms against the wrong kernel and crashed on vermagic.
  • setup_state write is now atomic. Uses tmp + flock + mv -f via a PID-specific tmp path (${STATE_FILE}.tmp.$BASHPID). Parallel-invocation scenarios can no longer read a half-written step number.
  • awg_common.sh and manage_amneziawg.sh download via mktemp + SHA256 + atomic mv. New helper _secure_download(): curl writes to mktemp, SHA256 is verified, the verified file is mv'd to the target in one step. An interrupted connection no longer leaves a half-written helper in /root/awg/. Same pattern applied to the GPG keyring during PPA import.

Fixed — manage_amneziawg.sh + awg_common.sh

  • restore_backup now rolls back on failure. Before any destructive operation restore creates a pre-restore snapshot (this already existed as an undo aid). In v5.11.0 the snapshot is made known to the function (via LAST_BACKUP_PATH) and all error paths are wrapped in trap _restore_cleanup RETURN. If anything fails after systemctl stop, the cleanup unpacks the snapshot back into place and runs systemctl start awg-quick@awg0. A pre-flight validate_awg_config check is added before service start — if the restored config does not validate, the service is not started "broken"; rollback kicks in instead. The trap clears its own RETURN handler first (trap - RETURN) to avoid leaking into subsequent calls.
  • _backup_configs_nolock no longer hides cp failures on critical files. The silent || true is gone. A cp failure on critical artifacts (awg0.conf, awgsetup_cfg.init, server_public.key, server_private.key, client *.conf, $KEYS_DIR/*, expiry/, /etc/cron.d/awg-expiry) now returns 1 — a corrupted backup is more dangerous than a missing one. Optional artifacts (QR *.png, *.vpnuri) keep log_warn semantics. Empty globs are distinguished from cp failures via a compgen -G pre-check.
  • modify_client no longer runs a destructive sed after a failed backup. Previously cp "$cf" "$bak" || log_warn "..." — a warning in the log, then sed -i would destroy the config with no way back. The backup is now a hard gate: if ! cp ...; then log_error + release lock + return 1.
  • regenerate_client is serialized under a lock and every sed is checked. The function now wraps its body in .awg_config.lock (flock, 10 s timeout) — concurrent regen calls on the same client name can no longer corrupt the client config. The three sed -i statements that restore user settings (DNS, PersistentKeepalive, AllowedIPs) each use if ! — on failure the function returns 1 and the lock is released. The lock is held only while .conf is being mutated; it is released before generate_qr/generate_vpn_uri, which remain best-effort derived artifacts.
  • modify_client flock-timeout no longer leaks the fd. The "another operation holds the lock" branch now calls exec {modify_lock_fd}>&- before return 1. Previously the fd stayed open until shell exit.
  • manage_amneziawg.sh version is back in sync between RU and EN. The drifted 5.10.0 / 5.10.1 values converge to 5.11.0.

CI / build

  • ARM matrix: Ubuntu 25.10 and Debian 13 added, Ubuntu 22.04 removed. .github/workflows/arm-build.yml now builds the prebuilt amneziawg.ko for 7 targets: 3× Raspberry Pi + ubuntu-2404-arm64 + ubuntu-2510-arm64 + debian-bookworm-arm64 + debian-trixie-arm64. The matrix matches the installer supported-OS list exactly. _try_install_prebuilt_arm in install_amneziawg.sh was updated in sync — new branches *-generic* + 25.10 → ubuntu-2510-arm64 and *-arm64* + debian + 13 → debian-trixie-arm64; the dead 22.04 branch is gone.
  • Timeouts on every workflow job. shellcheck:10m, test:10m, release:15m, arm-build prepare:5m, build:60m. A hung job no longer quietly burns CI minutes. (Already shipped in the polish PR #55 toward v5.10.2; listed here for completeness.)

Docs

  • Minimum awg0.conf for AWG 2.0 in ADVANCED.md / ADVANCED.en.md. A new collapsible section with a ready example for manual setups (amneziawg-go in LXC, etc.): all 11 obfuscation parameters (Jc/Jmin/Jmax/S1-S4/H1-H4), notes about S3/S4 (added to AWG 2.0 later than S1/S2 — configs carried over from AWG 1.x may not have them), INT32_MAX upper bound on H1-H4, I1 being optional.
  • Explanation of the #_Name = <name> marker inside the "Full List of Management Commands" section — previously implicit in examples only. It is now explicit: list/remove/regen/modify rely on this marker in each [Peer] block; if you migrate awg0.conf from an old server, add #_Name by hand.
  • "LXC / Docker via amneziawg-go (userspace)" section in ADVANCED (source: @Akh-commits, Issue #51). A working recipe for a privileged LXC on Proxmox 9 with a Debian 13 guest, security tradeoffs, prebuilt binary vs source build. Shipped to main before the v5.11.0 tag; listing it here for completeness.

Tests

  • +84 new bats (230 total, up from 146 on v5.10.2).
    • test_state_machine.bats (+18) — atomic update_state, boot_id guard, step 2 entry die, request_reboot capture.
    • test_manage_robustness.bats (+24) — _backup_configs_nolock contract (LAST_BACKUP_PATH, compgen -G, critical vs optional), modify_client backup gate, regenerate_client lock + sed checks, flock-timeout fd release.
    • test_restore_rollback.bats (+27) — _restore_do_rollback helper, trap RETURN + cleanup contract, _destructive_ops_started gate, pre-flight validate_awg_config, trap/rollback regression guards.
    • test_arm_matrix.bats (+15) — matrix-to-installer cross-reference, RU/EN mapping parity, absence of the dropped 22.04 branch.
  • Bonus: 9 tests whose names contained Unicode em-dash/arrow characters (silently skipped by the bats parser) are now ASCII and actually execute.

Breaking changes

  • None. restore_backup externally behaves as before (success → service up; failure → previously partial state, now rollback); the manage CLI is unchanged; the awgsetup_cfg.init format stays compatible; SHA256 pins for helpers were updated — a downgrade from v5.11.0 back to v5.10.2 is possible by restoring the previous files.

5.10.2 — 2026-04-20

Urgent hotfix. In v5.10.1 every fresh AmneziaWG 2.0 install died at step 1 with apt_update_tolerant: command not found — on all mirrors, not only Hetzner. If you tried v5.10.1 on a new server, or you're about to deploy from scratch, move to v5.10.2. This release also closes an edge case where apt_update_tolerant could silently ignore a crash (SIGKILL, OOM).

Fixed

  • Critical regression in v5.10.1: apt_update_tolerant: command not found broke installation. The function was defined in awg_common.sh, but that file is only downloaded at step 5. The first apt update at step 1 (before the system upgrade) and the second at step 2 (after adding the PPA) received command not found, and installation aborted with die "apt update error". In v5.10.2 the definition moved inline into install_amneziawg.sh — next to log/die, following the existing pattern used for generate_awg_params. It has been removed from awg_common.sh.
  • Edge case in apt_update_tolerant: silent crash / OOM / SIGKILL are no longer masked. If apt-get update returned non-zero WITHOUT classifiable E:/Err:/W: lines in stderr (SIGKILL from OOM-killer, silent crash, unknown output format), the function erroneously returned 0 with a "source packages unavailable" message. Now, before falling back to that branch, the output is checked for explicit source-markers; if none are present, the error is propagated.
  • Regex future-proofing. The pattern Sources([[:space:]]|$) is replaced with Sources([^[:alpha:]]|$) — catches future variants like Sources.xz, while preventing false-match on strings like SourcesMirror.
  • Synced header date in install_amneziawg_en.sh (was 2026-04-16, now 2026-04-20 to match the release).

Tests

  • +9 new bats tests (146 total, was 137).
    • test_apt_tolerant.bats: +3 tests — silent crash (rc!=0, empty stderr), DNS failure without E: prefix, regex does not match SourcesMirror. Function loading migrated from source awg_common.sh to sed range extraction from install_amneziawg.sh.
    • test_install_defines_apt_tolerant.bats (new, 6 tests) — regression guard: asserts the invariant "definition is inline in both install scripts, absent from awg_common" + all calls follow the definition line.

5.10.1 — 2026-04-19

Compatibility with mirrors that don't publish source packages (Hetzner, AWS, and others) — Discussion #47.

Fixed

  • apt update no longer dies on 404 for source packages. Some mirrors (Hetzner Ubuntu, AWS Ubuntu) don't publish source packages, but the default /etc/apt/sources.list.d/ubuntu.sources contains Types: deb deb-src. The previous apt update -y || die failed in that case. The new apt_update_tolerant function (in awg_common.sh; moved inline into install_amneziawg.sh in v5.10.2) ignores 404s only on source/Sources/deb-src, but propagates every other error (GPG, network, unreachable PPA).
  • Removed modification of /etc/apt/sources.list.d/ubuntu.sources. The installer no longer enables deb-src — we never used source packages (kernel module installs via DKMS + binary headers), so the modification was unnecessary and caused the issue.

Tests

  • +6 new bats tests (137 total, was 131). test_apt_tolerant.bats: clean update, source-only 404, deb-src 404, GPG error, binary 404, mixed errors.

5.10.0 — 2026-04-16

Mobile network optimization: --preset=mobile and --jc/--jmin/--jmax CLI flags, comprehensive security and reliability audit across the entire codebase (Discussion #38, Issue #42).

Added

  • --preset=mobile CLI flag for mobile carriers. Locks Jc=3 with narrow Jmax (Jmin+20..80) — confirmed working settings for Tele2, Yota, Megafon, Tattelecom and other carriers that block AWG connections with Jc>3 or Jmax>300. --preset=default is also available for explicit selection of the standard profile (Jc=3-6, Jmin=40-89, Jmax=Jmin+50..250).
  • --jc=N, --jmin=N, --jmax=N CLI flags. Fine-grained override of obfuscation parameters on top of any preset. Jc: 1-128, Jmin/Jmax: 0-1280, Jmax must be ≥ Jmin. Example: --preset=mobile --jc=4 uses the mobile profile but with Jc=4 instead of 3.
  • Protocol boundary validation in validate_awg_config. Checks AWG parameter ranges after backup restore: Jc (1-128), Jmin/Jmax (0-1280, Jmax ≥ Jmin), S3 (0-64), S4 (0-32), H1-H4 range ordering (lower < upper).
  • AWG_PRESET saved to configuration. The selected preset is recorded in awgsetup_cfg.init for diagnostics and reproducibility.

Security

  • Config parser BOM and CRLF hardening. safe_load_config and safe_read_config_key now strip BOM (UTF-8 \xEF\xBB\xBF) and CR (\r) before parsing. Prevents issues when configs are edited in Windows text editors.
  • Special character escaping in regenerate_client. sed replacements properly escape &, \, / in values, preventing injection through client keys.
  • GitHub Actions pinned to SHA. All 7 actions across 4 workflows are pinned to specific commit SHAs instead of mutable tags (supply chain protection).
  • Endpoint masking in diagnostic report. generate_diagnostic_report replaces the server IP address with ***MASKED*** for safe sharing of diagnostic output.
  • File permissions for vpn:// URIs. secure_files and restore_backup set chmod 600 for .vpnuri and .png (QR code) files.
  • Client name validation in set_client_expiry. Prevents path traversal through client names.
  • Quoted paths in cron file. install_expiry_cron properly quotes paths with spaces.

Reliability

  • TOCTOU fix in modify_client. Parameter validation moved before lock acquisition, client state checks moved inside the lock. File descriptor is properly closed on all error paths.
  • Correct service restart. Step 7 now detects an already-running service and uses enable + restart instead of a duplicate awg-quick up, preventing the "interface already exists" error.
  • I1 stale value fix. load_awg_params clears AWG_I1 before parsing the server config, preventing CPS parameter contamination from the initial configuration.
  • Forced regeneration with CLI flags. Re-running with --preset or --jc/--jmin/--jmax forces AWG parameter regeneration even when the config already exists.
  • ARM prebuilt step completion. The prebuilt .deb installation path now correctly updates state and requests a reboot, preventing an infinite step-2 loop.
  • Correct regex in release.yml. Dots are now escaped in the version pattern (5\.10\.0 instead of 5.10.0).
  • Extended preflight in build-arm-deb.sh. Added modinfo, sha256sum, awk, xz checks, kernel detection via /lib/modules/*/build, empty MODULE_VER guard.

CI/CD

  • Expanded ShellCheck scope. Workflow now lints scripts/*.sh and tests/*.bash in addition to root .sh files.
  • Test workflow hygiene. Added permissions: contents: read and concurrency group to prevent parallel runs.

Tests

  • +33 new bats tests (131 total, up from 98). test_preset.bats (18): preset selection, CLI overrides, validation. test_validate.bats (+8): protocol boundary checks. test_safe_load_config.bats (+4): CRLF, BOM, BOM+CRLF, values with =. test_validate_endpoint.bats (+3): full IPv6, single-label hostname, empty brackets.

📣 Main features of the 5.x branch — see the v5.8.0 release notes. ARM support — see v5.9.0. v5.10.0 adds mobile network optimization and a comprehensive audit with no breaking changes.


5.9.0 — 2026-04-15

Raspberry Pi (arm64 and armhf) and ARM64 server support (AWS Graviton, Oracle Ampere, Hetzner arm64). Full implementation by @pyr0ball (PR #43, Issue #37).

Added

  • Prebuilt kernel modules for ARM. A new GitHub Actions workflow (.github/workflows/arm-build.yml) builds amneziawg.ko for 6 ARM targets via QEMU on every v* tag push. Targets: rpi-bookworm-arm64 (Raspberry Pi 3/4), rpi5-bookworm-arm64 (Pi 5 / Cortex-A76), rpi-bookworm-armhf (Pi 3/4 32-bit), ubuntu-2404-arm64, ubuntu-2204-arm64, debian-bookworm-arm64. Built .deb plus .sha256 are published to a dedicated arm-packages release. Build script (scripts/build-arm-deb.sh) can also be run manually on ARM hardware outside CI.
  • Automatic install path selection on ARM. On aarch64/armv7l, step 2 first tries the prebuilt .deb from the arm-packages release (kernel vermagic must match exactly) and falls back to DKMS silently if it does not. Curl uses --max-time 60 to avoid hangs; SHA256 is verified before dpkg -i. Saves time and RAM on minimal systems without build tools.
  • Correct kernel headers detection for Raspberry Pi. RPi Foundation kernels (+rpt/-rpi suffix) now pick linux-headers-rpi-v8 or linux-headers-rpi-2712 instead of the non-existent linux-headers-arm64. amneziawg-tools (userspace) on ARM is already shipped by the PPA for arm64/armhf — no separate build needed.
  • Bats tests for header selection. tests/test_rpi_headers.bats — 6 cases: +rpt-rpi-v8 → rpi-v8, +rpt-rpi-2712 → rpi-2712, legacy -rpi-v8, mainline arm64 Debian, amd64, generic Ubuntu kernel.

Tests

  • x86_64 regression on a clean Ubuntu 24.04 LTS, kernel 6.8.0-110-generic: DKMS build, module load, awg show, manage add/list/backup, uninstall — all unchanged. The ARM path is skipped correctly on x86_64, _try_install_prebuilt_arm is not invoked.
  • ARM end-to-end on a Raspberry Pi 4 / Debian 12 / kernel 6.12.75+rpt-rpi-v8 (DKMS path, prebuilts not published at PR time): full install lifecycle, awg-quick@awg0 active, vermagic matches.

Out of scope for this release

  • OpenWrt — separate package ecosystem, needs the OpenWrt SDK
  • Auto-tracking kernel updates / broken-package detection
  • Armbian and other SBC vendor kernels (follow-up)

📣 Main release notes for the 5.x branch — see the v5.8.0 release notes. v5.9.0 is a minor bump adding ARM support with no breaking changes for existing x86_64 installs.


5.8.4 — 2026-04-13

Reliability and security hardening following a review of the installer and management script.

Security

  • Extended file type check in restore_backup. The verbose archive listing (tar -tvzf) now checks the type of each entry by its first character. Archives containing block devices (b), character devices (c), FIFOs (p), hardlinks (h), or symlinks (l) are rejected before extraction. The --no-same-permissions flag is also added to the extract step so file permissions are always derived from the process umask, never from archive metadata. Protects against crafted archives that bypassed the path checks introduced in v5.8.3.
  • IPv4 octet range validation in validate_endpoint. Previously the regex accepted 999.0.0.1 as a "valid" IPv4 address because [0-9]{1,3} did not check the numeric value. A second pass via BASH_REMATCH now verifies each octet is in the 0-255 range. validate_endpoint "256.0.0.1" and validate_endpoint "999.999.999.999" now correctly return 1.
  • restore_backup — abort on first copy error. All five critical cp -a operations (server/, clients/, keys/, server_private.key, server_public.key) are now explicitly checked. On failure both locks are released and the function returns 1 immediately with a message identifying which file failed. Prevents a half-restored configuration from being left in place.

Reliability

  • File locks in backup_configs and restore_backup. backup_configs() now acquires .awg_backup.lock (30 s timeout) before creating the archive. restore_backup() acquires .awg_backup.lock (outer) and .awg_config.lock (inner, 30 s) before extraction. Lock ordering is fixed (backup → config), deadlock is impossible. If manage backup and manage restore run concurrently the second process waits or exits with a clear diagnostic.
  • Self-deadlock prevention in restore_backup. Before this fix restore_backup() called backup_configs() for its safety snapshot — both tried to acquire .awg_backup.lock → deadlock. An internal _backup_configs_nolock() helper was extracted; restore_backup() calls it inside its own locked scope. backup_configs() (the public entry point) keeps its own lock acquisition.
  • UFW exit code checks in setup_improved_firewall. Every ufw command (default deny/allow, limit SSH, allow VPN port, route rule) on both branches (inactive and active) now checks the exit code. Accumulated errors cause return 1. Previously a single UFW rule failure did not abort firewall setup.
  • SHA256 bypass logged at WARN level. When starting with a custom AWG_BRANCH (used during branch testing) the SHA256 check is skipped. This was previously logged at log_debug, invisible in normal output. Now it logs at log_warn so developers can see that integrity was not verified.

Tests

  • +7 new bats tests. test_validate_endpoint.bats +4: reject 999.999.999.999, 256.1.1.1; accept 255.255.255.255, 0.0.0.0. test_restore_backup.bats +1: real archive + mock tar injecting a block device entry → type-check rejects (proper negative test with real archive creation). test_apply_config.bats +2: flock timeout returns 1; systemctl restart failure returns non-zero. Total: 92 bats tests, all PASS.

📣 The main release notes bundle for the 5.8.x branch lives in v5.8.0 release notes. v5.8.4 is a hardening patch on top of 5.8.3 with no breaking changes.


5.8.3 — 2026-04-11

A batch of hardening fixes and targeted improvements following Issue #42 and an internal audit.

Security

  • Downloaded script integrity check (SHA256). install_amneziawg.sh in step 5 now computes sha256sum for awg_common.sh and manage_amneziawg.sh right after curl and compares the result to hardcoded values updated at each release. On mismatch the installer aborts. Protects against tampering on an intermediate hop or a compromise of raw.githubusercontent.com. Verification is automatically skipped when AWG_BRANCH is overridden by the user for testing a custom branch.
  • Tar archive validation before extraction in restore_backup. Before extraction the script reads the file list via tar -tzf and rejects the archive if it contains absolute paths (/etc/...) or path traversal (..). After extraction it scans the unpacked tree for symlinks and rejects the archive if any are found. Plus tar -xzf --no-same-owner to guarantee extracted files are owned by root rather than by metadata inside the archive. Protects against crafted or tampered backups.

Fixed

  • Mobile internet — Yota/Tele2 blocked VPN (Issue #42). Reported by @markmokrenko: after a standard install the VPN fails to connect on Yota and Tele2, while Beeline works. Root cause: Jmin/Jmax values. This continues the Discussion #38 story — mobile carriers are sensitive to junk packet size. Lowered the Jmax offset from Jmin+100..500 to Jmin+50..250, the maximum junk packet size drops from ~590 to ~340 bytes. Obfuscation strength is preserved, mobile compatibility improves.

Tests

  • 4 new bats tests for restore_backup tar validation: happy path (good backup), absolute path rejection, path traversal rejection, server key chmod 600. Total: 85 bats tests, all PASS.

Live VPS tests

The release was validated on a clean Ubuntu 24.04 LTS: 13/13 checks passed. Tar validation was tested against three attack types — path traversal, absolute paths, symlinks. The SHA256 verify_sha256 function was tested with both correct and incorrect hash inputs. UFW routing cleanup during --uninstall was confirmed.

📣 The main release notes bundle for the 5.8.x branch lives in v5.8.0 release notes. v5.8.3 is a hotfix on top of 5.8.2 with security hardening and a narrower Jmax range for mobile networks.


5.8.2 — 2026-04-10

Fixed

  • Hoster VNC console breaks, network drops on Hetzner (Discussion #41): rp_filter lowered from 1 (strict) to 2 (loose). Strict mode broke routing on cloud hosters (Hetzner and similar) where the gateway is in a different subnet. Added kernel.printk = 3 4 1 3 to suppress kernel warning messages in the VNC console. Thanks @z036.
  • --uninstall now correctly removes UFW routing rules: added out on <nic> to the delete command — UFW requires an exact match with the rule that was created during install.
  • Default Jc lowered from 4-8 to 3-6 (Discussion #38): mobile networks (LTE/5G) do not tolerate large amounts of junk packets well. @elvaleto confirmed that Jc=3 works reliably on Tattelecom (Letai).

Documentation

  • ADVANCED.md/en FAQ: added 2 new entries — Jc/I1 recommendation for mobile networks and workaround for the VNC/Hetzner rp_filter issue. Parameter table updated: Jc range 4-8 → 3-6.

5.8.1 — 2026-04-09

Targeted hotfix on top of v5.8.0 following Discussion #40 from @z036: the randomized H1-H4 values from v5.8.0 could fall into the [2^31, 2^32-1] range, which the amneziawg-windows-client config editor underlines as invalid and refuses to save. The server (amneziawg-go) accepts the full uint32; the issue is purely in the client-side UI validator.

Fixed

  • H1-H4 Windows client compatibility (Discussion #40): generate_awg_h_ranges now caps the upper bound at 2^31-1 = 2147483647 instead of the full uint32. This matches isValidHField() in amnezia-vpn/amneziawg-windows-client#85 (upstream bug, open since February 2026, not yet fixed). Implementation: a 0x7FFFFFFF bit mask is applied to the od -N32 -tu4 /dev/urandom output, and the fallback path now uses rand_range 0 2147483647. No bias is introduced — each lower bit stays independent. Obfuscation strength is not weakened: four non-overlapping pairs in [0, 2^31) with a minimum width of 1000 each still give an astronomically large key space, DPI cannot fingerprint by default values. Thanks @z036 for the precise screenshot with the underlined fields.

Compatibility

  • Existing v5.8.0 installs continue to work on the server side. amneziawg-go accepts the full uint32, handshake with clients is not affected. The only inconvenience is that amneziawg-windows-client's config editor underlines H2-H4 in red if they happen to land in the upper half of the range (~99.6% of fresh v5.8.0 installs). The cross-platform amnezia-client (Qt, Android/iOS/Desktop) does not have this limit.
  • Upgrading from v5.8.0 is recommended if you use amneziawg-windows-client: run sudo bash /root/awg/install_amneziawg.sh --uninstall --yes, then install v5.8.1 fresh. New H1-H4 values will land in the safe half of the range.
  • Algorithm and config format are unchanged, only the generation space is narrower. No breaking changes for the server or existing client .conf files.

Tests

  • tests/test_h_ranges.bats updated: upper-bound check changed from 2^32-1 to 2^31-1, plus a new regression test running the generator 20 times × 8 values (160 samples) and asserting every value is ≤ 2147483647. Total: 81 bats tests (+1 from 5.8.0).

Documentation

  • ADVANCED.md/en FAQ: added an entry about the upstream amneziawg-windows-client bug with a root-cause explanation, links to upstream issue #85 and Discussion #40, and three workaround options for v5.8.0 users.

📣 The main release notes bundle for the 5.8.x branch lives in v5.8.0 release notes. That is where the full Discussion #38 (Russian DPI fingerprinting) context and the multi-round code-audit story lives. v5.8.1 is a hotfix on top of 5.8.0, recommended for everyone using the Windows client.


5.8.0 — 2026-04-07

Major security and reliability update after several consecutive code audits. The reason for a minor bump instead of a patch release is the significant volume of breaking-semantics changes in config handling, parameter source of truth, and error propagation.

Security

  • Russian DPI fingerprinting via static H1-H4 (Discussion #38): The H1-H4 ranges in generate_awg_params were hardcoded identically across all installs (100000-800000, 1000000-8000000, ...). Russian DPI fingerprinted this static signature — installs stopped working over Russian mobile carriers. H1-H4 are now randomized per install: 8 random uint32 values are sorted and grouped into 4 non-overlapping pairs. Every install gets unique ranges with no static signature. Thanks @Klavishnik (report) and @elvaleto (diagnosis).

  • Split-brain prevention in load_awg_params: When the live awg0.conf exists, it is now the SOLE source of truth for AWG protocol parameters. A partially corrupt live config (for example, a missing H4 field) produces an explicit error with return 1 instead of silently falling back to stale values from the init file. This closes a class of split-brain bugs where the server runs one config while regen would issue clients a different set of J*/S*/H*.

  • Atomic export in load_awg_params_from_server_conf: The parser no longer exports AWG_* variables as it finds each field. It now either reads all 11 required fields successfully and exports them, or the environment is not modified at all. Protects against mixed state when awg0.conf is partially corrupt.

  • restore_backup forces chmod 600 on restored server keys instead of inheriting the mode from the archive via cp -a. Protects against restoring keys with broken permissions if the backup was created with a bad umask.

  • --uninstall no longer disables UFW globally (HIGH severity, audit). Previously ufw --force disable wiped the entire firewall on a VPS where UFW was used for SSH/web hardening before our script was installed. The installer now writes a marker .ufw_enabled_by_installer only if UFW was inactive before installation, and uninstall disables UFW only when that marker is present. Backwards compat: older installs without the marker get safer-by-default — UFW stays active.

  • Process-wide install lock (audit). Two concurrent install_amneziawg.sh --yes runs could read the same setup_state, race each other on apt-get and corrupt package state. flock -n on $AWG_DIR/.install.lock is now taken at the start of main() for the entire process lifetime — a second instance gets die "Another installer is already running".

  • --endpoint validation (audit). Previously the value was accepted verbatim and written to init and client.conf without any sanity check. Newlines or quotes in the endpoint could smuggle extra directives into configs. A new validate_endpoint() function rejects newlines, CR, quotes, backslashes, and requires FQDN / IPv4 / [IPv6] format.

Fixed

  • regen did not update AWG parameters in client configs (#38): load_awg_params only read AWG parameters from the cached /root/awg/awgsetup_cfg.init, not from the live /etc/amnezia/amneziawg/awg0.conf. If a user manually edited awg0.conf (for example, to change obfuscation parameters), regen produced client configs with stale values. load_awg_params now reads the live server config first, with the init file used only as a bootstrap fallback on first install. Added new function load_awg_params_from_server_conf.

  • manage add/remove ignored apply_config exit code (audit). On apply_config failure the commands still logged "Configuration applied" and returned success — the user saw "OK" while the peer was applied only to the config file, not to the live interface. The caller now checks the return code, logs an actionable error pointing at systemctl status, and sets _cmd_rc=1.

  • check_expired_clients left peers on the live interface on apply failure (audit). If apply_config failed after expired peers were removed from state files, the peer vanished from expiry/ but remained active on the interface until a manual restart. Permanent stuck state. The function now checks the return code and returns 1 with an actionable message.

  • --uninstall removed /etc/fail2ban/jail.local by heuristic (audit). Previously the entire file was deleted if it contained banaction = ufw — too broad a filter, could wipe an unrelated jail.local with custom jails. The removal block has been dropped entirely, leaving only rm -f /etc/fail2ban/jail.d/amneziawg.conf (our own artefact).

  • check_server did not check awg show exit code (audit). Could report "State OK" even when awg itself crashed. The command is now captured and its exit code verified.

  • backup_configs/restore_backup leaked temp directories on SIGINT (audit). mktemp -d was used directly, while the _awg_cleanup trap only removed files. A new manage_mktempdir helper registers the dir in an array and chains cleanup properly.

  • add_peer_to_server now takes an inner flock to protect against direct calls outside generate_client (defense-in-depth, self-audit). The "caller must hold the lock" contract was fragile.

  • check_expired_clients validates the client name before using it in paths (defense-in-depth, self-audit). Previously name=$(basename "$efile") was used without validation.

  • Backup file names no longer contain colons: %F_%T → %F_%H-%M-%S. Colons are incompatible with FAT/NTFS when copying backups to another medium.

  • apply_config has an explicit return 0 on the success path — removes exit-code ambiguity from exec {fd}>&-.

Optimizations

  • generate_awg_h_ranges does a single /dev/urandom read instead of 8 rand_range subprocess calls. od -An -N32 -tu4 /dev/urandom reads 32 bytes = 8 uint32 values in one operation. Falls back to rand_range if /dev/urandom is unavailable.

Tests

  • 80 bats tests (+34 from the 5.7.12 baseline of 46 tests):
    • test_h_ranges.bats — 9 H1-H4 generation checks
    • test_load_awg_params.bats — 14 awg0.conf parser, init-file priority, split-brain prevention, atomic export, bootstrap path checks
    • test_validate_endpoint.bats — 14 validate_endpoint checks (valid FQDN/IPv4/IPv6, reject newline/CR/quotes/space/backslash/empty)
  • All 46 existing tests (apply_config, IP allocation, parse_duration, peer management, safe_load_config, validate) still pass without regressions.

Documentation

  • ADVANCED.md/en FAQ: added workflow "Rotating obfuscation parameters when DPI detects them" — how to edit awg0.conf + restart + regen, noting that as of 5.8.0 regen reads the live config.

5.7.12 — 2026-04-06

Fixed

  • Fail2Ban on Debian (Discussion #39): On Debian 12/13 rsyslog is not installed — fail2ban crashed without /var/log/auth.log. Added backend = systemd and python3-systemd package for Debian. Ubuntu continues using backend = auto.

5.7.11 — 2026-03-31

Fixed

  • regen corrupts Address on Debian/mawk (#31): \s in awk (PCRE extension) not supported by mawk. Replaced with [ \t]. Also replaced grep -oP with POSIX-compatible sed for private key extraction.
  • regen loses values after modify (#31): User settings (DNS, PersistentKeepalive, AllowedIPs) changed via modify are now preserved during config regeneration.
  • modify leaves .bak files (#31): Backup file is now deleted after successful parameter change.
  • check fails to detect port on Debian (#31): grep -qP replaced with POSIX-compatible grep in all 6 port-checking locations.

5.7.10 — 2026-03-31

Added

  • Batch remove clients (#30): manage remove client1 client2 client3 — remove multiple clients in one command with a single apply_config at the end.
  • AWG_SKIP_APPLY=1 (#30): Environment variable to skip apply_config entirely. Allows accumulating changes and applying once — for automation and API integrations. Correct "Apply deferred" message instead of "Configuration applied".
  • flock in apply_config (#30): Inter-process lock (${AWG_DIR}/.awg_apply.lock) prevents concurrent restart/syncconf calls.
  • Unit tests (bats-core): 43 tests for awg_common.sh — parse_duration, safe_load_config, IP allocation, peer management, apply_config modes, validate. CI workflow .github/workflows/test.yml.

5.7.9 — 2026-03-25

Added

  • Config apply mode (#30): New --apply-mode=restart option for manage_amneziawg.sh. Switches to full service restart instead of awg syncconf — bypasses upstream deadlock in amneziawg kernel module (amneziawg-linux-kernel-module#146). Persists via AWG_APPLY_MODE=restart in awgsetup_cfg.init.

5.7.8 — 2026-03-24

Added

  • Batch add clients (#29): manage add client1 client2 client3 ... — create multiple clients in one command. awg syncconf is called once at the end instead of N times. Prevents kernel panic during mass client creation (upstream bug amneziawg-linux-kernel-module#146).

5.7.7 — 2026-03-24

Fixed

  • Peer loss on reinstall: render_server_config overwrote awg0.conf from scratch. Existing [Peer] blocks are now automatically restored from backup when step 6 re-runs.
  • Race condition when adding clients (TOCTOU): get_next_client_ip and add_peer_to_server now execute in a single critical section (flock in generate_client). Two parallel add operations can no longer pick the same IP.
  • Silent restore success on failure: restore_backup now returns non-zero exit code when file copy errors occur, instead of silently reporting success.
  • Config parser double quote support: safe_load_config now correctly handles double-quoted values ("value") in addition to single quotes.

5.7.6 — 2026-03-24

Fixed

  • UFW blocks VPN traffic (Discussion #28): Added ufw route allow in on awg0 out on <nic> rule during firewall setup. Previously, the default deny (routed) policy blocked forwarded packets from awg0 to the main interface, despite PostUp iptables rules. The rule is automatically removed on uninstall.
  • PostUp FORWARD ordering: Changed iptables -A FORWARD to iptables -I FORWARD to insert the rule at the top of the chain. Ensures correct routing when UFW is absent (--no-tweaks).

5.7.5 — 2026-03-20

Fixed

  • Trailing newlines in awg0.conf (#27): Multiple blank lines accumulated in the server config after peer removals. Added normalization via cat -s on each remove.
  • Timeout for awg syncconf (#27): awg-quick strip and awg syncconf are now called with timeout 10. On hang (upstream deadlock amneziawg-linux-kernel-module#146), the script falls back to a full service restart instead of waiting indefinitely.

5.7.4 — 2026-03-20

Fixed

  • MTU 1280 by default (Closes #26): Server and client configs now include MTU = 1280. Fixes smartphone connectivity over cellular networks and on iPhone.
  • Jmax cap: Maximum junk packet size capped at Jmin+500 (was Jmin+999). Prevents fragmentation with MTU 1280.
  • validate_subnet: Last subnet octet must be 1 (server address). Previously allowed arbitrary values, causing conflicts with get_next_client_ip.
  • awg show dump parsing: Interface line skipped via tail -n +2 instead of unreliable empty psk field check.
  • manage help without AWG: help and empty command show usage before check_dependencies, allowing --help without AWG installed.
  • help text: Installer help now lists all 4 supported OS (Ubuntu 24.04/25.10, Debian 12/13).
  • manage --expires help: Added 4w format to --expires help text (already supported by parser, but missing from help).

Improved

  • IP caching: get_server_public_ip() caches the result — repeated calls (add/regen) skip external service requests.
  • O(N) IP lookup: get_next_client_ip() uses an associative array for free IP lookup instead of O(N²) nested loops.

Documentation

  • Fixed client compatibility table: amneziawg-windows-client >= 2.0.0 supports AWG 2.0 (previously incorrectly listed as AWG 1.x only).
  • Fixed APT format for Ubuntu 24.04: DEB822 .sources (was .list).
  • Fixed restore example in migration FAQ: correct path /root/awg/backups/.
  • Fixed uninstall reference in EN README FAQ: install_amneziawg_en.sh.
  • Added Ubuntu 25.10 to the "Which hosting?" FAQ answer.
  • Updated config examples: added MTU = 1280.
  • Updated Jmax range in parameters table: +500 instead of +999.
  • Rewrote MTU section: automatic for v5.7.4+, manual workaround for older versions.
  • Removed "MTU not set" from Known Limitations.
  • Updated "How to change MTU?" FAQ for automatic MTU.

5.7.3 — 2026-03-18

Fixed

  • Uninstall SSH lockout: UFW is now disabled BEFORE fail2ban unban — prevents SSH lockout if the connection drops during uninstall.
  • CIDR validation (strict): Invalid CIDR in --route-custom now calls die() in CLI mode. In interactive mode — retry prompt. Previously, installation continued with invalid AllowedIPs.
  • validate_subnet .0/.255: Subnets with last octet 0 (network address) or 255 (broadcast) are now rejected.
  • ALLOWED_IPS resume: Custom CIDR values (mode=3) are now validated when resuming installation from saved config.
  • modify sed mismatch: Synchronized sed pattern with grep in modify_client() — handles .conf files with any whitespace formatting around =. Added post-replacement verification.
  • --no-color ANSI leak: Fixed ESC code \033[0m leaking into list --no-color output.
  • Uninstall wildcard cleanup: Removed meaningless wildcard patterns from uninstall — *amneziawg* files in /etc/cron.d/ and /usr/local/bin/ were never created.

Documentation

  • Added AmneziaWG for Windows 2.0.0 as a supported client.
  • Removed misleading note about curl requirement on Debian.

5.7.2 — 2026-03-16

Security

  • safe_load_config(): Replaced source with a whitelist config parser in awg_common.sh — only permitted keys (AWG_, OS_, DISABLE_IPV6, etc.) are loaded from the file. Eliminates potential code injection via awgsetup_cfg.init.
  • Supply chain pinning: Script download URLs are pinned to the version tag (AWG_BRANCH=v${SCRIPT_VERSION}) instead of main. The AWG_BRANCH variable can be overridden for development.
  • HTTPS for IP detection: get_server_public_ip() uses HTTPS instead of HTTP for external IP detection.

Fixed

  • modify allowlist: Removed Address and MTU from allowed modify parameters — these are managed by the installer and should not be changed manually.
  • flock for add/remove peer: Peer addition and removal operations are protected with flock -x to prevent race conditions during parallel invocations.
  • cron expiry env: Expiry cron job explicitly sets PATH and uses --conf-dir for correct operation in minimal cron environments.
  • log_warn for malformed expiry: Malformed expiry files are handled via log_warn instead of being silently skipped.
  • Dead code: Removed unused functions and variables from awg_common.sh.

Changed

  • list_clients O(N): Optimized list_clients — single-pass algorithm instead of O(N*M).
  • backup/restore: Backups now include client expiry data (expiry/) and cron job.
  • Version: 5.7.1 → 5.7.2 across all scripts.

5.7.1 — 2026-03-13

Fixed

  • vpn:// URI AllowedIPs: generate_vpn_uri() was hardcoding 0.0.0.0/0 instead of using actual AllowedIPs from client config — split-tunnel configurations are now correctly passed to the URI.
  • Fail2Ban jail.d: Installation now writes to /etc/fail2ban/jail.d/amneziawg.conf instead of overwriting jail.local — user Fail2Ban customizations are preserved.
  • Fail2Ban uninstall: Uninstall now removes only its own artifacts instead of rm -rf /etc/fail2ban/.
  • validate_client_name: Client name validation added to remove and modify commands — previously only worked for add and regen.
  • exit code: Management script now returns proper error codes instead of unconditional exit 0.
  • expiry cron path: Expiry cron job uses $AWG_DIR instead of hardcoded /root/awg/.

Removed

  • rand_range(): Removed unused function from awg_common.sh (installer defines its own copy).

5.7.0 — 2026-03-13

Added

  • syncconf: add and remove commands now auto-apply changes via awg syncconf — zero-downtime, no active connection drops (#19).
  • apply_config(): New function in awg_common.sh — applies config via awg syncconf with fallback to full restart.
  • --no-tweaks: Installer flag — skips hardening (UFW, Fail2Ban, sysctl tweaks, cleanup) for advanced users with pre-configured servers (#21).
  • setup_minimal_sysctl(): Minimal sysctl configuration for --no-tweaks — only ip_forward and IPv6 settings.

Fixed

  • trap conflict: Fixed EXIT handler being overwritten when sourcing awg_common.sh. Each script now owns its trap and chains the library cleanup explicitly.

Changed

  • Expiry cleanup: Auto-removal of expired clients now uses syncconf instead of full restart.
  • Manage help: Removed manual restart warning after add/remove (no longer required).
  • Version: 5.6.0 → 5.7.0 across all scripts.

5.6.0 — 2026-03-13

Added

  • stats: stats command — per-client traffic statistics (format_bytes via awk).
  • stats --json: Machine-readable JSON output for integration and monitoring.
  • --expires: --expires=DURATION flag for add — time-limited clients (1h, 12h, 1d, 7d, 30d, 4w).
  • Expiry system: Auto-removal of expired clients via cron (/etc/cron.d/awg-expiry, checks every 5 min).
  • vpn:// URI: Generation of .vpnuri files for one-tap import into Amnezia Client (zlib compression via Perl).
  • Debian 12 (bookworm): Full support — PPA via codename mapping to focal.
  • Debian 13 (trixie): Full support — PPA via codename mapping to noble, DEB822 format.
  • linux-headers fallback: Auto-fallback to linux-headers-$(dpkg --print-architecture) on Debian.

Fixed

  • JSON sanitization: Safe serialization in JSON output.
  • Numeric quoting: AWG numeric parameters properly quoted.
  • O(n) stats: Single-pass stats collection instead of multiple calls.
  • backup filename: %F_%T → %F_%H%M%S (removed colons from filename).
  • cron auto-remove: Cron cleanup when the last expiry client is removed.
  • backups perms: chmod 700 after mkdir for the backups directory.
  • apt sources location: Apt sources backup moved to $AWG_DIR instead of sources.list.d.
  • Multiple minor fixes from code review (19 fixes).

Changed

  • Debian-aware installer: OS_ID detection, adaptive behavior (cleanup, PPA, headers).
  • Version: 5.5.1 → 5.6.0 across all scripts.

5.5.1 — 2026-03-05

Fixed

  • read -r: Added -r flag to all read -p calls (15 places) — prevents \ from being interpreted as an escape character in user input.
  • curl timeout: Added --max-time 60 --retry 2 to script downloads during installation — prevents indefinite hanging on network issues.
  • subnet validation: Subnet validation now checks each octet ≤ 255 — previously accepted addresses like 999.999.999.999/24.
  • chmod checks: Added error checking for chmod 600 when setting permissions on key files.
  • pipe subshell: Fixed variable loss in config regeneration loop due to pipe subshell — replaced with here-string.
  • port grep: Improved port matching precision in ss -lunp — replaced grep ":PORT " with grep -P ":PORT\s" to avoid false matches.
  • sed → bash: Replaced sed 's/%/%%/g' with ${msg//%/%%} — removed 2 unnecessary subprocesses per log call.
  • cleanup trap: Added trap EXIT for automatic cleanup of installer temp files.

5.5 — 2026-03-02

Fixed

  • uninstall: Uninstall proceeded without confirmation when /dev/tty was unavailable (pipe, cron, non-TTY SSH) due to default confirm="yes".
  • uninstall: Kernel module amneziawg remained loaded after uninstall — added modprobe -r.
  • uninstall: Working directory /root/awg/ was recreated by logging after deletion — moved cleanup to the end.
  • uninstall: Empty /etc/fail2ban/ and PPA backup .bak-* files remained after uninstall.
  • --no-color: Reset escape code \033[0m was not suppressed with --no-color — fixed color_end initialization.
  • step99: Duplicate "Cleaning apt…" message — removed extra log call before cleanup_apt().
  • step99: Lock file setup_state.lock was not removed after installation completed.
  • manage: Inconsistent spelling "удален"/"удалён" — standardized (RU only).

5.4 — 2026-03-02

Fixed

  • step5: manage_amneziawg.sh download failure is now fatal (die), consistent with awg_common.sh.
  • update_state(): die() inside flock subshell did not terminate the main process — moved outside.
  • step6: Server config backup now created before render_server_config, not after overwrite.
  • cloud-init: Conservative detection — cloud-init markers checked first to avoid removing it on cloud hosts.
  • restore_backup(): Added non-interactive guard (explicit file path required in automation).
  • Subnet: Validation now only allows /24 mask (matches actual IP allocation logic).
  • Version: Removed stale v5.1 references in logs/diagnostics; introduced SCRIPT_VERSION constant.

5.3 — 2026-03-02

Added

  • English scripts: Full English versions of all three scripts (install_amneziawg_en.sh, manage_amneziawg_en.sh, awg_common_en.sh) with translated messages, help text, and comments.
  • CI: ShellCheck and bash -n checks for English scripts.
  • PR template: Checklist item for EN/RU version synchronization.
  • CONTRIBUTING.md: Requirement to synchronize EN/RU when modifying scripts.

5.2 — 2026-03-02

Fixed

  • check_server(): Fixed inverted exit code (return 1 on success → return 0).
  • Diagnostics restart/restore: systemctl status output is now correctly captured in the log.
  • restore_backup(): Server config restoration path now uses $SERVER_CONF_FILE.

Changed

  • awg_mktemp(): Activated automatic temp file cleanup via trap EXIT.
  • modify: Added an allowlist of permitted parameters (DNS, Endpoint, AllowedIPs, Address, PersistentKeepalive, MTU). (Address and MTU removed in v5.7.2)
  • Documentation: Removed incorrect mention of /16 subnet support.
  • Removed dead trap code from install_amneziawg.sh.

5.1 — 2026-03-01

Fixed

  • CRITICAL: Command injection via special characters #, &, /, \ in modify_client() — added escape_sed() function for escaping.
  • CRITICAL: Race condition in update_state() — added locking via flock -x.
  • MEDIUM: curl in get_server_public_ip() could receive HTML instead of IP — added -f flag (fail on error) and whitespace cleanup.
  • MEDIUM: $RANDOM fallback in rand_range() gave max 32767 instead of uint32 — replaced with (RANDOM<<15|RANDOM) for 30-bit range.
  • MEDIUM: Pipe subshell in check_server() — replaced with process substitution < <(...).
  • MEDIUM: Awk script in remove_peer_from_server() didn't handle non-standard sections — added handling for any [...] blocks.

Added

  • CI: GitHub Actions workflow — ShellCheck + bash -n on push/PR to main.
  • GitHub: Issue templates (bug report, feature request) in YAML form format.
  • GitHub: PR template with checklist (bash -n, shellcheck, VPS test, changelog).
  • SECURITY.md: Security policy, responsible vulnerability disclosure.
  • CONTRIBUTING.md: Contributor guide with code and testing requirements.
  • .editorconfig: Unified formatting settings (UTF-8, LF, indentation).
  • Trap cleanup: Automatic temp file cleanup via trap EXIT + awg_mktemp().
  • Bash version check: Bash >= 4.0 check at the start of install and manage scripts.
  • Documentation: Config examples, Mermaid architecture diagram, extended FAQ, troubleshooting.

Changed

  • Version: 5.0 → 5.1 across all scripts and documentation.
  • README.md: Command table expanded to 10 (+ modify, backup, restore), FAQ expanded to 8 questions.
  • ADVANCED.md: Added config examples, manage command examples, diagnostics description, update instructions.

5.0 — 2026-03-01

⚠️ Breaking Changes

  • AWG 2.0 protocol is not compatible with AWG 1.x. All clients must update their configuration.
  • Requires Amnezia VPN >= 4.8.12.7 client with AWG 2.0 support.
  • Previous version is available in the legacy/v4 branch.

Added

  • AWG 2.0: Full protocol support — parameters H1-H4 (ranges), S1-S4, CPS (I1).
  • Native generation: All keys and configs generated using Bash + awg with no external dependencies.
  • awg_common.sh: Shared function library for install and manage scripts.
  • Server cleanup: Automatic removal of unnecessary packages (snapd, modemmanager, networkd-dispatcher, unattended-upgrades, etc.).
  • Hardware-aware optimization: Automatic swap, network buffer, and sysctl tuning based on server characteristics (RAM, CPU, NIC).
  • NIC optimization: GRO/GSO/TSO offload disabling for stable VPN tunnel operation.
  • Extended sysctl hardening: Adaptive network buffers, conntrack, additional protection.
  • Individual client regeneration: regen <name> command for regenerating a single client's configs.
  • AWG 2.0 validation: Verification of all protocol parameters in the server config.
  • AWG 2.0 diagnostics: check command shows AWG 2.0 parameter status.

Removed

  • Python/venv/awgcfg.py: Python dependency and external config generator completely removed.
  • awgcfg.py bug workaround: Moving awgsetup_cfg.init during generation is no longer necessary.
  • Parameters j1-j3, itime: Legacy AWG 1.x parameters are no longer supported.

Changed

  • Architecture: 2 files → 3 files (install + manage + awg_common.sh).
  • Install step 1: Added system cleanup and optimization.
  • Install step 2: Installs qrencode instead of Python.
  • Install step 5: Downloads awg_common.sh + manage (no Python/venv).
  • Install step 6: Fully native config generation.
  • Key generation: Native via awg genkey / awg pubkey.
  • QR codes: Generated via qrencode directly (no Python).
  • Documentation: README.md and ADVANCED.md updated for AWG 2.0.

4.0 — 2025-07-15

Added

  • AWG 1.x support (Jc, Jmin, Jmax, S1, S2, H1-H4 fixed values).
  • DKMS installation.
  • Config generation via Python + awgcfg.py.
  • Client management: add, remove, list, regen, modify, backup, restore.
  • UFW firewall, Fail2Ban, sysctl hardening.
  • Resume-after-reboot support.
  • Diagnostic report (--diagnostic).
  • Full uninstall (--uninstall).