How Does amnezia-client Handle DNS Resolution? A Deep Dive into the Cross-Platform VPN DNS Architecture

amnezia-client isolates DNS traffic inside the VPN tunnel by routing all queries through platform-specific resolver APIs, then restores the system DNS configuration when the tunnel disconnects.

DNS resolution in the amnezia-vpn/amnezia-client follows a strict lifecycle: flush cached entries, push VPN-specific resolvers, enforce traffic isolation via firewall rules, and finally restore the original system state. The implementation spans a layered architecture that abstracts operating system differences behind a common Router façade while delegating actual resolver manipulation to platform-specific DnsUtils classes.

Architecture Overview: Router and DnsUtils Layers

The DNS handling strategy splits responsibilities between two layers defined in service/server/router.cpp and client/daemon/dnsutils.h.

  • Router – Acts as a high-level façade that forwards DNS operations (flushDns(), updateResolvers(), restoreResolvers()) to OS-specific implementations.
  • DnsUtils – Directly interfaces with the system resolver (systemd-resolved, mDNSResponder, or Win32 DNS API) to modify DNS server assignments for the VPN interface.

This separation allows VpnConnection in client/vpnConnection.cpp to orchestrate tunnel state changes without managing platform-specific quirks.

Flushing the DNS Cache

When the VPN disconnects, amnezia-client clears the OS DNS cache to prevent stale entries from persisting after the tunnel closes.

Linux DNS Cache Flushing (nscd and systemd-resolved)

On Linux, RouterLinux::flushDns() in service/server/router_linux.cpp (lines 63-78) detects which caching service is active. If nscd.service is running, it restarts that service; otherwise, it falls back to restarting systemd-resolved.service.

// service/server/router_linux.cpp
bool RouterLinux::flushDns()
{
    QProcess p;
    p.setProcessChannelMode(QProcess::MergedChannels);

    if (isServiceActive("nscd.service")) {
        p.start("systemctl", { "restart", "nscd" });
    } else if (isServiceActive("systemd-resolved.service")) {
        p.start("systemctl", { "restart", "systemd-resolved" });
    } else {
        return false; // No known DNS cache manager
    }
    p.waitForFinished();
    return true;
}

macOS DNS Cache Flushing (mDNSResponder)

On macOS, RouterMac::flushDns() in service/server/router_mac.cpp (lines 62-73) sends a SIGHUP to mDNSResponder, forcing the daemon to reread /etc/resolv.conf.

// service/server/router_mac.cpp
bool RouterMac::flushDns()
{
    QProcess p;
    p.start("killall", QStringList() << "-HUP" << "mDNSResponder");
    p.waitForFinished();
    return true;
}

Windows DNS Cache Flushing

Windows delegates to RouterWin::flushDns() in service/server/router_win.cpp, which invokes the Windows DNS API to clear the resolver cache.

Updating DNS Servers for the VPN Interface

When the tunnel activates, amnezia-client pushes the desired DNS servers—either the default AmneziaDNS or custom user-defined resolvers—to the OS and blocks external DNS queries through the kill-switch.

Linux Implementation (systemd-resolve D-Bus API)

DnsUtilsLinux in client/platforms/linux/daemon/dnsutilslinux.cpp uses the systemd-resolved D-Bus interface. The updateResolvers() method (lines 53-64) obtains the interface index via if_nametoindex(), then calls three helpers:

  1. setLinkDNS() – Sets DNS addresses via the SetLinkDNS D-Bus method (lines 98-122).
  2. setLinkDefaultRoute() – Marks the interface as the default route using SetLinkDefaultRoute (lines 146-156).
  3. updateLinkDomains() – Preserves search domains via SetLinkDomains (lines 158-174).
// client/platforms/linux/daemon/dnsutilslinux.cpp
bool DnsUtilsLinux::updateResolvers(const QString& ifname,
                                    const QList<QHostAddress>& resolvers)
{
    m_ifindex = if_nametoindex(qPrintable(ifname));
    if (m_ifindex <= 0) return false;

    setLinkDNS(m_ifindex, resolvers);            // Set DNS addresses
    setLinkDefaultRoute(m_ifindex, true);       // Make them the default route
    updateLinkDomains();                        // Preserve search domains
    return true;
}

All three platforms expose the identical public interface bool updateResolvers(const QString&, const QList<QHostAddress>&), called from Router::updateResolvers() in service/server/router.cpp (lines 92-100).

macOS Implementation (scutil)

DnsUtilsMacos in client/platforms/macos/daemon/dnsutilsmacos.cpp interfaces with the macOS system resolver using the scutil command-line utility. It saves the current DNS configuration before overwriting it, enabling clean restoration later.

Windows Implementation (Win32 DNS API)

DnsUtilsWindows in client/platforms/windows/daemon/dnsutilswindows.cpp utilizes the SetAdapterDnsAddresses Win32 API to configure DNS servers for the VPN adapter.

Restoring Original DNS Configuration

When the VPN stops, Router::restoreResolvers() delegates to the platform-specific DnsUtils::restoreResolvers() to revert changes.

DnsUtilsLinux::restoreResolvers() in client/platforms/linux/daemon/dnsutilslinux.cpp (lines 67-88) first reapplies any saved search domains, then calls the RevertLink D-Bus method to return the interface to its pre-VPN state.

// client/platforms/linux/daemon/dnsutilslinux.cpp
bool DnsUtilsLinux::restoreResolvers()
{
    // Re‑apply saved search domains
    for (auto it = m_linkDomains.constBegin(); it != m_linkDomains.constEnd(); ++it) {
        setLinkDomains(it.key(), it.value());
    }
    m_linkDomains.clear();

    // Ask systemd‑resolve to revert the interface’s DNS configuration
    QList<QVariant> args = { QVariant::fromValue(m_ifindex) };
    QDBusPendingReply<> reply = m_resolver->asyncCallWithArgumentList(
        QStringLiteral("RevertLink"), args);
    // Watch for completion …
    return true;
}

macOS and Windows Restoration

On macOS, DnsUtilsMacos restores the saved DNS settings from the tunnel initialization phase. On Windows, DnsUtilsWindows resets the adapter's DNS servers using the Win32 API.

Integration with the Kill-Switch

The kill-switch ensures DNS traffic cannot leak outside the VPN tunnel. It blocks external DNS queries by installing PF rules on macOS or iptables rules on Linux, defined in service/server/killswitch.cpp (lines 348-388).

After configuring DNS servers, the firewall enables the "allow DNS" anchor (310.blockDNS / 320.allowDNS), creating a whitelist that permits queries only to the VPN-assigned DNS servers.

Summary

  • Layered Architecture: The Router class abstracts DNS operations while DnsUtils handles platform-specific resolver APIs.
  • Cache Management: Linux restarts nscd or systemd-resolved; macOS signals mDNSResponder; Windows uses native DNS APIs.
  • DNS Push: Linux uses D-Bus (SetLinkDNS, SetLinkDefaultRoute); macOS uses scutil; Windows uses SetAdapterDnsAddresses.
  • Clean Teardown: Linux calls RevertLink via D-Bus; all platforms restore original settings via restoreResolvers().
  • Leak Prevention: The kill-switch blocks external DNS traffic while the VPN is active, allowing only tunneled queries.

Frequently Asked Questions

How does amnezia-client prevent DNS leaks when the VPN is connected?

The client implements a kill-switch in service/server/killswitch.cpp that installs firewall rules (iptables on Linux, PF on macOS) to block all DNS traffic outside the VPN tunnel. Only the DNS servers pushed via updateResolvers() are whitelisted, ensuring queries cannot bypass the encrypted tunnel.

What happens to my system DNS settings when I disconnect from amnezia-client?

When the VPN disconnects, Router::restoreResolvers() triggers platform-specific restoration logic. On Linux, this calls the RevertLink D-Bus method to return the interface to its original configuration; on macOS and Windows, saved settings are reapplied from the pre-connection state.

Does amnezia-client support custom DNS servers?

Yes. The updateResolvers() method accepts a QList<QHostAddress> parameter that can contain either the default AmneziaDNS servers or user-specified custom resolvers. These are pushed to the system resolver regardless of whether they use the systemd-resolved D-Bus API, macOS scutil, or the Windows DNS API.

Why does amnezia-client flush the DNS cache when disconnecting?

Flushing the DNS cache via Router::flushDns() ensures that cached entries from the VPN's internal DNS servers do not persist after the tunnel closes. This prevents stale resolution data from interfering with subsequent queries that should use the restored system resolvers.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →