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.
287 KiB
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 lineexport CLIENT_DNS='10.9.9.1'(IPs separated by commas) gives that DNS to clients created after the edit and to a client whose configregenrebuilds 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, whileregenof clients already handed out works as before. An empty value means1.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 andmanageinstead 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-prebuiltflag. On ARM the installer skips the prebuilt module package from thearm-packagesrelease 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-dkmsfrom 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. ANO_PREBUILTother than 0 or 1 stops the install too. The choice is saved inawgsetup_cfg.initand 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> DNSno longer expands globs. The DNS list check parsed the value in an unquoted loop, so1.1.1.*passed whenever a file named1.1.1.1sat in the current directory. The check is now shared withCLIENT_DNSand lives inawg_common.sh.--uninstallno longer runsapt-get autoremove. It removes everything apt considers unneeded across the whole system, not just what the installer left; at install time in #84 it tooknetplan-generatorthat 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-v8orrpi-2712. Previously everything but a Pi 5 gotrpi-v8, which the 32-bit Raspberry Pi OS repository does not carry. --diagnosticreports the loaded module separately from the file on disk. The Module Info section now has two parts: the version andsrcversionof the loaded module from/sys/module/amneziawg, andmodinfofor the file on disk. The report compares theirsrcversion: a mismatch usually means the module was updated but not reloaded. "No file", "no permission" and "empty" are now told apart.--ssh-portwithout a valid port stops the install. A value such as2222/tcpor222222used to become 22 silently, and with--yesUFW 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
restartapply mode works everywhere it is promised.--apply-mode=restartandAWG_APPLY_MODE=restartin the environment are no longer overridden by the value inawgsetup_cfg.initduringadd, and a mode saved in the init file is now honoured byremoveand by the cron removal of expired clients as well. The order is: flag or environment, then the init file, thensyncconf. An unknown value (such asRestart) still givessyncconf, 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 -qunderpipefail, 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 checkno longer says "port is not listening" about a listening port. Same cause:| grep -qunderpipefailon a long socket list.manage diagnoseno longer hangs on anI1with 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=inaddis 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
restoreor a newaddwith the same name, a client could inherit the deadline of the previous one and be deleted by cron at that deadline.addandrestorenow drop such a stamp; if it cannot be removed,addrefuses that client andrestorerolls back. Stamps from the archive are restored as before, andrestorenow checks that they actually landed, rolling back otherwise. An archive without anexpiry/directory had no deadlines in it, so clients restored from it lose their stamps. Ifrestorerolls back, the rollback also takes away stamps that came from the archive, so a client that was permanent beforerestorestays permanent. IPV6_SUBNETcan no longer land in the IPv6 sink prefix. The routing mode 2 sink lives infddd:2c4:2c4:ffff::/64and is recognised by that prefix. With the tunnel subnet set inside it, a client's real IPv6 address was taken for a sink:regenandmodifycould drop it, andvpn://andlistdid 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--uninstalland an install with another ULA subnet.- The English
managewarning 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 ofI1decides the handshake, whileJc,JminandJmaxdo not affect it. The manual edit step no longer swaps a DNS-shapedI1for 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-mssinPostUpandPostDownmust 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--forcerecomputes the MSS from the new MTU only without the--preset,--jc,--jmin,--jmaxflags: 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'sAllowedIPswith the list-based routing modes, port 53 closed onawg0(and open to the whole internet when the resolver runs in Docker), andCRLFin files copied over from Windows (discussion #237). - The README first screen describes the install more accurately. The installer creates two clients,
my_phoneandmy_laptop, and saves their configs, QR codes andvpn://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 thevpn://link;name.pngis the QR of the.confitself, for AmneziaWG clients. The README and the English install guide used to point toname.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,--jminand--jmaxthese are--mobileand--port: they change the obfuscation parameters or the port.--endpointis 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 -Pwith the full public key, and3E598A1C01907E17is 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.shandawg_common.shinto a temporary folder, verifies each file withminisign -Pand 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
--uninstallleaves. README lists the main changes the way the code makes them now, including what--yesand--no-tweaksdo. 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.gzarchive 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 stopsawg0and deletes/etc/amneziaand/root/awgas 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,
scpfrom your own computer with-Pfor 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,ListenPortandAWG_ENDPOINT.regentakes the address fromAWG_ENDPOINT. The "Amnezia list" mode is no longer called split routing. The Russian install guide describes the switch tonobleas conditional. The English install guide givesI1 = <r 48>for Tele2 (Krasnoyarsk). For a leaked config the guides adviseremoveandadd:regenkeeps 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_VERSIONamong the version places, the commit hygiene check. wgcfis checked in the WARP guide. The binary is downloaded under its release name, checked againstchecksums.txtfrom the same release and run only after that check passes; awgcfleft 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
nobleonly if it does not. Debian uses a fixed mapping: 12 ->focal, 13 ->noble. - Samples match what the code produces. The
statsoutput is rendered with the same functions as inmanage; the diagnostic report table lists all 17 sections; the client configs in ADVANCED and CASCADE show two DNS servers and0.0.0.0/0, ::/0; the log lines for a prebuilt ARM module and the DKMS fallback, the Fail2Banbackend, the MTU invpn://and the fact thatmanagederivesserver_public.keyitself 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 --carrierprofile names are listed;tmobile_usis a historical name for the Russian T-Mobile (Moscow and region). - Smaller fixes. The
add guestcommand 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_BRANCHgoes aftersudo, cascade step 1 warns about the two reboots,--forcedoes not keep the WARPPostUpline, the threat model link points todocs/SIGNING_DESIGN.md, SECURITY names both language versions of the scripts and the release pipeline, andsigning/README.mdusesscripts/sign-release.shand 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
::/0appended so the device's IPv6 would not go around the tunnel. But with::/0the 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 gets2000::/3(all global IPv6), and the client'sAddressa service address likefddd: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 plainmanage regen, then re-imported on the devices;::/0is replaced only on a list matching the server's current list, clients issued with--allow-ipv6-tunnelare 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 to2000::/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 fingerprint75C9DD72C799870E310542E24166F2C257290828stays: 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.1is 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,--verboseincluded, 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,showanddiagnoseshow the protocol generation of the installation, andcheck --jsonreturns it in theprotocolfield. The generation comes from theAWG_PROTOCOLmarker, 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":checktreats it as a problem (ok=false,protocol_error: "unreadable"),showanddiagnosesay the marker cannot be read and exit with an error.diagnosereportsJmingreater thanJmaxon 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 ofawg0.conf. A missingJmaxwithJminset counts as the same error.
Changed
- Removing
RandomTrailersorDisableCookiesfromawg0.confgives the same warning as the other interface parameters.awg syncconfdoes not clear them, so the script points at a service restart.
Fixed
- The header protection key no longer reaches the
manage showandmanage checkoutput or the installer diagnostic report whenWG_COLOR_MODE=alwaysis set. With that variableawg showcolours its labels even when the output is not a terminal, and the secrets filter did not recognise theheader protection keyline. The scripts now run the tools withWG_COLOR_MODE=never. Under the same variable the step 7 service check of the installer failed falsely,checkreported the obfuscation parameters as not detected, anddiagnosefound nojc,jminandjmax; that is fixed too. - The service status text in
manage check, and on a failedrestoreorrestart, no longer shows keys fromamneziawg-toolsmessages.systemctl statusends with the unit's journal lines, and with a malformed key inawg0.conf(a character lost in a hand edit,PrivateKeyinside[Peer]) those carried theamneziawg-toolsmessage with the key value:Line unrecognized: ...orKey is not the correct length or format: .... This text used to go to the screen, to themanagelog 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,checkhides the status text and exits with an error, whilerestoreandrestartsay the status is hidden and name the command to see it. With--jsonthis text still goes to stderr, and in the plain modesystemctl's own warnings stay on stderr too. - The secrets filter catches a key where a hand edit broke the line.
amneziawg-toolsstrip 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 aLine unrecognized: ...message with the key still visible. Where the line still reads asName = valuefor a known parameter (for exampleRandomTrailersoutside 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 exampleUnable to parse Jc: ...when a key landed in the wrong field, and inUnable to parse IP address: ...a value that cannot be an address is hidden: this is howamneziawg-toolsanswer a key pasted intoAllowedIPs. Addresses, endpoints, ports and numbers in these messages stay visible. This applies to the installer's diagnostic report,manage check,show,diagnoseand 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.
restoreruns it before starting the service and rolls back on a refusal, and the installer runs it too, as a warning. WhenHeaderProtectionKeyis set in[Interface], the check acceptsH1-H4as a single number, requiresS1-S4to be at least 12, as the kernel module andamneziawg-gorequire, and refuses a repeated key, a key of the wrong form and a key outside[Interface]. With such a keyJc,Jmin,Jmax,S1-S4andH1-H4are read the wayamneziawg-toolsread them: section and key case-insensitive, the last value applies, and a value above 4294967295 is refused before any comparison.ContentPaddingAdditionis checked in any config: a value above 65535 is now refused, becauseamneziawg-toolssilently wrap it modulo 65536, and a reversed range is refused before the service starts.H1-H4are now read in base 10, asamneziawg-toolsread 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 withoutHeaderProtectionKeyandContentPaddingAdditionand with ordinaryH1-H4ranges are checked as before. -
add,regen,modify,vpn://issuing and a repeated installer run no longer produce broken profiles whenHeaderProtectionKeyis 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 whoseawg0.confhad the key added by hand, these commands silently issued client profiles without the key. They now refuse and name the reason: theAWG_PROTOCOLmarker inawgsetup_cfg.initand the key in the config contradict each other.modifychecks 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 inmanagecommands. Otherwise servers withoutHeaderProtectionKeywork 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 holdthe installer compared the result with theapt-mark showholdlist piped intogrep, and on some systems it refused with the hold in place: in the report from the issue dpkg showedhold ok not-installedand a manualapt-mark showholdlisted the package. One mechanism for such a refusal reproduces:apt-markwrites the list line by line,grep -qquits at the first match, and if more held packages follow in the list the pipeline can count as failed underpipefail. 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 theapt-mark showholdlist as a fallback, without a pipe. If neither shows the hold, the install still stops, because without itamneziawg-toolscan pull the AmneziaWG 3.0 module from the PPA, bypassing the chosen path. Such a refusal now prints whatapt-mark holdanswered and the package status in dpkg, not only a general hint to check the apt lock. manage checkandmanage showno longer print the header protection key.awg showprints it in clear text, andcheckcarried 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,checkhides the output,showreports that the output was not shown in full, and both commands exit with an error. Indiagnosethe first error line ofawg showgoes through the same filter.- A hung
awg showis now named as a timeout. Incheckandshowthe 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
--verboseand ofmanageunderbash -xno longer shows keys. It used to show server and client private keys,PresharedKeyvalues of carried-over peers, the header protection key andvpn://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 tomanagerun underbash -x(list,stats,check,diagnose,modify,add,regen,removeand the service status printed whenrestoreorrestartfails), 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.
ADVANCEDgains a section "Device IPv6,::/0and the local network". It says plainly:--disallow-ipv6andDISABLE_IPV6turn IPv6 off on the server, while whether a client gets::/0is 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 inADVANCEDnames the exact condition: without--allow-ipv6-tunnel, your own network list gets::/0when it covers all public IPv4. It also says that with--allow-ipv6-tunnelon 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-I5are now checked before profiles and the server config are issued.amneziawg-goaccepts<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 (upstreamamneziawg-linux-kernel-module#233). The installer itself never generates such a value, but it could reachawg0.confthrough a manual edit or someone else's config, andregenwould hand it to every client. Issuing profiles and the server config with such a value now stops with a clear error,modifydoes not reissue a profile whose.confalready carries such a value, andrestorerefuses 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;restorealso checks everyI1-I5line regardless of key and section case, asamneziawg-toolsread them, andmodifychecks the client.confthe same way. A trailing comment is now also dropped whenawg0.confis read, so a# ...tail no longer reaches client profiles andvpn://. Unknown tags and other malformed input are still rejected by the implementations themselves. The boundary: the check guards issuing profiles, generating the server config andrestore. Paths that only start or reload the service apply theawg0.confalready in place without it, for examplesystemctl restart,manage restart,repair-module, the start at boot and the apply afterremoveor after expired clients are removed. - After
modify, QR andvpn://files match the edited.confor are absent. After editing a client.conf, the command rebuildsNAME.png,NAME.vpnuriandNAME.vpnuri.png. When one of them could not be rebuilt, for example withoutqrencodeor theperlmodules, 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 theI1-I5check in this same release, a server with a refused value and a clean client.confbecame one more such case: the.confedit goes through, butvpn://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; amodifywhose own value check fails (for example an invalid DNS) leaves the files alone. The--jsonreply ofmodifygains theqrandvpnurifields, as inadd: a path when the file exists,nullwhen it does not. If a previous file cannot be removed,modifyfails and leaves the.confunchanged; the files removed before it are rebuilt by repeating themodifyonce 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, ::/0instead 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-amneziaflag. Why. The Amnezia app opens its own split-tunneling page only when it sees0.0.0.0/0among the client routes; the form it reliably recognises is the pair0.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-quickengages its fwmark machinery only for a/0route, 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 emits0.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 inawgsetup_cfg.init, and client profiles do not change by themselves. To switch the mode on a running server: reinstall with the flag you want, thenmanage 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_MODEvalue 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-routeshint 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.1flag. 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,awgtools 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
I1concealment packet now has a meaningful shape. The generator emittedI1 = <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
I1of 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 withoutI1at all (the--no-cpsflag), see no message. - The
I1generator no longer emits a half-built packet silently. The function had no failure signal at all: its exit status is that of the lastprintf, 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 diagnosecompares the installedI1against 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 structuredI1is 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 nowaddonly took--expiresand--psk, and a new client always received the server-wide routing mode - custom routes could only be patched in afterwards withmodify <name> AllowedIPs <value>. JSON-interface consumers (bots, scripts, panels) had to run two calls -add, thenmodify- deliver files only after themodify, 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,addwarns right away - by the same conditionregenuses (one shared predicate). The value follows the same rules as the global mode: a full-tunnel IPv4 list gets::/0added 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 theCLIENT_ALLOWED_IPSenv contract ingenerate_client/render_client_config, modeled afterCLIENT_PSK;regenclears it and per-client routes of existing clients are unaffected. The list validation moved out of themodifydispatcher into the shared helperawg_validate_allowed_ips_list, called from both places - copies of such checks have drifted apart before.createdentries inadd --jsonresults[]gained anallowed_ipsfield - the applied routes from the created.conf(with the::/0mirroring); for a bot this removes the verification call after creation. - Protocol generation marker
AWG_PROTOCOLinawgsetup_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-cpsor 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--forceon top of a working server stops earlier and does not touch the file. Behaviour does not change: today the value is always2.0, and a file without the field also reads as2.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.restorewarns 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 --jsonnow reports client expiry (expires_at). The field is present in every record and mirrors the semantics of theaddsuccess record: unix time as a number for a client created with--expires,nullfor a permanent one. Previously onlyaddreturned it inresults[], 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-vflag 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_errorflag:nullin 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: nullwould 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" asnull.
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
01750000000became262144000, 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 withvalue 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 --jsonandadd --jsonnow 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.
listread 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 fromadd, but the server config is also edited by hand.
Documentation
ADVANCED: a warning aboutRandomTrailers. 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-mimickingI1needs 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
AllowedIPsstring with0.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::/0just like "All traffic" does. - Re-issuing a profile now delivers the fix to already issued clients. The same fork sat on the
regenpath, which is why "update your profile" did not help before - the re-issued config still had no::/0. To roll the fix out, runsudo bash /root/awg/manage_amneziawg.sh regenand re-import the profiles on the devices. IndividualAllowedIPsset throughmodifyare still preserved. Clients issued with--allow-ipv6-tunnelwill not receive::/0from a plain re-issue: their IPv6 part is kept as an individual setting, and a warning now says so and namesregen --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. modifysays so when it drops::/0. The command still writes exactly what it was asked for: dropping::/0through 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 likemodify <name> AllowedIPs "$ALLOWED_IPS": the list inawgsetup_cfg.initis 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-tunnelon 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 (::/0is not doubled), a multi-line list, a config withCRLF, 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.mdandREADME.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 byscripts/update-facts-block.shfrom repository data (SCRIPT_VERSION, the CHANGELOG heading,docs/support-matrix.json, thearm-build.ymlmatrix) 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.mdandADVANCED.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.diagnosecomputes 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 inREADME.mdandREADME.en.md, two in each installation guide and eight more inADVANCED.mdandADVANCED.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 outv5.14.1from 19 May, thirty-seven releases back, with neither the critical package protection nor the/etc/fstabfix. The version-less address does not age in a cache or in training data, and it serves a signed release asset:.minisigfiles andKEYS.txtsit beside it, which a branch address cannot offer. Four pinned addresses stay on purpose: theADVANCEDsection 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;CASCADEpinscascade/ru.zone, which is not a release asset at all. The guard incheck-docs-consistency.shis 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 situationlistleaves clients at "No data", andno_dataunder--json, rather than declaring everyone without a handshake - Every interface call is bounded in time.
check,stats,show,list, the diagnostics and the--diagnosticreport 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
ContentPaddingAdditionwas 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:HeaderProtectionKeyis 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 howset -ebehaves, 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
.minisigfiles andKEYS.txtwith 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 unnoticedscripts/sign-release.sh, signing a release in one command. The trusted comment has to read exactlyamneziawg-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 runningverify-signatures.sh, so "signed 6" never rests onminisignmerely 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-resolvedon a hunch. On Debian with a static resolver or withresolvconfthat 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
mainafter 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.shnow 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-upgradeSUCCEEDED 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-upgradeis by definition free to choose removal when that makes dependency resolution easier. Step 1 now runsapt-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-pkgsflag keeps the only reasonfull-upgradewas needed here: a new kernel arrives as a package with a new name (linux-image-6.8.0-NNN-generic), and a plainupgraderefuses 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 wassystemd-resolved, pinningsystemdto 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
fusercheck 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, becausedpkg --configure -aand 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 undertimeout. 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 becausetimeoutkilled 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-sand droppingtimeoutall 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: withfull-upgradethis outcome was rare, withupgradeit 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
udevand leave the server unable to boot. The package cleanup purges its own list, and apt takes theubuntu-servermeta-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",udevandinitramfs-toolsincluded: on a stock cloud image both are marked auto-installed. Theapt full-upgradethat 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,udevamong 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 thanholdon purpose: aholdwould block the upgrade of those very packages, which is not what anyone wants, while manual only clears the "no longer required" status. The pre-existingapt-mark holdinside the cleanup did not cover this: its list names neitherudevnorinitramfs-tools, and it is released before the upgrade, which is whynetplan.iois 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
udevand 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.pkgsand 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
unpackedorhalf-configuredcounts as present for the snapshot: it is there and has to be protected. For the verdict before the reboot it does not:initramfs-toolsleftunpackedmeans 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/fstabcould be glued onto the preceding line.optimize_swapappended/swapfile none swap sw 0 0with>>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
PresharedKeyin 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. YetPresharedKeylives 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 inamneziawg-toolsis case insensitive too (config.c,get_valuethroughstrncasecmp), which makespresharedkey = ...a valid line. An exhaustive search across all 168 issues and pull requests and all 55 discussions of this repository found no exposedPresharedKeyorPrivateKeyvalue. 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
awgprints the line itself to stderr (Line unrecognized: ...), already stripped of whitespace and wrapped in a backtick, and on a malformed key it printsKey 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
--diagnosticbranch 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 adie. 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.
--diagnosticruns before the installer narrows the permissions on/root/awg, so the window in which the file sat with default permissions was reachable. Aumask 077is now set for the duration, the same idiom already used for keys - The server config section now has a fallback line. If
awg0.confcannot 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 likePrivateKey = 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 showprintsHeaderProtectionKeyin clear text, and it reached the report too. The private and preshared keys are hidden byawg showitself, but the header protection key is printed as is (upstreamsrc/show.c:key()instead ofmasked_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 thenofailworkaround for when udev cannot be restored. The second explains the new installer stop that lists missing packages: what/root/awg/boot-critical.pkgsis, 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 casetests/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,HeaderProtectionKeyin the config, two journal forms (bare and with asystemctl statusprefix), the malformed-key message, and the threeawg showforms. 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 asPrivateKeyFile = /path, obfuscation parameters, and a server name, which may itself contain the textPrivateKey = ...- that is where the unanchored version of the filter broketests/test_boot_critical_packages.bats(47 checks): asserts the order, not just the presence.apt-mark manualmust come before the upgrade and the comparison after it but before the reboot; presence alone would not be enough, since the pre-existingapt-mark holdwas 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 itsdieto 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
regensilently collapsed the lists. The installer writesAllowedIPsandDNSwith a space after each comma, whileregenerate_clientread the current values from the client.confthroughtr -d '[:space:]', stripping the spaces together with the carriage return, and wrote what it had read straight back. The very firstregenturned1.0.0.0/8, 2.0.0.0/7into1.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 canonicala, b, cform, so a repeatedregennot only preserves the format but also restores it in configs damaged by earlier versions. Verified on a test server: afterregenthe damaged config matched the original byte for byte. Thanks to@humownsfor the find and for checking the routes on his side (discussion #38)manage modifystored the list exactly as it was typed. The value was validated element by element but written verbatim, somodify NAME AllowedIPs "a,b"left a collapsed variant of the same list in the config.AllowedIPsandDNSare now normalised here as well, and the entered value and the normalised one are logged separately- Repeated
AllowedIPsandDNSlines are no longer lost.wgallows those keys to be given on several lines, and the values add up. The old code glued them into a plainly invalid address, soawg-quickrefused 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 modifycan no longer wipe a setting. The post-write check required only theKey =prefix, so an empty value passed as success while the backup was deleted. The value must now be non-empty. A separate check makes sureawg_common.shis 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. 1used to be cleaned by the oldtrby luck, and trimming edges only would have preserved the typo. Whitespace is now stripped inside the element, the same way themodifyvalidator 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.confdamaged by an earlierregen. The separate structuredallowed_ipsfield 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 functionalregenerate_clientruns, two functionalmanage modifyruns and two decodes of a realvpn://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 intests/test_issue170_route_mode_change.batswas 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, pluscloud-initwhen it does not manage the network. The removal was apurgefollowed by wiping the/snap,/var/snapand/var/lib/snapddirectories, all without a single question: thatsnapdhad taken the installed snaps and their data with it was something you found out afterwards, andrm -rfdoes 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/cloudand/var/lib/cloudwhencloud-initis 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 inawgsetup_cfg.initso 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@kemkofor raising it (#213) - Any failed check means "do not remove". No terminal available (
ssh host bash ...without-t, ansible, user-data, cron),dpkgnot 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 explicity,yesorдаand nothing else, whereas the earlier version tested the answer for the Latin lettern, 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*,bareandlxddo 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/snapsrather than fromsnap list, because thesnapbinary may already be gone from an earlier run and the list would silently come back empty - New
--keep-packagesflag: 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--yesno question is asked and the packages are removed as before, so keeping them without a human present takes an explicit--keep-packages
Fixed
- The
--helptext hid the fact that--no-tweaksalso 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 inADVANCED.mdandADVANCED.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.shbroke each other, and one of them died halfway through. The script fromCASCADE.en.mdbuilt the set of Russian networks through a temporaryru_tmpwith a fixed name and no locking at all. With two instances running, the one finishing first destroyedru_tmpfrom under the second, and the second failed withipset v7.17: The set with the given name does not exist. Where the second run comes from: theawg-routingunit starts at boot, a reinstall reboots the machine, loading ~8600 networks goes oneipsetcall 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 withipset 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 sharedru.zone.tmpand moved it into place withmv, while thetrapof the first deleted the file of the second: in one run out of three the failure looked likemv: 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 withipsetand applying the rules. Thanks to@aya2workfor the report (#212) - The downloaded list of Russian networks is now validated by content, not just for being non-empty.
curl -fonly 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-timewas 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
OKwhen some of your addresses did not make it. FailedEXTRA_RU_NETSentries 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 toRequires=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,ipsetset andmanglerules - 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 theawg-routingunit, theip ruleby mark, the contents of table 100, the list ofipsetset names with the entry count forru, the age and size of theru.zonefile, the cascade rules frommangleand the NAT rule onawg1. That last one matters most: the script appliesMASQUERADEonawg1LAST, 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. Theruset 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_NETSvariable. It exists for the case an address list cannot fix: a site in the.ruzone behind a foreign CDN looks foreign by destination address and leaves through the far leg. Addresses fromEXTRA_RU_NETSare 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 liveruset 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 existmeans. Theipset list rucommand 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: theipsetblock 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 ruleandip route show table 100can 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_CONNECTIONalone is not enough: the script runs throughsudo,sudodoesenv_resetby default, and that variable is not in the Debian/Ubuntuenv_keeplist - so the second path iswho, 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 instancewhoreturning a hostname instead of an address withUseDNS 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--yestoo: 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 theAddressline of the server config, and only then from the environment variable; if nothing is found the verdict is "unknown". That matters becausemanage restartdoes not loadawgsetup_cfg.init: with a substituted literal10.9.9.1/24anyone who installed with--subnetwould 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 twoapply_configbranches where a restart happens UNEXPECTEDLY - whenawg-quick striporawg syncconffails during a routineadd/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,ContentPaddingAdditionandRekeyAfterTimethat had been set stayed on the live interface after applying a config without them. For AWG parameterssetconfandsyncconfonly ever add, so the operation "drop the line fromawg0.confand apply" silently did nothing: the file changed, the interface did not, and nothing caught that divergence. It affected nobody so far - ouradd/remove/regenonly touch[Peer]sections, and the single hint about deleting anI1line 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 withmanage restart, the interface is already recreated and the parameter already cleared, yet the next ordinaryaddwould 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 aftermanage restart,manage restoreand 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:syncconfapplies values correctly. The set is deliberately not compared against the live interface -awg showconfprints 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 writesJc/S/H, so emptiness speaks of a file being rewritten at that moment. The snapshot write is atomic (temp plusmv), 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.mdgain a section, "What a site can see when traffic is split by destination", covering the consequence for each of the three schemes: a client-sideAllowedIPslist 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.8and@77.88.8.8) and run against thecascade/ru.zonesnapshot (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.ruzone says nothing about the address itself; andapi.ipify.orghanded out three different addresses within a single day, being anycast behind Cloudflare.CASCADE.mdandWARP-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 tokuza2000for 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
AllowedIPscarries a::/0route: a full tunnel without--allow-ipv6-tunnel(the default) gets0.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-tunnelenabled 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.cofrom 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 the1.0.xline; 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 plainapt upgrade" path for such hosts - the installer keepsamneziawg-dkmsonapt-mark holdprecisely 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
alttext 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 thealttext 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 holdwas set only on the pinned path, that is with a kernel older than 6.7, while the branch for newer kernels ranapt-mark unhold; the ARM block with the prebuilt package runs AFTER that gate and does not depend on it. The prebuilt package is namedamneziawg-kmod-<KERNEL_ID>and declares noProvides: amneziawg-modules, so it does not satisfy the alternative inamneziawg-tools'Recommends, andamneziawg-modulesitself is absent from the PPA (verified against the livePackages.gzmetadata for noble, amd64 and arm64). The tools are installed viaapt install -ywith recommends, so apt pulledamneziawg-dkms- leaving two trees carrying a module of the same name on the system: the pinned 2.0 inextra/and 3.0 inupdates/dkms/. That is exactly the state theholdexists to prevent. The scenario is reachable: the prebuilt packages forubuntu-2510-arm64anddebian-trixie-arm64are built against kernels 6.7+. Theholdis 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: theholdis a precondition, and nobody was looking at the result, even thoughstep3_check_modulereadsvermagicand themodinfooutput 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 fromextra/(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 anddpkgstill believes the prebuilt package is installed. The earlier wording "which of the two trees loads cannot be predicted" is replaced by the measured outcome. Theholditself was verified live: an explicitapt-get install -y amneziawg-dkmson an installed bench is refused (Held packages were changed), so no second tree appears at all checkanddiagnosecould report a module version that is not in the kernel. Both commands askedmodinfo amneziawg, andmodinforeads the metadata of whichever.kowas selected on disk viamodules.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,modinfonames 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_VERSIONis declared insrc/main.cin both lines, so the file exists for the pinned 2.0 as well as for 3.0.modinforemains 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.mdand 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.mdcarries 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; whyS1-S4have to match (the receiver reads the message type at exactly that offset) and whyJc/I1-I5do not. There is also an explicit list of what the installer does not enable yet:HeaderProtectionKeyneeds every client to understand it at once, andContentPaddingAdditionwaits on an upstream fix - A "Rotating the server key" section in
ADVANCED.mdandADVANCED.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 intoawg0.conf, service restart andregen. Three easy mistakes are called out separately: skipping/root/awg/server_private.key(a--forcereinstall then brings the old key back), skippingregen(existing clients keep.png,.vpnuriand.vpnuri.pngcarrying 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 checkanddiagnosereport 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 separatemodinfo.diagnoseadditionally names AmneziaWG 3.0 when the version starts with3.; otherwise the raw value is printed rather than a derived protocol number, because upstream tag names have never tracked the protocol version.check --jsongains amodule.versionfield (string ornull) - adding a key keeps the existing envelope readable by current consumers
Fixed
- The second S-padding size collision is gone:
S3can no longer equalS2 + 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 onlyS2 = S1 + 56was checked.S3 = S1 + 84is unreachable with our ranges, butS3 = S2 + 28is not: it came up in roughly 0.17% of installs, about one in six hundred.S3is the value regenerated, so the already-checked condition onS2still holds. Tests were added for all three collisions and for theS1-S4ranges. ⚠️ 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] '- ifS3is exactly 28 more thanS2, you are in that 0.17%. Fixing it means regenerating the whole set (--preseton reinstall) followed byregenand redistributing configs, which for most people is not worth the trouble - Cascade guide:
Persistent=trueis 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: theawg-routingunit is enabled at boot, so the list of Russian networks refreshes on every startup before any timer is involved, andPersistentonly 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 hardRequires=on both tunnels, which neither the timer norPersistentcan 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.mdand the limitations list inCASCADE.md/CASCADE.en.mdnow say it outright: with--forcethe 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 wholeJc/S/H/I1set and do force a re-issue viaregen. For the cascade it is now stated that its logic lives inawg-routing.shand survives a reinstall, but the routing script has to be run once more afterwards - The update section of
INSTALL_VPS.mdnow says what the reinstall itself looks like: with--forcethe 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_phoneandmy_laptopclients. 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, versionv3.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.mdand 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-moduledefault branch, and theppa:amnezia/ppaPPA switched to it the same night. The 3.0 module uses the kernel functionnla_put_uint, which only appeared in kernel 6.7, so on Debian 12 (kernel 6.1) the PPA module build fails withimplicit 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 PPAamneziawg-dkmspackage 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. Theamneziawg-toolsuserland 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-dkmspackage is put onholdon the pinned path.amneziawg-toolsrecommendsamneziawg-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 laterapt 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.txtpointed at the upstream default-branchHEAD, 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 - anddebian-bookworm-arm64(kernel 6.1) already stopped compiling for the same reason as on x86. The pin now points at the tagv1.0.20260725so 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.initdisagrees with the live config.manage regenandmanage checkcompare the obfuscation parameters in/root/awg/awgsetup_cfg.initagainst the[Interface]section of the live/etc/amnezia/amneziawg/awg0.confand name the keys that disagree. Prompted by #196: someone editedI1in the init file, ranregen, and the client kept its old value. That is by design - after the install the obfuscation parameters come fromawg0.confalone, 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: editingawg0.confand 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 anawg0.confthat 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 publishedbird.conffails 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-S4andH1-H4must match on the server and the client: the receiver strips that padding and checks those headers.Jc/Jmin/JmaxandI1-I5do not have to match, being separate decoy packets that the far side discards (checked against theamneziawg-gosources: 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 anI1mismatch that cannot be the cause; the same place now notes thatH1-H4are 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,
JcandI1-I5included, 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 onS1-S4andH1-H4alone. Separately, the rationale in the--no-cpsFAQ is corrected: clients have to be reissued after CPS is turned off not because a client that still hasI1"will not match" a server without it (measured on a test box: the handshake completes with anyI1mismatch, including the parameter being present on one side only), but because the client config still carriesI1and 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 treatsstarton an already active unit as done, so withRemainAfterExit=yesthe timer fires into the void, and invisibly -systemctl list-timersshows a freshLASTwhile 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, andRandomizedDelaySec, so that everyone following the guide does not hit the list source in the same second. It is also spelled out that withRemainAfterExit=no,systemctl status awg-routingshowsinactive (dead)between runs and that is normal - The code comment about
I1-I5having to match is now in line with the facts (awg_common.sh/awg_common_en.sh, theregenpath). #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 anyI1mismatch, including the parameter being present on one side only. The accurate half of the line is kept -regenreally 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 withawg0.confas 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
addandregenno longer write a client.confwith a broken port: the server port (from the liveawg0.conf, elseawgsetup_cfg.init) went into theEndpoint = IP:PORTline 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 socheck,diagnose,addandregenuse one copystats --jsonno longer breaks on non-numeric counters:rx/tx/last_handshakefromawg show dumpwent 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 to0- 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.XXXXbehind. 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 --jsonno longer breaks on a non-numericAWG_PORT: the value fromawgsetup_cfg.initwent into theport.numberfield 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 becomes0- A corrupt port in the config is now an error, not a silent warning:
checkwithAWG_PORT=abcused to finish withok=true, so monitoring saw nothing wrong. A non-empty value that is not a port now yieldsok=falseand 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 indiagnose, 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
--jsonfor 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 --jsonandstats --jsonare 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#130AWG_STRICT_CONFIRM=1(opt-in, per-run ENV): a non-interactive run of a destructive command without an explicit--yes/AWG_YES=1is refused instead of silently proceeding - protection for cron, CI and bots. Default behavior is unchangedcheckgot 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_escapehardened: C0 control characters are escaped as\u00XX, invalid UTF-8 is replaced with U+FFFD - closes a latent way to breaklist --json/stats --jsonwith garbage bytes (same class as the vpn:// ESC bug from v5.20.0)removewith partially missing names now exits with code 1 (previously 0) - symmetry withadd/regenfor automation (#175)- An
--apply-modeerror now honors a--jsonplaced later in argv; usage errors with--jsonno longer pollute stdout with help text - Stale artifacts of a same-named client (
.png/.vpnuri) are cleaned up beforeadd, so the file report is always honest depmodstdout is silenced during module repair (keepsrepair-module --jsonclean)
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,
--mobileas 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-cpsand--reset-routesadded to the CLI references; the autotest counter updated
Tests
- Around 60 new bats tests: the strict-confirm matrix,
json_escapehardening (ESC/BEL/broken UTF-8), the JSON contract on failure paths, behavioral envelope checks for every command, RU/EN schema parity, thelist/statsformat 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.shandawg_common.shis 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 foundand a bogus "Invalid CIDR format" on a perfectly valid0.0.0.0/0, ::/0(the missing function's exit code 127 was inverted by anif !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 likemy..backup.conforv1..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, the0.0.0.0/0mode 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'AllowedIPsso devices can see each other. The setting is persisted inawgsetup_cfg.init(theCLIENT_ISOLATIONkey) and changed by reinstalling; existing clients needmanage regen --reset-routesafter 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 (theCLIENT_ISOLATION_NETkey). Both keys are validated on config load: a non-0|1CLIENT_ISOLATIONand anything but a single canonical CIDR inCLIENT_ISOLATION_NETproduce a warning and a safe default (#178) --mobileflag - the mobile setup in one option:--preset=mobileplus 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=mobileonly tuned the obfuscation: two users installed with the preset and stayed on the dead port. An explicit--port=Nwins over 443; a contradicting--presetis an error;--preset=mobileitself 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-gopath in ADVANCED. Previously the install would reach step 3 and die with a rawmodprobe: FATALwith no explanation - Configurable server name in the
vpn://link (--server-name=NAME, a question on first install). The field used to be hardcoded toAWG 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 inawgsetup_cfg.init(theAWG_SERVER_NAMEkey) and survives reinstalls; old configs without the key keep gettingAWG 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 reissuevpn://links viamanage 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/99after 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 toawgsetup_cfg.initwhileawg0.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 theirEndpointstill holds the old port and points atmanage regen(#175) - Reinstalling with a new
--portdeletes the old port's UFW rule: previously the old UDP port stayed open forever - the onlyufw deletelives in uninstall and reads the already rewritten config, so even--uninstallnever removed it. The old port is persisted inawgsetup_cfg.init(thePREV_AWG_PORTkey) 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-moduleno longer reports "service is active" with exit 0 while the service is down:ensure_amneziawg_kernel_module fullnow 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/removekeep their previous behavior (warning + config written; apply_config reports the apply failure itself) (#175)manage addexits non-zero when a requested client already exists (no-op): parity withremove/regen, automation can distinguish "created" from "nothing was done" (#175)- The
NO_CPSkey was added to thesafe_load_configwhitelist inawg_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-routesflag. A regularregendeliberately preserves the client's individualAllowedIPs(modifycustomizations), so a new global routing mode never reached existing clients. With--reset-routesregenerated clients get theAllowedIPsof the current global mode fromawgsetup_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-amneziaon reinstall (--force) changed onlyALLOWED_IPS_MODEwhile theALLOWED_IPSlist kept the old value fromawgsetup_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
Addressline in any order. Previously it took the first comma field, so anAddresswith 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.
--subnetand 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. The10.9.9.1/24default 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--uninstalland 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-cpsflag (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-cpsdrops 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 withmanage regen(#159)
Fixed
- Network-interface detection no longer aborts the install on hosts where the
1.1.1.1probe 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_nicnow 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 withAWG_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.vpnurifile 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 withawg-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 inawg-routing.shis now: ipdeny (the live list) -> the repo snapshot overraw.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.shretries adding the route to the exit server with theonlinkflag when the default gateway sits outside the server subnet - otherwise on VPSes like Hetzner (real public IP on a/32interface, private gateway) the route failed withError: Nexthop has invalid gateway(#158, Discussion #120) - Added a
cascade/README.mdnote 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-actionfrom 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 onlyy. Previouslyconfirm_action(inmanage) and five installer prompts (reboot, unsupported OS, low disk space, UFW enable, backup on removal) matched a bareyonly, and answeringyesread as "no". They now accepty/Y/yes/YESwith any surrounding whitespace (issue #154) - UFW is enabled via
ufw --force enable. The old approach (feedingyintoufw enable) silently left the firewall off on some systems - only--forcehelped, 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_usprofile 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
dpkglock is busy. At the start of step 1 the installer setsDPkg::Lock::Timeout(via/etc/apt/apt.conf.d), so everyaptcall waits for the lock thatunattended-upgrades/apt-dailyhold on first boot. Ifapt full-upgradestill does not go through, the installer logs the process holding the lock, runsdpkg --configure -aand 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=Nnow changes the server port. Previously the new port was silently ignored: when rendering the server config, the port was re-read from the oldawg0.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. Clientregenis unaffected - it still reads the port from the liveawg0.conf- Full-tunnel clients (
AllowedIPs = 0.0.0.0/0) now get0.0.0.0/0, ::/0. With a bare0.0.0.0/0, AmneziaVPN on iOS treated the config as incomplete split tunneling and refused to bring the tunnel up.::/0sends 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.1instead of a single resolver (a fallback DNS in case the first one is unreachable) --portnow 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/S4range 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
--forcegate in the re-run FAQ
Tests
- Added
tests/test_v5181_bugfix.bats- the port from the saved parameters wins over the oldawg0.confon 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 justI1) from the serverawg0.confinto client configs on generation andregen: the live-config parser readsI2-I5, the client.confrender appends the set values, and the previously hard-coded emptyI2-I5in thevpn://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-I5are optional and independent: unset ones are simply not emitted - The params inherit the same preservation semantics as the rest of the obfuscation:
--forcewithout--preset/--jc/--jmin/--jmaxreadsI2-I5from the liveawg0.confand keeps them;--preset/--jcregenerate the whole obfuscation set, which drops manually setI2-I5(same asH1-H4/S1-S4/I1)
Tests
- Added
tests/test_v5180_i2i5_passthrough.bats- parsingI2-I5fromawg0.conf, anti-stale protection across calls, client.confrender (present when set, absent when empty), and a realvpn://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-mssrule in themangletable'sFORWARDchain caps the advertised TCP MSS to a tunnel-safe value in both directions (-o %iand-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_MTUwhen 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 conservativeMTU = 1280rather than replacing it
Tests
- Added
tests/test_v5170_mss_clamp.bats- asserts the rules in both directions, the mirrored-Din PostDown, IPv6 gating, and MSS auto-derivation fromAWG_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/5range, which covers the reserved0.0.0.0/8; the iOS kernel chokes on that block and never reaches the rest of the routes. The first range is split into1.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
--expireserror message now lists30damong 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(was51820from upstream WireGuard); the troubleshooting section no longer references a nonexistentafter-awg1.confdrop-in - the autostart check now points at theawg-routing.serviceunit; 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 to0.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
/tmpinto$AWG_DIR(0700): a predictable name in/tmpwould 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 andchmodis gone; thekeys/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
uninstallno longer purges thefail2banpackage if it was installed before our installer (the.fail2ban_installed_by_installermarker, symmetric to the UFW marker); a user-owned fail2ban is restarted without our jailuninstallremoves only the exact lines written by legacy versions of this installer from/etc/sysctl.confinstead of any line containingdisable_ipv6(it could previously wipe user-added settings)- Library functions
generate_client/add_peer_to_server/remove_peer_from_server/regenerate_clientnow 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_configappends peers before themv). 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--forcereinstall remove_peer_from_serverchecks 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
regenrecovers the PresharedKey from the server config when the client.confis 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, soregenover all clients performed a full curl round (up to 6 services, 5 s each) per client validate_awg_configchecks 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-editedJc=4no 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-modeis validated at argument parse time: a typo like--apply-mode=restratused to silently behave assyncconf--diagnosticchecks 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
.listformat (previously only DEB822.sourceswas 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 about0/udp)list: a corrupted expiry file no longer causes a bash arithmetic error in the tablegenerate_vpn_uriverifies 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 addno longer suggests the--endpointflag that manage does not have
Improved
- Installer steps 1-2 no longer re-run
apt-get updatewithout a sources change (saves 10-60 seconds per run on slow mirrors, up to two redundant runs per install) backupadditionally takes the config lock: a paralleladd/removecan 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/--jmaxprints an explicit warning that the ENTIRE obfuscation parameter set is regenerated and existing client configs will needregen(the idempotency-guard message was clarified too) setup_fail2banwarns when UFW is inactive (bans withbanaction=ufware effectively inert in that case)- The
apt-get updateerror classification ignores known informational linesW: Target ... is configured multiple timesandW: ... legacy trusted.gpg- they no longer turn a tolerable failure into a false fatal diagnosemakes oneawg showcall instead of four;statsno longer spawnsdateper peer;list --jsondoes 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/urandomis unavailable) covers the full 31-bit range instead of 30 bits managehelp:regendocuments accepting multiple names,repair-modulelists itsrepairalias; the client-creation message mentions.pngonly when the QR was actually created- Dead code removed: the unreachable
help)dispatcher branch in manage, the unreachablereturnafterrequest_rebootin the installer, two redundantAWG_APPLY_MODEre-exports, stale shellcheck directives - CI/release scripts:
update-sha-pins.shpreserves file permissions on rewrite;build-arm-deb.shsurfaces xz stderr on failure;build-release-notes.shvalidates 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 --jsonandstats --jsonnow emit an extrastatus_codefield with a stable, language-independent value (active|recent|inactive|no_handshake|key_error|no_data). The existing localizedstatusfield is kept - the change is additive and backward-compatible, and convenient forjqand automation (the contract is documented inADVANCED.en.md)
Improved
manage modify <client>validates each DNS entry as a real IPv4/IPv6 address (via the shared_valid_ipv4/_valid_ipv6helpers) rather than a character set, so values likeabcor999.999.999.999are no longer accepted. The same octet-range check (0-255) now applies to the auto-detected server public IP--expiresduration 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--pskis 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.vpnuriand the temporary QR PNGs are written through the shared safe-temp mechanism with an atomicmv, 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:--endpointaccepts an FQDN, IPv4 or[IPv6]; clarified that--no-tweaksstill applies the minimal forwarding sysctl - The command tables in
README.en.mdnow includediagnoseandrepair-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_codecontract
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 = systemdon Ubuntu as well, matching Debian since v5.7.12 (PR #106, thanks @stereomonk) - fail2ban status after restart is now checked honestly:
systemctl restartreturns 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 viasystemctl 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 (
0080is 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-customis 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
--helpstill returns 0. manage restoreis 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 modifyvalidates IP and CIDR values by range, not just by shape.- File-operation atomicity: config temp files are created on the destination filesystem (so
mvis actually atomic), the client QR PNG is written via temp +mv, and temp cleanup, comma validation in lists, andvpn://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.ymlbuilds 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,
--jsonscope and the supported OS count; the CLI reference in ADVANCED is synced with the actual flags. - README and INSTALL_VPS point at
--ssh-portfor 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
manageCRUD), 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/curlexamples 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.comlinks 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::/0IPv6 route. A full tunnel (ALLOWED_IPS=0.0.0.0/0) still gets::/0with 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.confeven after the server settings or amanage modifychanged 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
::/0route 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.
installfails fast ifapt updateerrors out, instead of continuing a multi-step install against a stale package cache.manage modifyvalidates the Endpoint and AllowedIPs it is given (host:port, CIDR list) before writing them.manage addis safe against name reuse: it refuses a name that already exists and cleans up partial key or config artifacts if creation fails partway.manage restoreprunes stale clients and keys that are absent from the backup, so a restore yields exactly the backed-up state.manage helpexits 0 for an explicit help request and 1 only for a real usage error.manage --jsonkeeps stdout pure JSON by routing info and debug logging to stderr.- Log messages no longer double percent signs ("95%" stays "95%").
manage restorerecreates the expiry directory before restoring expiry timestamps.
Documentation
--subnethelp states the/24-only requirement;install --helplists Ubuntu 26.04.manage --helpand the README documentlist --json(including theclient_ipv6field).- Restored the changelog compare-links for 5.11.0-5.15.0, fixed several broken in-page anchors, refreshed
SECURITY.mdandCONTRIBUTING.md, and addeddocs/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-tunnelflag ininstall_amneziawg.sh(opt-in, off by default). Enables dual-stack IPv6 inside the VPN tunnel: the server and clients get addresses from the ULA subnetfddd:2c4:2c4:2c4::/64next to IPv4. The subnet can be overridden viaIPV6_SUBNET=inawgsetup_cfg.init. This is separate from the existing--allow-ipv6(host-level IPv6 via sysctl); the behavior of--allow-ipv6/--disallow-ipv6is unchanged. - 🌐 Dual-stack server and client configuration. With the flag on, the server
awg0.confgets IPv6 inAddressandAllowedIPs, an ip6tables MASQUERADE rule on the public NIC is set up, and the client.confplusvpn://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::/0route, 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 listandmanage regenare 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 setALLOW_IPV6_TUNNEL=1inawgsetup_cfg.init) recreate that client:manage remove <name>, thenmanage add <name>. Only recreation allocates an IPv6 for the client on the server and issues a dual-stack config. A plainregenkeeps the client IPv4-only, because its[Peer]entry on the server has no IPv6 yet. The new.confmust 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 realsha256sumof the four helper scripts against theCOMMON_SCRIPT_SHA256/MANAGE_SCRIPT_SHA256pins in both installers, so a partial bump cannot ship an installer with drifted pins. - New helper
scripts/update-sha-pins.sh(with a--verifyflag) to recompute and verify the SHA pins, andscripts/preflight-check.shfor 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
::/0route, 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-portflag, then the effectivesshd -Tconfig (thePortdirective andListenAddress host:port, honouring drop-in files insshd_config.d), then the realsshdlistening sockets viass, thensshd_configparsing, and port 22 as the default. UFW now opens every detected SSH port, the pre-enable warning names them explicitly, and the--yesmode also detects the port instead of assuming 22. - 🆕
--ssh-port=PORTflag 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, fromsshd -T(thePortdirective andListenAddress, IPv4 and bracketed IPv6), union withsssockets, 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
--yesinstall: 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): answeringNto 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--yesflag 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 callufw enable; accepting enables UFW;--yesmode 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_systemininstall_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 aggressiveapt-get autoremoveafterapt-get purge cloud-initcascaded into removingnetplan-generatoras a transitive cloud-init dependency. Withoutnetplan-generatorthe/etc/netplan/00-installer-config.yamlfile (subiquity creates it on ISO installs) was no longer translated into/run/systemd/network/*.network, andsystemd-networkdstarted with empty configuration. Changes incleanup_system(): theapt-get autoremovecall is dropped; before anyapt-get purgeanapt-mark holdis applied to critical network stack packages (netplan.io,netplan-generator,systemd-resolved,netcfg,ifupdown) - we first snapshot the user's existing holds viaapt-mark showholdand 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.iois reinstalled unconditionally,netplan-generatoronly when it is available in the archive viaapt-cache show, so Debian 12 - which does not ship that package - does not abort the apt transaction), restartsystemd-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 viaip link set up, firstnetworkctl renewwith a route re-check, thendhclient -4if 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 afterpurge(~50-200 MB) - acceptable trade-off for stability; users can manually runapt-get autoremove --no-install-recommendsafter 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 ofdpkg-query,apt-get,apt-mark,apt-cache,ip,systemctl,netplan,networkctl,dhclient,sleep:apt-get autoremoveis never invoked;apt-mark holdfires for the netplan and systemd-resolved packages before anypurge(withoutsystemd-networkd- that is not a standalone package on Ubuntu 24+, the binary lives insidesystemd); pre-existing user apt-mark holds are left alone (our hold/unhold cycle skips them); recovery path when default route is lost (installnetplan.iounconditionally,netplan-generatoronly behind anapt-cache showgate,netplan apply+ wait loop); last-ditch path after primary recovery fails (ip link set up+networkctl renewwith a route re-check, thendhclient -4as fallback);diepath on total failure with the--no-tweakshint; existing cloud-init guard (three marker checks) preserved. Structural checks: RU/EN line parity ofcleanup_system, presence ofapt-mark hold/unhold/diein both files, absence of a realapt-get autoremoveline (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-tweaksworkaround 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.pngQR is now scannable off a screen.awg_common.sh:generate_qr_vpnurinow invokesqrencodewith an explicit-s 6(the previous default was3). 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 currentqrencodedefaults are also pinned explicitly:-l L(lowest error-correction level) and-m 4(standard quiet zone) - so future default changes inlibqrencodecannot regress this fix. Pasting the text of.vpnuriinto the app already worked; the fix restores the primary camera-scan path. - 🛠️
scripts/build-arm-deb.sh: explicitKERNEL_VERSION+ fail on ambiguity. The ARM .deb builder used to pick the first matching/lib/modules/*/builddirectory 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_versionhelper with three paths: whenKERNEL_VERSIONis set, the helper validates/lib/modules/$KERNEL_VERSION/buildand 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 setKERNEL_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 thatqrencodereceives-l L,-s 6,-m 4, that-t pngis still passed, regression check that the PNG file is still produced from the.vpnuripayload, 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 abuild/subdir ignored;KERNEL_VERSIONenv 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 safesourcefrom tests, and the old inline detection loop is gone from the main body.
Compatibility
- Backward compatible with v5.14.x. Default behaviour is unchanged:
qrencodestill produces.vpnuri.png, and on CI hosts with exactly one installed kernel_resolve_kernel_versionreturns the same result as the previous loop. - The workaround previously required for long QRs (manually copying
.vpnuricontents 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 bothawg_common.sh:render_client_configandrender_server_confighardcodedMTU = 1280. If the user hand-edited MTU in/etc/amnezia/amneziawg/awg0.conf,manage_amneziawg.sh regenstill wrote the stale1280into the new client.conf. MTU resolution is now ordered: value from the[Interface]section of serverawg0.conf(the source of truth for a running server), thenAWG_MTUfromawgsetup_cfg.init, then1280as fallback. The live-config parser (load_awg_paramsfor AWG parameters from awg0.conf) also reads theMTU = ...line now and exportsAWG_MTU. Values outside the sane range576..9100at any stage fall back to1280. Reported in Discussion #38 by @E-lmedano. - 🔧 Installer:
AWG_MTUvariable inawgsetup_cfg.init. Fresh installs writeAWG_MTU=1280into 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 thesafe_load_configwhitelist.
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 onrender_client_config(noMTU = 1280hardcode, uses${mtu}substitution),render_server_config(uses${AWG_MTU:-1280});safe_load_configwhitelist containsAWG_MTUin all 4 files; installer writesAWG_MTUtoawgsetup_cfg.init(RU + EN); byte-identical_extract_mtu_from_server_confbetween RU and EN.
Compatibility
- Backwards-compatible with v5.13.x and v5.14.0. Default scenario behaviour is unchanged: with
MTU = 1280on the server,regenstill produces1280in the client config. - The previous workaround (hand-editing
/root/awg/<name>.confafterregen) is no longer needed - regen picks up whatever is inawg0.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.
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.comis reachable from AWS / GCP / OCI private subnets behind a NAT Gateway whereifconfig.mecan rate-limit;ifconfig.iois a backup forifconfig.medowntime. 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 clientEndpoint =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=NAMEit 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 fromADVANCED.en.mdoperator 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.ymlworkflow that uploads pre-generated.minisigfiles. Activation is gated on the maintainer generating an offline keypair and committingKEYS.txtto the repository root; until then the section inSECURITY.mddescribes 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 ondiagnose_serverand the_diagnose_carrier_known,_diagnose_carrier_list,_diag_linehelpers; CLI parser accepts--carrier=NAME; command dispatcher wires updiagnose; usage help mentions diagnose; functional checks on the carrier map (beeline_mskrow matches default-preset shape,tele2_krasnoyarskhasi1=absent,tmobile_ushasi1=binary+ Jc=6; unknown carrier returns 1); carrier list has 7 distinct confirmed entries; previously-included unconfirmedmts_msk+megafon_mskare 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 newdiagnose --carrier=NAMErecognises 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-awg1for I1=none carriers.
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 ondists/<codename>/Release, remaps the suite tonoblein/etc/apt/sources.list.d/amnezia-ppa.sourcesand re-runsapt update. If the server has leftover kernel headers from a previous 24.04 install (typical afterdo-release-upgrade), the installer also pulls ingcc-13fromquesting/universeso the DKMS autoinstall succeeds for every kernel. No manualsources.listedits, no DKMS surprises. The script also repairs a "sticky".sourcesfile from a previous (≤ v5.12.1) run that leftSuites: questingbehind after an apt failure. - 🔒
--forcesafety guard (Issue #78). Re-running the installer on a server with AmneziaWG already configured now requires an explicit--force(orAWG_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 upgrademay pull a new kernel (and require another reboot), and Step 7 restartsawg-quick@awg0— handshakes drop for a few seconds. The guard removes that foot-gun. - 🧹
manage_amneziawg.shlogging: WARN → stderr. In v5.12.1manage_amneziawg.sh:log_msgrouted 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 withinstall_amneziawg.sh:log_msg. - 💾 Precise
/swapfilecheck in/etc/fstab. The old substring checkgrep -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 mockedcurl(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(missingSuites:) gets recreated, legacy.sourcesmismatch 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|-fCLI flag, theAWG_FORCE_REINSTALL=1env 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 byFORCE_REINSTALLoccurrence count; functional matrix of 6 cases (clean install / configured+active+no-force / configured+inactive+no-force → repair flow / configured+active+--force / env bridge / strict=1env vsyes).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/swapfilecorrectly 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=mobileworks 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=1works again inmanage add/manage remove. v5.12.0 added an unconditional pre-call toensure_amneziawg_kernel_modulebefore both actions to makeawg syncconfreliable. Side effect: it broke the offline edit-only flow on dev / CI machines without the kernel module loaded, whereAWG_SKIP_APPLY=1accumulates changes for batch apply (seeADVANCED.en.md— environment variables section). The pre-call is now wrapped inif [[ "${AWG_SKIP_APPLY:-0}" != "1" ]].manage restartis intentionally NOT gated — it is an explicit apply, AWG_SKIP_APPLY has no meaningful semantics for it. Only the literal1is 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 bymanage repair-module) on Debian only triedlinux-headers-${kernel_ver}andlinux-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 metalinux-headers-cloud-${arch}stays available. The installer's step 2 already knew about cloud-headers via smart detection; nowrepair-moduleknows 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.shusedxz -9(CRC64 check, 64 MiB dictionary) — the userspacexz -ttool considered the stream valid, but the in-tree Linux decoder on Debian 13 trixie kernel6.12.85+deb13-arm64(build 2026-04-30) returneddecompression failed with status 6. Switched to a kernel-compatible presetxz --check=crc32 --lzma2=dict=1MiB— matches mainlinescripts/Makefile.modinst. Plus a build-time sanity gate: after compression,xz -t+xz -d -cround-trip; if anything fails —exit 1, no broken prebuilt ships to thearm-packagesrelease. On the v5.12.1 tag push, CI workflowarm-build.ymlre-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 onadd/remove/restartblocks, plus runtime semantics of the gate for every documented value (unset / 0 / 1 / yes / true / YES); confirmsrepair-modulekeeps theAWG_ALLOW_APT_IN_ENSURE=1 ensure_amneziawg_kernel_module fullinvocation; bash -n syntax sanity.test_v5121_cloud_headers.bats(+9) — functional test of_install_kernel_headersvia mockedapt-getanddpkg: cloud kernel gets the cloud meta in candidates before the generic one, standard kernel does not, Ubuntu codepath untouched; the EN mirrorawg_common_en.shis verified separately.test_v5121_xz_kernel_compat.bats(+7) — structural greps for the new xz flags inbuild-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.
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 upgradeof the kernel, DKMS did not always rebuild theamneziawgmodule by the nextreboot—awg-quick@awg0failed withmodprobe: 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-Invokeruns/usr/local/sbin/amneziawg-ensure-module --hook, which iterates/lib/modules/*/build, rebuilds DKMS for every target kernel with installed headers, thendepmod -a. Log at/var/log/amneziawg-ensure-module.log(logrotate weekly, 4 copies). The stamp file/var/lib/amneziawg/ensure-module.stampsilences 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, beforeawg-quick, it iterates kernels with already-installed headers (/lib/modules/*/build), rebuilds DKMS, runsmodprobe amneziawg, and verifies vialsmod. 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 ofmanage 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. SetsAWG_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).
- apt hook (
- 🧠 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 fromuname -r(aws/azure/gcp/oracle/kvm/lowlatency/raspi) with a fallback tolinux-headers-generic; on Debian —linux-headers-cloud-${arch}for cloud kernels, otherwiselinux-headers-${arch}. This protects against the case whereapt-get upgraderaised the kernel, but the new module fails to build without matching headers.
Other
- 🛠
manage_amneziawg.shpre-callsensure_amneziawg_kernel_moduleinadd/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 afterapt upgrade" reports. - 🧹
step_uninstallcleans up the auto-repair components. On--uninstallthe 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 inawg_common.sh+_en.sh, ≥3 pre-calls in manage (add/remove/restart), themanage repair-module|repaircommand, theAWG_ALLOW_APT_IN_ENSUREgate; smart kernel-headers candidate-loop (Ubuntu flavor extraction, Debian cloud detection, RPi guard, fallback ordering: flavor BEFORE generic + cloud BEFORE arch); helper--hook|--systemdmodes, stamp fast-path gated to--hook, modprobe+lsmod in--systemdplus 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 withbash -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 isType=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 c3now regenerates all three clients, not just the first. Until v5.11.5 theregencase picked up only the first argument from the list — the rest were silently dropped.addandremovealready had the loop over arguments, butregenwas missing it. Behaviour is now consistent: each name is validated and processed individually, a missing client logs a warning and contributesrc=1, valid ones still get regenerated. A summary at the end printsProcessed: N of M. The no-args path (manage regenregenerates 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 updatecheck 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 staleapt-cacheand 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_packageretries; any other non-source error —diewith 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.mdnoting that an AmneziaWG 2.0 server withS3>0orS4>0is not compatible with AWG 1.0 clients (see upstream issueamnezia-vpn/amneziawg-linux-kernel-module#168). My installer always generatesS3=8..55andS4=4..27— both>0— so in the typical scenario (Amnezia VPN client + clients generated bymanage) 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 iteratesARGS[@](the headline fix for #70);_regen_countcounter and the summary lineОбработано: N из M/Processed: N of M; no regression: single-argregen <name>and no-argsregenbehave as before; missing/invalid names yield a warning +rc=1and the batch keeps going; RU/EN structural parity on the regen branch control-flow tokens;apt_update_tolerantaccepts the--ppa-amnezia-tolerantflag withlocal ppa_tolerant=0declared in both installers; step 2 calls the helper with that flag anddies on a hard error; the OOM / silent-crash guard viaraw_had_non_src_errorsis 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-argsmanage regenare 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. Withmanage add --pskthe PresharedKey was correctly written to the server[Peer]and the client.confsince v5.11.1, but thevpn://URI was missing thepsk_keyfield that the AmneziaVPN parser reads — clients silently came up without the preshared key, the server (which had it) rejected the handshake, andawg show transferstayed at «never». Also tightened trailing CR / space stripping forPresharedKey =andAllowedIPs =so CRLF configs edited on Windows no longer leak\rinto the JSON. (#67, @haritos90) - 🔁 Install survives a brief Launchpad PPA outage. When
ppa.launchpadcontent.netis briefly unreachable (as on May 3rd per #68), the installer now waits foramneziawg-dkmsto show up inapt-cachefor up to 3 attempts with 30 s and 60 s backoff (and a freshapt-get updatebetween retries). Checkingapt-cachematters:apt-get updateitself 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 → nopsk_keyfield; indentedPresharedKey; CRLF-edited config does not leak\rinto the JSON; emptyPresharedKey =line does not turn intopsk_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 --pskwithoutvpn://import behaves the same as before (the PSK has always been written to the.confcorrectly). - 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
--pskflag formanage addshipped 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.rulesin the FAQ. Explicit warning:ufw allow ... proto icmpdoes not work (UFW only supportstcp/udp/esp/ah/gre/ipv6via theprotoflag). (#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) orAWG_YES=1(env) skip the confirm prompt inremove,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) — extractconfirm_actionviaawk+eval. Verifies that non-"1"values ofAWG_YES("yes","true","0") do not match the bypass branch under forced-interactive mode.test_backup_collision.bats(+8) —date +%3Nproduces distinct values under rapid fire;findpattern andsort -rcorrectly 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--pskhighlight; protection against RU/EN cross-link drift in README → ADVANCED.
Compatibility and dependencies
- Fully backwards-compatible.
--yesis 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_vpnurihelper inawg_common.sh/awg_common_en.sh. It reads/root/awg/<name>.vpnuri(the URI thatgenerate_vpn_urihas been producing for a while now — full Amnezia envelope: zlib-JSON withcontainers/defaultContainer/hostName/dns/mtu/protocol_version=2plus all AWG 2.0 params), pipes it intoqrencode -t png, writes/root/awg/<name>.vpnuri.pngwith mode 600. Writes are atomic: first into<name>.vpnuri.png.tmp.$$in the same directory,chmod 600, thenmv -fto the target path — ifqrencodeorchmodfails, the old file stays intact and the orphan.tmp.*is cleaned up. - Hooked into
generate_clientandregenerate_client. After a successfulgenerate_vpn_uri,generate_qr_vpnuriruns. 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 regenandmanage remove.regennow refreshes both QR codes (conf and vpn://) together.removecleans up<name>.vpnuri.pngalongside<name>.conf/.png/.vpnuriand keys. - Backup / restore picks up
.vpnuri.pngautomatically — no new code paths needed: the existing*.pngglob in_backup_configs_nolockandchmod 600 *.pnginrestore_backupalready 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,qrencodenon-zero exit → error, atomic write (pre-existing.vpnuri.pngis preserved on failure, no orphan.tmp.*left behind),chmod 600on Linux/Darwin, RU/EN structural parity of the helper (qrencode call +.vpnuri.pngtarget +command -vguard), hooks ingenerate_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 addandregennow work without theserver_public.keycache. A new_ensure_server_public_keyhelper computes the server public key from the[Interface]PrivateKeyinawg0.confviaawg pubkeyif/root/awg/server_public.keyis 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 beforePrivateKey =(hand-edited configs).- Endpoint fallback chain for egress-restricted setups. Previously
manage addinside LXC without access to external IP services died with "Failed to determine server public IP". Now, aftercurltoifconfig.me/ipify/icanhazip/ipinfofails, I try the first non-loopback IPv4 on a global-scope interface (ip -4 -o addr show scope global). The user gets alog_warnsuggesting they hand-editEndpointin the client.confif the server sits behind NAT. - New
manage add --pskflag. Optionally enablesPresharedKeyin the client.confand in the server[Peer]. Generates a 32-byte key viaawg genpskfor 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 inADVANCED.md/ADVANCED.en.mdmanage 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 fromawg0.conf, edge cases (missing file, missingPrivateKey, ignorePrivateKeyin[Peer]sections, indentedPrivateKey, 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) —PresharedKeyabsent without flag, written whenCLIENT_PSKis set, correct ordering inside[Peer]blocks,CLIENT_PSK="auto"resolution ingenerate_client,--pskparsing 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
installtwice without reboot no longer breaks DKMS.request_rebootnow saves/proc/sys/kernel/random/boot_idto$AWG_DIR/.boot_id_before_step2before 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_statewrite is now atomic. Usestmp + flock + mv -fvia a PID-specific tmp path (${STATE_FILE}.tmp.$BASHPID). Parallel-invocation scenarios can no longer read a half-written step number.awg_common.shandmanage_amneziawg.shdownload via mktemp + SHA256 + atomic mv. New helper_secure_download(): curl writes tomktemp, SHA256 is verified, the verified file ismv'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_backupnow rolls back on failure. Before any destructive operationrestorecreates a pre-restore snapshot (this already existed as an undo aid). In v5.11.0 the snapshot is made known to the function (viaLAST_BACKUP_PATH) and all error paths are wrapped intrap _restore_cleanup RETURN. If anything fails aftersystemctl stop, the cleanup unpacks the snapshot back into place and runssystemctl start awg-quick@awg0. A pre-flightvalidate_awg_configcheck 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 ownRETURNhandler first (trap - RETURN) to avoid leaking into subsequent calls._backup_configs_nolockno longer hides cp failures on critical files. The silent|| trueis 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) keeplog_warnsemantics. Empty globs are distinguished from cp failures via acompgen -Gpre-check.modify_clientno longer runs a destructivesedafter a failed backup. Previouslycp "$cf" "$bak" || log_warn "..."— a warning in the log, thensed -iwould destroy the config with no way back. The backup is now a hard gate:if ! cp ...; then log_error + release lock + return 1.regenerate_clientis serialized under a lock and everysedis checked. The function now wraps its body in.awg_config.lock(flock, 10 s timeout) — concurrentregencalls on the same client name can no longer corrupt the client config. The threesed -istatements that restore user settings (DNS, PersistentKeepalive, AllowedIPs) each useif !— on failure the function returns 1 and the lock is released. The lock is held only while.confis being mutated; it is released beforegenerate_qr/generate_vpn_uri, which remain best-effort derived artifacts.modify_clientflock-timeout no longer leaks the fd. The "another operation holds the lock" branch now callsexec {modify_lock_fd}>&-beforereturn 1. Previously the fd stayed open until shell exit.manage_amneziawg.shversion is back in sync between RU and EN. The drifted5.10.0/5.10.1values converge to5.11.0.
CI / build
- ARM matrix: Ubuntu 25.10 and Debian 13 added, Ubuntu 22.04 removed.
.github/workflows/arm-build.ymlnow builds the prebuiltamneziawg.kofor 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_armininstall_amneziawg.shwas updated in sync — new branches*-generic* + 25.10 → ubuntu-2510-arm64and*-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.conffor AWG 2.0 inADVANCED.md/ADVANCED.en.md. A new collapsible section with a ready example for manual setups (amneziawg-goin 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_MAXupper bound on H1-H4,I1being 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/modifyrely on this marker in each[Peer]block; if you migrateawg0.conffrom an old server, add#_Nameby 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) — atomicupdate_state,boot_idguard, step 2 entry die,request_rebootcapture.test_manage_robustness.bats(+24) —_backup_configs_nolockcontract (LAST_BACKUP_PATH,compgen -G, critical vs optional),modify_clientbackup gate,regenerate_clientlock + sed checks, flock-timeout fd release.test_restore_rollback.bats(+27) —_restore_do_rollbackhelper,trap RETURN+ cleanup contract,_destructive_ops_startedgate, pre-flightvalidate_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_backupexternally behaves as before (success → service up; failure → previously partial state, now rollback); themanageCLI is unchanged; theawgsetup_cfg.initformat 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 foundbroke installation. The function was defined inawg_common.sh, but that file is only downloaded at step 5. The firstapt updateat step 1 (before the system upgrade) and the second at step 2 (after adding the PPA) receivedcommand not found, and installation aborted withdie "apt update error". In v5.10.2 the definition moved inline intoinstall_amneziawg.sh— next tolog/die, following the existing pattern used forgenerate_awg_params. It has been removed fromawg_common.sh. - Edge case in
apt_update_tolerant: silent crash / OOM / SIGKILL are no longer masked. Ifapt-get updatereturned non-zero WITHOUT classifiableE:/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 withSources([^[:alpha:]]|$)— catches future variants likeSources.xz, while preventing false-match on strings likeSourcesMirror. - Synced header date in
install_amneziawg_en.sh(was2026-04-16, now2026-04-20to 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 withoutE:prefix, regex does not matchSourcesMirror. Function loading migrated fromsource awg_common.shtosedrange extraction frominstall_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 updateno 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.sourcescontainsTypes: deb deb-src. The previousapt update -y || diefailed in that case. The newapt_update_tolerantfunction (inawg_common.sh; moved inline intoinstall_amneziawg.shin v5.10.2) ignores 404s only onsource/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 enablesdeb-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=mobileCLI 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=defaultis also available for explicit selection of the standard profile (Jc=3-6, Jmin=40-89, Jmax=Jmin+50..250).--jc=N,--jmin=N,--jmax=NCLI 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=4uses 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_PRESETsaved to configuration. The selected preset is recorded inawgsetup_cfg.initfor diagnostics and reproducibility.
Security
- Config parser BOM and CRLF hardening.
safe_load_configandsafe_read_config_keynow 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.sedreplacements 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_reportreplaces the server IP address with***MASKED***for safe sharing of diagnostic output. - File permissions for vpn:// URIs.
secure_filesandrestore_backupsetchmod 600for.vpnuriand.png(QR code) files. - Client name validation in
set_client_expiry. Prevents path traversal through client names. - Quoted paths in cron file.
install_expiry_cronproperly 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 + restartinstead of a duplicateawg-quick up, preventing the "interface already exists" error. - I1 stale value fix.
load_awg_paramsclearsAWG_I1before parsing the server config, preventing CPS parameter contamination from the initial configuration. - Forced regeneration with CLI flags. Re-running with
--presetor--jc/--jmin/--jmaxforces AWG parameter regeneration even when the config already exists. - ARM prebuilt step completion. The prebuilt
.debinstallation 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\.0instead of5.10.0). - Extended preflight in
build-arm-deb.sh. Addedmodinfo,sha256sum,awk,xzchecks, kernel detection via/lib/modules/*/build, emptyMODULE_VERguard.
CI/CD
- Expanded ShellCheck scope. Workflow now lints
scripts/*.shandtests/*.bashin addition to root.shfiles. - Test workflow hygiene. Added
permissions: contents: readandconcurrencygroup 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) buildsamneziawg.kofor 6 ARM targets via QEMU on everyv*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.debplus.sha256are published to a dedicatedarm-packagesrelease. 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.debfrom thearm-packagesrelease (kernel vermagic must match exactly) and falls back to DKMS silently if it does not. Curl uses--max-time 60to avoid hangs; SHA256 is verified beforedpkg -i. Saves time and RAM on minimal systems without build tools. - Correct kernel headers detection for Raspberry Pi. RPi Foundation kernels (
+rpt/-rpisuffix) now picklinux-headers-rpi-v8orlinux-headers-rpi-2712instead of the non-existentlinux-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_armis 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@awg0active, 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-permissionsflag 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 accepted999.0.0.1as a "valid" IPv4 address because[0-9]{1,3}did not check the numeric value. A second pass viaBASH_REMATCHnow verifies each octet is in the 0-255 range.validate_endpoint "256.0.0.1"andvalidate_endpoint "999.999.999.999"now correctly return 1. restore_backup— abort on first copy error. All five criticalcp -aoperations (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_configsandrestore_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. Ifmanage backupandmanage restorerun concurrently the second process waits or exits with a clear diagnostic. - Self-deadlock prevention in
restore_backup. Before this fixrestore_backup()calledbackup_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. Everyufwcommand (default deny/allow, limit SSH, allow VPN port, route rule) on both branches (inactive and active) now checks the exit code. Accumulated errors causereturn 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 atlog_debug, invisible in normal output. Now it logs atlog_warnso developers can see that integrity was not verified.
Tests
- +7 new bats tests.
test_validate_endpoint.bats+4: reject999.999.999.999,256.1.1.1; accept255.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.shin step 5 now computessha256sumforawg_common.shandmanage_amneziawg.shright aftercurland 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 whenAWG_BRANCHis 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 viatar -tzfand 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. Plustar -xzf --no-same-ownerto 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/Jmaxvalues. This continues the Discussion #38 story — mobile carriers are sensitive to junk packet size. Lowered theJmaxoffset fromJmin+100..500toJmin+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_backuptar validation: happy path (good backup), absolute path rejection, path traversal rejection, server keychmod 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_filterlowered from1(strict) to2(loose). Strict mode broke routing on cloud hosters (Hetzner and similar) where the gateway is in a different subnet. Addedkernel.printk = 3 4 1 3to suppress kernel warning messages in the VNC console. Thanks @z036. --uninstallnow correctly removes UFW routing rules: addedout on <nic>to the delete command — UFW requires an exact match with the rule that was created during install.- Default
Jclowered from 4-8 to 3-6 (Discussion #38): mobile networks (LTE/5G) do not tolerate large amounts of junk packets well. @elvaleto confirmed thatJc=3works 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:
Jcrange4-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_rangesnow caps the upper bound at2^31-1 = 2147483647instead of the fulluint32. This matchesisValidHField()in amnezia-vpn/amneziawg-windows-client#85 (upstream bug, open since February 2026, not yet fixed). Implementation: a0x7FFFFFFFbit mask is applied to theod -N32 -tu4 /dev/urandomoutput, and the fallback path now usesrand_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-goaccepts the fulluint32, handshake with clients is not affected. The only inconvenience is thatamneziawg-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-platformamnezia-client(Qt, Android/iOS/Desktop) does not have this limit. - Upgrading from v5.8.0 is recommended if you use
amneziawg-windows-client: runsudo 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
.conffiles.
Tests
tests/test_h_ranges.batsupdated: upper-bound check changed from2^32-1to2^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-clientbug 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_paramswere 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 liveawg0.confexists, 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 whileregenwould issue clients a different set of J*/S*/H*. -
Atomic export in
load_awg_params_from_server_conf: The parser no longer exportsAWG_*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 whenawg0.confis partially corrupt. -
restore_backupforceschmod 600on restored server keys instead of inheriting the mode from the archive viacp -a. Protects against restoring keys with broken permissions if the backup was created with a bad umask. -
--uninstallno longer disables UFW globally (HIGH severity, audit). Previouslyufw --force disablewiped 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_installeronly 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 --yesruns could read the samesetup_state, race each other onapt-getand corrupt package state.flock -non$AWG_DIR/.install.lockis now taken at the start of main() for the entire process lifetime — a second instance getsdie "Another installer is already running". -
--endpointvalidation (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 newvalidate_endpoint()function rejects newlines, CR, quotes, backslashes, and requires FQDN / IPv4 /[IPv6]format.
Fixed
-
regendid not update AWG parameters in client configs (#38):load_awg_paramsonly read AWG parameters from the cached/root/awg/awgsetup_cfg.init, not from the live/etc/amnezia/amneziawg/awg0.conf. If a user manually editedawg0.conf(for example, to change obfuscation parameters),regenproduced client configs with stale values.load_awg_paramsnow reads the live server config first, with the init file used only as a bootstrap fallback on first install. Added new functionload_awg_params_from_server_conf. -
manage add/removeignoredapply_configexit 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 atsystemctl status, and sets_cmd_rc=1. -
check_expired_clientsleft peers on the live interface on apply failure (audit). If apply_config failed after expired peers were removed from state files, the peer vanished fromexpiry/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. -
--uninstallremoved/etc/fail2ban/jail.localby heuristic (audit). Previously the entire file was deleted if it containedbanaction = ufw— too broad a filter, could wipe an unrelatedjail.localwith custom jails. The removal block has been dropped entirely, leaving onlyrm -f /etc/fail2ban/jail.d/amneziawg.conf(our own artefact). -
check_serverdid not checkawg showexit code (audit). Could report "State OK" even whenawgitself crashed. The command is now captured and its exit code verified. -
backup_configs/restore_backupleaked temp directories on SIGINT (audit).mktemp -dwas used directly, while the_awg_cleanuptrap only removed files. A newmanage_mktempdirhelper registers the dir in an array and chains cleanup properly. -
add_peer_to_servernow takes an inner flock to protect against direct calls outsidegenerate_client(defense-in-depth, self-audit). The "caller must hold the lock" contract was fragile. -
check_expired_clientsvalidates the client name before using it in paths (defense-in-depth, self-audit). Previouslyname=$(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_confighas an explicitreturn 0on the success path — removes exit-code ambiguity fromexec {fd}>&-.
Optimizations
generate_awg_h_rangesdoes a single/dev/urandomread instead of 8rand_rangesubprocess calls.od -An -N32 -tu4 /dev/urandomreads 32 bytes = 8 uint32 values in one operation. Falls back torand_rangeif/dev/urandomis unavailable.
Tests
- 80 bats tests (+34 from the 5.7.12 baseline of 46 tests):
test_h_ranges.bats— 9 H1-H4 generation checkstest_load_awg_params.bats— 14 awg0.conf parser, init-file priority, split-brain prevention, atomic export, bootstrap path checkstest_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. Addedbackend = systemdandpython3-systemdpackage for Debian. Ubuntu continues usingbackend = auto.
5.7.11 — 2026-03-31
Fixed
- regen corrupts Address on Debian/mawk (#31):
\sin awk (PCRE extension) not supported by mawk. Replaced with[ \t]. Also replacedgrep -oPwith POSIX-compatiblesedfor private key extraction. - regen loses values after modify (#31): User settings (DNS, PersistentKeepalive, AllowedIPs) changed via
modifyare 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 -qPreplaced with POSIX-compatiblegrepin 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=restartoption formanage_amneziawg.sh. Switches to full service restart instead ofawg syncconf— bypasses upstream deadlock in amneziawg kernel module (amneziawg-linux-kernel-module#146). Persists viaAWG_APPLY_MODE=restartinawgsetup_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 syncconfis 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_configoverwroteawg0.conffrom scratch. Existing[Peer]blocks are now automatically restored from backup when step 6 re-runs. - Race condition when adding clients (TOCTOU):
get_next_client_ipandadd_peer_to_servernow execute in a single critical section (flockingenerate_client). Two paralleladdoperations can no longer pick the same IP. - Silent restore success on failure:
restore_backupnow returns non-zero exit code when file copy errors occur, instead of silently reporting success. - Config parser double quote support:
safe_load_confignow 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 defaultdeny (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 FORWARDtoiptables -I FORWARDto 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 -son each remove. - Timeout for awg syncconf (#27):
awg-quick stripandawg syncconfare now called withtimeout 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(wasJmin+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 +2instead of unreliable empty psk field check. - manage help without AWG:
helpand empty command show usage beforecheck_dependencies, allowing--helpwithout AWG installed. - help text: Installer help now lists all 4 supported OS (Ubuntu 24.04/25.10, Debian 12/13).
- manage --expires help: Added
4wformat to--expireshelp 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.0supports AWG 2.0 (previously incorrectly listed as AWG 1.x only). - Fixed APT format for Ubuntu 24.04: DEB822
.sources(was.list). - Fixed
restoreexample 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:
+500instead 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-customnow callsdie()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[0mleaking intolist --no-coloroutput. - 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
sourcewith a whitelist config parser inawg_common.sh— only permitted keys (AWG_, OS_, DISABLE_IPV6, etc.) are loaded from the file. Eliminates potential code injection viaawgsetup_cfg.init. - Supply chain pinning: Script download URLs are pinned to the version tag (
AWG_BRANCH=v${SCRIPT_VERSION}) instead ofmain. TheAWG_BRANCHvariable 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
modifyparameters — 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 -xto prevent race conditions during parallel invocations. - cron expiry env: Expiry cron job explicitly sets PATH and uses
--conf-dirfor correct operation in minimal cron environments. - log_warn for malformed expiry: Malformed expiry files are handled via
log_warninstead 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 hardcoding0.0.0.0/0instead 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.confinstead of overwritingjail.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
removeandmodifycommands — previously only worked foraddandregen. - exit code: Management script now returns proper error codes instead of unconditional
exit 0. - expiry cron path: Expiry cron job uses
$AWG_DIRinstead 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:
addandremovecommands now auto-apply changes viaawg syncconf— zero-downtime, no active connection drops (#19). - apply_config(): New function in
awg_common.sh— applies config viaawg syncconfwith 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— onlyip_forwardand 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
syncconfinstead 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:
statscommand — per-client traffic statistics (format_bytes via awk). - stats --json: Machine-readable JSON output for integration and monitoring.
- --expires:
--expires=DURATIONflag foradd— 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
.vpnurifiles 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 700aftermkdirfor the backups directory. - apt sources location: Apt sources backup moved to
$AWG_DIRinstead ofsources.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
-rflag to allread -pcalls (15 places) — prevents\from being interpreted as an escape character in user input. - curl timeout: Added
--max-time 60 --retry 2to 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 600when 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— replacedgrep ":PORT "withgrep -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 EXITfor automatic cleanup of installer temp files.
5.5 — 2026-03-02
Fixed
- uninstall: Uninstall proceeded without confirmation when
/dev/ttywas unavailable (pipe, cron, non-TTY SSH) due to defaultconfirm="yes". - uninstall: Kernel module
amneziawgremained loaded after uninstall — addedmodprobe -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[0mwas not suppressed with--no-color— fixedcolor_endinitialization. - step99: Duplicate "Cleaning apt…" message — removed extra
logcall beforecleanup_apt(). - step99: Lock file
setup_state.lockwas not removed after installation completed. - manage: Inconsistent spelling "удален"/"удалён" — standardized (RU only).
5.4 — 2026-03-02
Fixed
- step5:
manage_amneziawg.shdownload failure is now fatal (die), consistent withawg_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
/24mask (matches actual IP allocation logic). - Version: Removed stale
v5.1references in logs/diagnostics; introducedSCRIPT_VERSIONconstant.
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 -nchecks 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 statusoutput 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
#,&,/,\inmodify_client()— addedescape_sed()function for escaping. - CRITICAL: Race condition in
update_state()— added locking viaflock -x. - MEDIUM:
curlinget_server_public_ip()could receive HTML instead of IP — added-fflag (fail on error) and whitespace cleanup. - MEDIUM:
$RANDOMfallback inrand_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 -non 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.0check 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/v4branch.
Added
- AWG 2.0: Full protocol support — parameters H1-H4 (ranges), S1-S4, CPS (I1).
- Native generation: All keys and configs generated using Bash +
awgwith 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:
checkcommand 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.initduring 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
qrencodeinstead 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
qrencodedirectly (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).