[GH-ISSUE #2942] AWG 3.0 parameters are silently dropped when importing a .conf file — connection hangs on "Connecting…" #1344

Closed
opened 2026-10-13 20:40:35 +00:00 by bsakaguchi · 12 comments
Owner

Originally created by @uselessfire on GitHub (Aug 5, 2026).
Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/2942

Describe the bug

Importing a self-hosted AmneziaWG 3.0 config from a .conf file silently drops all AWG 3.0 parameters (HeaderProtectionKey, ContentPaddingAddition, RekeyAfterTime, RekeyTimeout, RejectAfterTime, KeepaliveTimeout, MaxHandshakeAttempts).

The import itself reports success and the profile appears in the list, but the connection then hangs on "Connecting…" forever. Because HeaderProtectionKey must match on both sides — a peer without it cannot complete a handshake at all — the tunnel never comes up.

The same configuration imported via a vpn:// link (where the key is a separate JSON field) connects normally, which isolates the problem to the .conf text parser.

To Reproduce

  1. Set up a self-hosted AmneziaWG 3.0 server with header protection enabled, e.g. /etc/amnezia/amneziawg/awg0.conf:
[Interface]
PrivateKey = <server key>
Address = 10.9.9.1/24
ListenPort = 443
HeaderProtectionKey = qOZ8vN2mK4pL7wR1tY6uI3oP5aS9dF0gH8jK2lM4nB0=
Jc = 4
Jmin = 35
Jmax = 95
S1 = 146
S2 = 48
S3 = 22
S4 = 26
H1 = 148736594-370455131
H2 = 621025620-1240228083
H3 = 1504827942-1530367889
H4 = 1629521638-1833671031
  1. Generate a client .conf containing the same HeaderProtectionKey:
[Interface]
Address = 10.9.9.2/32
PrivateKey = <client key>
DNS = 1.1.1.1
MTU = 1280
Jc = 4
Jmin = 35
Jmax = 95
S1 = 146
S2 = 48
S3 = 22
S4 = 26
H1 = 148736594-370455131
H2 = 621025620-1240228083
H3 = 1504827942-1530367889
H4 = 1629521638-1833671031
HeaderProtectionKey = qOZ8vN2mK4pL7wR1tY6uI3oP5aS9dF0gH8jK2lM4nB0=

[Peer]
PublicKey = <server pubkey>
AllowedIPs = 0.0.0.0/0
Endpoint = example.com:443
PersistentKeepalive = 33
  1. In the app: + → Add VPN → Import from file, select that .conf. Import succeeds.
  2. Tap Connect.
  3. The UI stays on "Connecting…" indefinitely. No error, no timeout.

Expected behavior

AWG 3.0 parameters should be parsed from the .conf file the same way 2.0 parameters (Jc, Jmin, Jmax, S1–S4, H1–H4, I1–I5) already are, and reach the tunnel — so that a .conf import behaves identically to the equivalent vpn:// import.

Log files

Nothing relevant is logged. The client keeps sending handshake initiations; the server drops them silently because it cannot decrypt the protected header. On the server side this is visible as a peer whose rx counter grows while latest handshake stays at 0:

$ awg show awg0 dump | tail -n +2 | awk '{print $1, "handshake:", $5, "rx:", $6}'
<peer pubkey> handshake: 0 rx: 4832

That combination (traffic arriving, handshake never completing) is what distinguishes this from a blocked port or a wrong endpoint.

Screenshots

Not applicable — the UI shows only the standard "Connecting…" spinner.

Desktop (please complete the following information):

  • OS: Arch Linux (kernel 7.0.12-zen1), package amneziavpn-bin from AUR
  • Version: 5.0.0.5

Smartphone (please complete the following information):

  • Not tested; the parser involved is shared, so Android/iOS are expected to behave the same.

Server (please complete the following information):

  • OS: Ubuntu Server 26.04 LTS
  • Kernel module: amneziawg 3.0.20260731-04 (DKMS, ppa:amnezia/ppa)
  • Tools: amneziawg-tools v3.0.20260730

Additional context

Root cause. ImportController::extractWireGuardConfig() parses the .conf text against two hard-coded field lists (core/controllers/selfhosted/importController.cpp, around lines 578–588):

const QStringList requiredJunkFields = { configKey::junkPacketCount,      configKey::junkPacketMinSize,
                                         configKey::junkPacketMaxSize,    configKey::initPacketJunkSize,
                                         configKey::responsePacketJunkSize, ... };

const QStringList optionalJunkFields = { configKey::cookieReplyPacketJunkSize,
                                         configKey::transportPacketJunkSize,
                                         configKey::specialJunk1, ... configKey::specialJunk5 };

Both lists contain only 2.0 parameters. Searching the whole file for HeaderProtection, RekeyAfterTime or ContentPadding returns zero matches (checked against tag 5.0.0.5).

Why the value cannot be recovered later. The rest of the pipeline reads these parameters as separate JSON fields, not from the config text:

Step What happens File
1 AwgClientConfig::fromJson does json.value(configKey::headerProtectionKey) — the key is absent, so the field stays empty core/models/protocols/awgProtocolConfig.cpp
2 processConfigWithLocalSettings only substitutes $PRIMARY_DNS / $SECONDARY_DNS core/configurators/configuratorBase.cpp
3 AwgClientConfig::toJson writes the key under if (!headerProtectionKey.isEmpty()) — condition is false same file, ~line 152
4 awg_config_data therefore reaches the backend without the key core/controllers/connectionController.cpp
5 Android: configData.optStringOrNull("HeaderProtectionKey") → null, so setHeaderProtectionKey() is never called android/wireguard/.../Wireguard.kt

The raw config text does survive as nativeConfig, but only AllowedIPs and PersistentKeepalive are ever read back out of it (vpnConnection.cpp), so the 3.0 keys are lost for good.

This looks like a missed branch rather than a missing feature. The client already:

  • declares all seven constants — core/utils/constants/configKeys.h, lines 93–99;
  • applies them — daemon.cpp, platforms/linux/daemon/wireguardutilslinux.cpp, platforms/macos/daemon/wireguardutilsmacos.cpp, Wireguard.kt;
  • receives them from the subscription API — core/controllers/api/subscriptionController.cpp.

Only the self-hosted .conf path was not updated when 3.0 landed, which is exactly the path self-hosted users depend on.

Suggested fix. Add the seven keys to optionalJunkFields (they are optional by nature — a 2.0 config simply has none of them), and copy them into lastConfig in the same loop that already handles the existing optional fields:

const QStringList optionalJunkFields = { configKey::cookieReplyPacketJunkSize,
                                         configKey::transportPacketJunkSize,
                                         configKey::specialJunk1, ... configKey::specialJunk5,
                                         // AWG 3.0
                                         configKey::headerProtectionKey,
                                         configKey::contentPaddingAddition,
                                         configKey::rekeyAfterTime,
                                         configKey::rekeyTimeout,
                                         configKey::rejectAfterTime,
                                         configKey::keepaliveTimeout,
                                         configKey::maxHandshakeAttempts };

Workaround for anyone hitting this. Distribute configs as vpn:// links instead of .conf files. A vpn:// payload carries these parameters as separate JSON fields inside last_config, so fromJson picks them up and the connection works. Note that the QR code generated from the .conf text fails in the same way — only the QR of the vpn:// link works.

Originally created by @uselessfire on GitHub (Aug 5, 2026). Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/2942 **Describe the bug** Importing a self-hosted AmneziaWG **3.0** config from a `.conf` file silently drops all AWG 3.0 parameters (`HeaderProtectionKey`, `ContentPaddingAddition`, `RekeyAfterTime`, `RekeyTimeout`, `RejectAfterTime`, `KeepaliveTimeout`, `MaxHandshakeAttempts`). The import itself reports success and the profile appears in the list, but the connection then hangs on **"Connecting…" forever**. Because `HeaderProtectionKey` must match on both sides — a peer without it cannot complete a handshake at all — the tunnel never comes up. The same configuration imported via a `vpn://` link (where the key is a separate JSON field) connects normally, which isolates the problem to the `.conf` text parser. **To Reproduce** 1. Set up a self-hosted AmneziaWG 3.0 server with header protection enabled, e.g. `/etc/amnezia/amneziawg/awg0.conf`: ```ini [Interface] PrivateKey = <server key> Address = 10.9.9.1/24 ListenPort = 443 HeaderProtectionKey = qOZ8vN2mK4pL7wR1tY6uI3oP5aS9dF0gH8jK2lM4nB0= Jc = 4 Jmin = 35 Jmax = 95 S1 = 146 S2 = 48 S3 = 22 S4 = 26 H1 = 148736594-370455131 H2 = 621025620-1240228083 H3 = 1504827942-1530367889 H4 = 1629521638-1833671031 ``` 2. Generate a client `.conf` containing the same `HeaderProtectionKey`: ```ini [Interface] Address = 10.9.9.2/32 PrivateKey = <client key> DNS = 1.1.1.1 MTU = 1280 Jc = 4 Jmin = 35 Jmax = 95 S1 = 146 S2 = 48 S3 = 22 S4 = 26 H1 = 148736594-370455131 H2 = 621025620-1240228083 H3 = 1504827942-1530367889 H4 = 1629521638-1833671031 HeaderProtectionKey = qOZ8vN2mK4pL7wR1tY6uI3oP5aS9dF0gH8jK2lM4nB0= [Peer] PublicKey = <server pubkey> AllowedIPs = 0.0.0.0/0 Endpoint = example.com:443 PersistentKeepalive = 33 ``` 3. In the app: **+ → Add VPN → Import from file**, select that `.conf`. Import succeeds. 4. Tap Connect. 5. The UI stays on **"Connecting…"** indefinitely. No error, no timeout. **Expected behavior** AWG 3.0 parameters should be parsed from the `.conf` file the same way 2.0 parameters (`Jc`, `Jmin`, `Jmax`, `S1`–`S4`, `H1`–`H4`, `I1`–`I5`) already are, and reach the tunnel — so that a `.conf` import behaves identically to the equivalent `vpn://` import. **Log files** Nothing relevant is logged. The client keeps sending handshake initiations; the server drops them silently because it cannot decrypt the protected header. On the server side this is visible as a peer whose `rx` counter grows while `latest handshake` stays at `0`: ``` $ awg show awg0 dump | tail -n +2 | awk '{print $1, "handshake:", $5, "rx:", $6}' <peer pubkey> handshake: 0 rx: 4832 ``` That combination (traffic arriving, handshake never completing) is what distinguishes this from a blocked port or a wrong endpoint. **Screenshots** Not applicable — the UI shows only the standard "Connecting…" spinner. **Desktop (please complete the following information):** - OS: Arch Linux (kernel 7.0.12-zen1), package `amneziavpn-bin` from AUR - Version: 5.0.0.5 **Smartphone (please complete the following information):** - Not tested; the parser involved is shared, so Android/iOS are expected to behave the same. **Server (please complete the following information):** - OS: Ubuntu Server 26.04 LTS - Kernel module: `amneziawg` 3.0.20260731-04 (DKMS, `ppa:amnezia/ppa`) - Tools: `amneziawg-tools` v3.0.20260730 **Additional context** **Root cause.** `ImportController::extractWireGuardConfig()` parses the `.conf` text against two hard-coded field lists (`core/controllers/selfhosted/importController.cpp`, around lines 578–588): ```cpp const QStringList requiredJunkFields = { configKey::junkPacketCount, configKey::junkPacketMinSize, configKey::junkPacketMaxSize, configKey::initPacketJunkSize, configKey::responsePacketJunkSize, ... }; const QStringList optionalJunkFields = { configKey::cookieReplyPacketJunkSize, configKey::transportPacketJunkSize, configKey::specialJunk1, ... configKey::specialJunk5 }; ``` Both lists contain only 2.0 parameters. Searching the whole file for `HeaderProtection`, `RekeyAfterTime` or `ContentPadding` returns **zero** matches (checked against tag `5.0.0.5`). **Why the value cannot be recovered later.** The rest of the pipeline reads these parameters as *separate JSON fields*, not from the config text: | Step | What happens | File | |---|---|---| | 1 | `AwgClientConfig::fromJson` does `json.value(configKey::headerProtectionKey)` — the key is absent, so the field stays empty | `core/models/protocols/awgProtocolConfig.cpp` | | 2 | `processConfigWithLocalSettings` only substitutes `$PRIMARY_DNS` / `$SECONDARY_DNS` | `core/configurators/configuratorBase.cpp` | | 3 | `AwgClientConfig::toJson` writes the key under `if (!headerProtectionKey.isEmpty())` — condition is false | same file, ~line 152 | | 4 | `awg_config_data` therefore reaches the backend without the key | `core/controllers/connectionController.cpp` | | 5 | Android: `configData.optStringOrNull("HeaderProtectionKey")` → `null`, so `setHeaderProtectionKey()` is never called | `android/wireguard/.../Wireguard.kt` | The raw config text does survive as `nativeConfig`, but only `AllowedIPs` and `PersistentKeepalive` are ever read back out of it (`vpnConnection.cpp`), so the 3.0 keys are lost for good. **This looks like a missed branch rather than a missing feature.** The client already: - declares all seven constants — `core/utils/constants/configKeys.h`, lines 93–99; - applies them — `daemon.cpp`, `platforms/linux/daemon/wireguardutilslinux.cpp`, `platforms/macos/daemon/wireguardutilsmacos.cpp`, `Wireguard.kt`; - receives them from the subscription API — `core/controllers/api/subscriptionController.cpp`. Only the self-hosted `.conf` path was not updated when 3.0 landed, which is exactly the path self-hosted users depend on. **Suggested fix.** Add the seven keys to `optionalJunkFields` (they are optional by nature — a 2.0 config simply has none of them), and copy them into `lastConfig` in the same loop that already handles the existing optional fields: ```cpp const QStringList optionalJunkFields = { configKey::cookieReplyPacketJunkSize, configKey::transportPacketJunkSize, configKey::specialJunk1, ... configKey::specialJunk5, // AWG 3.0 configKey::headerProtectionKey, configKey::contentPaddingAddition, configKey::rekeyAfterTime, configKey::rekeyTimeout, configKey::rejectAfterTime, configKey::keepaliveTimeout, configKey::maxHandshakeAttempts }; ``` **Workaround for anyone hitting this.** Distribute configs as `vpn://` links instead of `.conf` files. A `vpn://` payload carries these parameters as separate JSON fields inside `last_config`, so `fromJson` picks them up and the connection works. Note that the QR code generated from the `.conf` *text* fails in the same way — only the QR of the `vpn://` link works.
Author
Owner

@Vitalik93 commented on GitHub (Aug 5, 2026):

Сейчас вообще импорт конфигураций вроде отключили, может быть поэтому не работает. И насчет 3.0 - насколько слышал он сейчас работает только в Premium подписке.

<!-- gh-comment-id:5193329401 --> @Vitalik93 commented on GitHub (Aug 5, 2026): Сейчас вообще импорт конфигураций вроде отключили, может быть поэтому не работает. И насчет 3.0 - насколько слышал он сейчас работает только в Premium подписке.
Author
Owner

@uselessfire commented on GitHub (Aug 6, 2026):

Уже есть поддержка в модуле ядра и userspace модуле. Я себе развернул уже на self-hosted, но .conf не импортируются. В amneziawg приложениях ещё нет

<!-- gh-comment-id:5201367995 --> @uselessfire commented on GitHub (Aug 6, 2026): Уже есть поддержка в модуле ядра и userspace модуле. Я себе развернул уже на self-hosted, но .conf не импортируются. В amneziawg приложениях ещё нет
Author
Owner

@TheSviT commented on GitHub (Aug 7, 2026):

Подтверждаю что залочено программой, я спокойно 2 ядра между собой соединил на 3 версии и все работает. Можете поделиться vpn:// ссылкой чтобы я смог опробовать на мобиле погонять.

<!-- gh-comment-id:5216021087 --> @TheSviT commented on GitHub (Aug 7, 2026): Подтверждаю что залочено программой, я спокойно 2 ядра между собой соединил на 3 версии и все работает. Можете поделиться vpn:// ссылкой чтобы я смог опробовать на мобиле погонять.
Author
Owner

@TheSviT commented on GitHub (Aug 7, 2026):

На вайбкодил генератор и добавил в телефон, работает, интересно по наблюдать что это даст в плане блокировок.

<!-- gh-comment-id:5216250117 --> @TheSviT commented on GitHub (Aug 7, 2026): На вайбкодил генератор и добавил в телефон, работает, интересно по наблюдать что это даст в плане блокировок.
Author
Owner

@arshammi commented on GitHub (Aug 9, 2026):

Confirming on AmneziaVPN 5.0.0.5 with a third-party self-hosted AmneziaWG 3.0 .conf (HeaderProtectionKey present).

Same A/B:

  • with AWG 3.0 fields → import OK, connect hangs / no handshake
  • clear 3.0 fields, keep 2.0 obfuscation → connects

Server awg show: header protection enabled; peer rx grows, latest handshake stays 0.

<!-- gh-comment-id:5232336073 --> @arshammi commented on GitHub (Aug 9, 2026): Confirming on AmneziaVPN 5.0.0.5 with a third-party self-hosted AmneziaWG 3.0 `.conf` (HeaderProtectionKey present). Same A/B: - with AWG 3.0 fields → import OK, connect hangs / no handshake - clear 3.0 fields, keep 2.0 obfuscation → connects Server `awg show`: header protection enabled; peer rx grows, latest handshake stays 0.
Author
Owner

@openncomp commented on GitHub (Aug 10, 2026):

На вайбкодил генератор и добавил в телефон, работает, интересно по наблюдать что это даст в плане блокировок.

Та же проблема.
Поделись генератором, хочу тоже проверить, у меня все безуспешно.

<!-- gh-comment-id:5237877518 --> @openncomp commented on GitHub (Aug 10, 2026): > На вайбкодил генератор и добавил в телефон, работает, интересно по наблюдать что это даст в плане блокировок. Та же проблема. Поделись генератором, хочу тоже проверить, у меня все безуспешно.
Author
Owner

@maffinca69 commented on GitHub (Aug 14, 2026):

Any solutions?

<!-- gh-comment-id:5298885638 --> @maffinca69 commented on GitHub (Aug 14, 2026): Any solutions?
Author
Owner

@Vitalik93 commented on GitHub (Aug 15, 2026):

Из описания версии 3.0, как я понял, там такие изменения, что нужно сервер под него полировать, чтобы заработал. Поэтому пока он пашет только на Premium, то есть на собственных серверах Amnezia, которые они сами и настраивают и обслуживают. Надеюсь в будущем конечно заработает и на своем. Хотя бы на их Amnezia Hosting.

<!-- gh-comment-id:5299363348 --> @Vitalik93 commented on GitHub (Aug 15, 2026): Из описания версии 3.0, как я понял, там такие изменения, что нужно сервер под него полировать, чтобы заработал. Поэтому пока он пашет только на Premium, то есть на собственных серверах Amnezia, которые они сами и настраивают и обслуживают. Надеюсь в будущем конечно заработает и на своем. Хотя бы на их Amnezia Hosting.
Author
Owner

@arshammi commented on GitHub (Aug 15, 2026):

I built a generator that creates vpn:// links from .conf files, and the links work in the Amnezia app perfectly.

https://github.com/arshammi/awg-vpn-uri

<!-- gh-comment-id:5302880824 --> @arshammi commented on GitHub (Aug 15, 2026): I built a generator that creates vpn:// links from .conf files, and the links work in the Amnezia app perfectly. https://github.com/arshammi/awg-vpn-uri
Author
Owner

@maffinca69 commented on GitHub (Aug 15, 2026):

I built a generator that creates vpn:// links from .conf files, and the links work in the Amnezia app perfectly.

https://github.com/arshammi/awg-vpn-uri

It's working. Thanks

<!-- gh-comment-id:5304023345 --> @maffinca69 commented on GitHub (Aug 15, 2026): > I built a generator that creates vpn:// links from .conf files, and the links work in the Amnezia app perfectly. > > https://github.com/arshammi/awg-vpn-uri It's working. Thanks
Author
Owner

@uselessfire commented on GitHub (Aug 19, 2026):

Fixed in dev branch: github.com/amnezia-vpn/amnezia-client@2ad6721061
Waiting for release

<!-- gh-comment-id:5345222028 --> @uselessfire commented on GitHub (Aug 19, 2026): Fixed in dev branch: https://github.com/amnezia-vpn/amnezia-client/commit/2ad67210612c42f4b4ded456c92b27415c16367c Waiting for release
Author
Owner

@uselessfire commented on GitHub (Aug 28, 2026):

fixed in 5.0.1.5

<!-- gh-comment-id:5457897037 --> @uselessfire commented on GitHub (Aug 28, 2026): fixed in 5.0.1.5
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amnezia-vpn/amnezia-client#1344
No description provided.