[GH-ISSUE #2957] DNS servers set in app settings are silently ignored for imported (third-party) configs #1351

Open
opened 2026-10-13 20:40:49 +00:00 by bsakaguchi · 2 comments
Owner

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

Environment

  • AmneziaVPN 5.0.0.5 (4d28f81)
  • Ubuntu 26.04 LTS (x86_64)
  • Protocol: AmneziaWG
  • Server profile added by importing a .conf file (not configured by Amnezia itself)

Summary

For a server profile that was added by importing an external config file, the DNS servers configured in Settings → Connection → DNS servers are saved and displayed in the UI, but never applied to the connection. The interface keeps using the DNS values embedded in the imported config.

Crucially, the UI gives no indication of this: the settings screen keeps showing the values I entered, so there is no way to tell from the app that they are inert.

Steps to reproduce

  1. Add a server by importing an AmneziaWG .conf file whose [Interface] section contains e.g. DNS = 1.1.1.1, 8.8.8.8.
  2. Go to Settings → Connection → DNS servers, set Primary DNS to a different address (e.g. 10.8.0.1), save.
  3. Make sure "Use AmneziaDNS" is disabled.
  4. Connect, then inspect the resolver actually applied to the VPN interface:
    resolvectl status
    

Expected

The VPN interface uses the DNS server configured in the app (10.8.0.1), or — if that is not supported for imported configs — the UI makes clear that the setting does not apply to this profile.

Actual

The VPN interface uses the DNS from the imported config:

Link 20 (amn0)
    Current Scopes: DNS
Current DNS Server: 1.1.1.1
       DNS Servers: 1.1.1.1 8.8.8.8
        DNS Domain: ~.
     Default Route: yes

while the settings screen still displays 10.8.0.1 as Primary DNS.

Reconnecting, fully quitting and restarting the app, rebooting, disabling AmneziaDNS, and disabling site-based split tunneling all make no difference.

Routing is not the cause — the DNS server is reachable through the tunnel the whole time:

$ dig @10.8.0.1 example.com
;; ANSWER SECTION:
example.com.  276  IN  A  172.66.147.243
;; Query time: 40 msec
;; SERVER: 10.8.0.1#53(10.8.0.1) (UDP)

Root cause

In ~/.config/AmneziaVPN.ORG/AmneziaVPN.conf there are two independent places holding DNS configuration.

Global app settings — correctly updated from the UI:

primaryDns=10.8.0.1
secondaryDns=1.1.1.1
useAmneziaDns=false

And, inside serversList, the profile carries its own copy, plus the full config text verbatim:

{
  "description": "Server 1",
  "dns1": "1.1.1.1",
  "dns2": "8.8.8.8",
  "containers": [{
    "awg": {
      "isThirdPartyConfig": true,
      "last_config": "{... \"config\":\"[Interface]\\nAddress = ...\\nDNS = 1.1.1.1, 8.8.8.8\\n...\"}"
    }
  }]
}

Because the profile is flagged isThirdPartyConfig: true, the stored config text is handed to the daemon as-is, and the global DNS setting is never applied on top of it. The app-level DNS fields appear to be consumed only when Amnezia generates a config; for an imported profile there is nothing to regenerate, so the value is stored and then ignored.

Workaround

Edit the .conf file so its [Interface] section already contains the desired DNS = ..., then re-import it as a new connection. The embedded value is applied verbatim, so this works reliably.

Suggested fix

Either of:

  1. Apply the app-level DNS setting as an override for imported profiles too, or
  2. Make the UI reflect reality for such profiles — disable the DNS fields and show the effective values from the config, the way split tunneling already does with "Can't be disabled for current server".

Option 2 alone would prevent the confusing part: the current behaviour saves a setting, shows it back, and silently does nothing with it.

  • #2121 — DNS settings from app ignored (Windows, AmneziaWG)
  • #1591 — Custom DNS settings ignored (Windows)

Both report the same symptom without identifying a cause. Since every profile stores its own dns1/dns2 alongside the global setting, the same shadowing may explain those reports as well.

Originally created by @imhelle on GitHub (Aug 7, 2026). Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/2957 ## Environment - AmneziaVPN 5.0.0.5 (`4d28f81`) - Ubuntu 26.04 LTS (x86_64) - Protocol: AmneziaWG - Server profile added by importing a `.conf` file (not configured by Amnezia itself) ## Summary For a server profile that was added by importing an external config file, the DNS servers configured in **Settings → Connection → DNS servers** are saved and displayed in the UI, but never applied to the connection. The interface keeps using the DNS values embedded in the imported config. Crucially, the UI gives no indication of this: the settings screen keeps showing the values I entered, so there is no way to tell from the app that they are inert. ## Steps to reproduce 1. Add a server by importing an AmneziaWG `.conf` file whose `[Interface]` section contains e.g. `DNS = 1.1.1.1, 8.8.8.8`. 2. Go to Settings → Connection → DNS servers, set Primary DNS to a different address (e.g. `10.8.0.1`), save. 3. Make sure "Use AmneziaDNS" is disabled. 4. Connect, then inspect the resolver actually applied to the VPN interface: ``` resolvectl status ``` ## Expected The VPN interface uses the DNS server configured in the app (`10.8.0.1`), or — if that is not supported for imported configs — the UI makes clear that the setting does not apply to this profile. ## Actual The VPN interface uses the DNS from the imported config: ``` Link 20 (amn0) Current Scopes: DNS Current DNS Server: 1.1.1.1 DNS Servers: 1.1.1.1 8.8.8.8 DNS Domain: ~. Default Route: yes ``` while the settings screen still displays `10.8.0.1` as Primary DNS. Reconnecting, fully quitting and restarting the app, rebooting, disabling AmneziaDNS, and disabling site-based split tunneling all make no difference. Routing is not the cause — the DNS server is reachable through the tunnel the whole time: ``` $ dig @10.8.0.1 example.com ;; ANSWER SECTION: example.com. 276 IN A 172.66.147.243 ;; Query time: 40 msec ;; SERVER: 10.8.0.1#53(10.8.0.1) (UDP) ``` ## Root cause In `~/.config/AmneziaVPN.ORG/AmneziaVPN.conf` there are two independent places holding DNS configuration. Global app settings — correctly updated from the UI: ``` primaryDns=10.8.0.1 secondaryDns=1.1.1.1 useAmneziaDns=false ``` And, inside `serversList`, the profile carries its own copy, plus the full config text verbatim: ```json { "description": "Server 1", "dns1": "1.1.1.1", "dns2": "8.8.8.8", "containers": [{ "awg": { "isThirdPartyConfig": true, "last_config": "{... \"config\":\"[Interface]\\nAddress = ...\\nDNS = 1.1.1.1, 8.8.8.8\\n...\"}" } }] } ``` Because the profile is flagged `isThirdPartyConfig: true`, the stored config text is handed to the daemon as-is, and the global DNS setting is never applied on top of it. The app-level DNS fields appear to be consumed only when Amnezia *generates* a config; for an imported profile there is nothing to regenerate, so the value is stored and then ignored. ## Workaround Edit the `.conf` file so its `[Interface]` section already contains the desired `DNS = ...`, then re-import it as a new connection. The embedded value is applied verbatim, so this works reliably. ## Suggested fix Either of: 1. Apply the app-level DNS setting as an override for imported profiles too, or 2. Make the UI reflect reality for such profiles — disable the DNS fields and show the effective values from the config, the way split tunneling already does with *"Can't be disabled for current server"*. Option 2 alone would prevent the confusing part: the current behaviour saves a setting, shows it back, and silently does nothing with it. ## Possibly related - #2121 — DNS settings from app ignored (Windows, AmneziaWG) - #1591 — Custom DNS settings ignored (Windows) Both report the same symptom without identifying a cause. Since every profile stores its own `dns1`/`dns2` alongside the global setting, the same shadowing may explain those reports as well.
Author
Owner

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

Это давняя проблема, давно уже здесь неоднократно сообщалось. По крайней мере точно относится к Premium, т.к. в self hosted после настройки DNS будет работать тот, что установлен.

<!-- gh-comment-id:5218680181 --> @Vitalik93 commented on GitHub (Aug 7, 2026): Это давняя проблема, давно уже здесь неоднократно сообщалось. По крайней мере точно относится к Premium, т.к. в self hosted после настройки DNS будет работать тот, что установлен.
Author
Owner

@HeisLuka commented on GitHub (Sep 9, 2026):

I traced this through the current dev source. The issue is deterministic, but the direct Linux desktop cause is slightly different from the original hypothesis.

The imported literal DNS = ... line is not directly winning at the daemon boundary. The DNS values are first promoted into profile state during import, and that profile state has explicit precedence over the global DNS setting.

Current path

ImportController::extractWireGuardConfig() does both of these things:

  • preserves the original native config in lastConfig[configKey::config];
  • extracts DNS = ... from the imported config into top-level profile dns1 / dns2.

It also marks the imported WG/AWG profile as:

wireguardConfig[configKey::isThirdPartyConfig] = true;

Then NativeServerConfig::getDnsPair() explicitly prefers those profile values:

QPair<QString, QString> NativeServerConfig::getDnsPair(
    const QString &primaryDns,
    const QString &secondaryDns) const
{
    QString d1 = dns1;
    QString d2 = dns2;

    if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
        d1 = primaryDns;
    }
    if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
        d2 = secondaryDns;
    }

    return { d1, d2 };
}

So for an imported profile containing valid DNS addresses, the app-level primaryDns / secondaryDns are only fallbacks.

ConnectionController::prepareConnection() reads the global DNS settings, but for a Native profile calls:

dns = cfg->getDnsPair(primaryDns, secondaryDns);

and createConnectionConfiguration() then writes that already-resolved pair to:

vpnConfiguration[configKey::dns1] = dns.first;
vpnConfiguration[configKey::dns2] = dns.second;

Finally, desktop WireGuard/AWG LocalSocketController::activate() sends those top-level values to the daemon:

json.insert("primaryDnsServer",
            rawConfig.value(amnezia::configKey::dns1));

json.insert("secondaryDnsServer",
            rawConfig.value(amnezia::configKey::dns2));

So the effective precedence today is:

valid imported/profile DNS
        ↓
NativeServerConfig::getDnsPair()
        ↓
global app DNS only as fallback
        ↓
ConnectionController
        ↓
LocalSocketController
        ↓
daemon resolver

That explains why changing Settings → DNS is saved correctly in the UI but has no effect on this connection.

Suggested fix

I think the real issue is that DNS source/precedence is implicit.

There are two reasonable product policies.

A. Custom app DNS is allowed to override imported DNS

In that case I would make DNS source explicit, e.g. conceptually:

enum class DnsSource {
    Profile,
    Custom,
    AmneziaDns
};

and resolve one effective DNS pair based on that state.

When DnsSource::Custom is selected, the app DNS should override the profile DNS. Otherwise an imported profile can keep its embedded DNS.

I would not simply reverse NativeServerConfig::getDnsPair() and always prefer primaryDns / secondaryDns.

Those app settings are always populated with defaults (1.1.1.1 / 1.0.0.1), so doing that would silently replace embedded DNS for every imported profile even when the user never explicitly requested an override.

There needs to be a distinction between:

"default global DNS values exist"

and:

"user explicitly selected custom DNS for connections"

B. Imported profile DNS is intentionally authoritative

Then the current routing/runtime behavior can remain, but the UI should reflect it.

PageSettingsDns.qml currently disables custom DNS for API-backed servers, but not for imported/native profiles.

For a third-party profile with valid embedded DNS, the page could either:

  • disable the DNS controls and show that DNS comes from the imported profile, or
  • show the effective profile DNS values and explain that global DNS does not override this profile.

That would eliminate the current saved-but-inert setting state.

Native config consistency

There is also a secondary reconciliation issue.

ConfiguratorBase::applyDnsToNativeConfig() currently only does:

config.replace("$PRIMARY_DNS", dns.primaryDns);
config.replace("$SECONDARY_DNS", dns.secondaryDns);

So an imported WG/AWG config containing a literal:

DNS = 1.1.1.1, 8.8.8.8

is never rewritten.

This is not the direct Linux desktop cause above, because the desktop daemon receives top-level dns1/dns2, but if custom DNS override is supported, the native representation should probably be reconciled as well so different consumers/export paths do not observe different DNS configuration.

Regression cases

I would cover at least:

  • imported WG/AWG with embedded DNS + explicit custom-DNS mode → effective connection DNS is the custom value;
  • imported profile + untouched global defaults → embedded DNS remains intact if profile DNS is the default policy;
  • imported profile without DNS → app DNS is used;
  • only one embedded DNS value → define explicitly whether profile/app sources may be mixed;
  • WG and AWG use identical precedence rules;
  • DNS settings UI displays the same source that prepareConnection() actually uses;
  • if app override is enabled, native [Interface] DNS = ... does not retain a contradictory value.

So I think the underlying fix should be: resolve one explicit effective DNS source, and make both the UI and runtime use that same resolved state, rather than keeping profile DNS and global DNS as competing values with implicit precedence.

<!-- gh-comment-id:5607047450 --> @HeisLuka commented on GitHub (Sep 9, 2026): I traced this through the current `dev` source. The issue is deterministic, but the direct Linux desktop cause is slightly different from the original hypothesis. The imported literal `DNS = ...` line is not directly winning at the daemon boundary. The DNS values are first promoted into profile state during import, and that profile state has explicit precedence over the global DNS setting. ### Current path `ImportController::extractWireGuardConfig()` does both of these things: * preserves the original native config in `lastConfig[configKey::config]`; * extracts `DNS = ...` from the imported config into top-level profile `dns1` / `dns2`. It also marks the imported WG/AWG profile as: ```cpp wireguardConfig[configKey::isThirdPartyConfig] = true; ``` Then `NativeServerConfig::getDnsPair()` explicitly prefers those profile values: ```cpp QPair<QString, QString> NativeServerConfig::getDnsPair( const QString &primaryDns, const QString &secondaryDns) const { QString d1 = dns1; QString d2 = dns2; if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) { d1 = primaryDns; } if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) { d2 = secondaryDns; } return { d1, d2 }; } ``` So for an imported profile containing valid DNS addresses, the app-level `primaryDns` / `secondaryDns` are only fallbacks. `ConnectionController::prepareConnection()` reads the global DNS settings, but for a Native profile calls: ```cpp dns = cfg->getDnsPair(primaryDns, secondaryDns); ``` and `createConnectionConfiguration()` then writes that already-resolved pair to: ```cpp vpnConfiguration[configKey::dns1] = dns.first; vpnConfiguration[configKey::dns2] = dns.second; ``` Finally, desktop WireGuard/AWG `LocalSocketController::activate()` sends those top-level values to the daemon: ```cpp json.insert("primaryDnsServer", rawConfig.value(amnezia::configKey::dns1)); json.insert("secondaryDnsServer", rawConfig.value(amnezia::configKey::dns2)); ``` So the effective precedence today is: ```text valid imported/profile DNS ↓ NativeServerConfig::getDnsPair() ↓ global app DNS only as fallback ↓ ConnectionController ↓ LocalSocketController ↓ daemon resolver ``` That explains why changing Settings → DNS is saved correctly in the UI but has no effect on this connection. ### Suggested fix I think the real issue is that DNS source/precedence is implicit. There are two reasonable product policies. #### A. Custom app DNS is allowed to override imported DNS In that case I would make DNS source explicit, e.g. conceptually: ```cpp enum class DnsSource { Profile, Custom, AmneziaDns }; ``` and resolve one effective DNS pair based on that state. When `DnsSource::Custom` is selected, the app DNS should override the profile DNS. Otherwise an imported profile can keep its embedded DNS. I would **not** simply reverse `NativeServerConfig::getDnsPair()` and always prefer `primaryDns` / `secondaryDns`. Those app settings are always populated with defaults (`1.1.1.1` / `1.0.0.1`), so doing that would silently replace embedded DNS for every imported profile even when the user never explicitly requested an override. There needs to be a distinction between: ```text "default global DNS values exist" ``` and: ```text "user explicitly selected custom DNS for connections" ``` #### B. Imported profile DNS is intentionally authoritative Then the current routing/runtime behavior can remain, but the UI should reflect it. `PageSettingsDns.qml` currently disables custom DNS for API-backed servers, but not for imported/native profiles. For a third-party profile with valid embedded DNS, the page could either: * disable the DNS controls and show that DNS comes from the imported profile, or * show the effective profile DNS values and explain that global DNS does not override this profile. That would eliminate the current saved-but-inert setting state. ### Native config consistency There is also a secondary reconciliation issue. `ConfiguratorBase::applyDnsToNativeConfig()` currently only does: ```cpp config.replace("$PRIMARY_DNS", dns.primaryDns); config.replace("$SECONDARY_DNS", dns.secondaryDns); ``` So an imported WG/AWG config containing a literal: ```ini DNS = 1.1.1.1, 8.8.8.8 ``` is never rewritten. This is not the direct Linux desktop cause above, because the desktop daemon receives top-level `dns1/dns2`, but if custom DNS override is supported, the native representation should probably be reconciled as well so different consumers/export paths do not observe different DNS configuration. ### Regression cases I would cover at least: * imported WG/AWG with embedded DNS + explicit custom-DNS mode → effective connection DNS is the custom value; * imported profile + untouched global defaults → embedded DNS remains intact if profile DNS is the default policy; * imported profile without DNS → app DNS is used; * only one embedded DNS value → define explicitly whether profile/app sources may be mixed; * WG and AWG use identical precedence rules; * DNS settings UI displays the same source that `prepareConnection()` actually uses; * if app override is enabled, native `[Interface] DNS = ...` does not retain a contradictory value. So I think the underlying fix should be: **resolve one explicit effective DNS source, and make both the UI and runtime use that same resolved state**, rather than keeping profile DNS and global DNS as competing values with implicit precedence.
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#1351
No description provided.