[GH-ISSUE #3283] [Linux] DNS-leak protection blocks LAN DNS — 310.blockDNS evaluated before 300.allowLAN #2375

Open
opened 2026-10-14 21:01:08 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @ilyapichugin78-hash on GitHub (Oct 14, 2026).
Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/3283

Summary

With the AmneziaVPN tunnel up, the client firewall rejects all DNS traffic to LAN resolvers
(the router, a local Pi-hole, 192.168.x.x, …). The 300.allowLAN anchor is clearly intended to
permit local-network traffic, but 310.blockDNS runs ahead of it, so that intent is unreachable
for port 53.

Root cause

client/platforms/linux/daemon/linuxfirewall.cpp, LinuxFirewall::install() (branch dev):

installAnchor(IPv4, QStringLiteral("320.allowDNS"), {});          // line 216

installAnchor(Both,  QStringLiteral("310.blockDNS"), {            // line 218
                                                            QStringLiteral("-p udp --dport 53 -j REJECT"),
                                                            QStringLiteral("-p tcp --dport 53 -j REJECT"),
                                                        });

installAnchor(IPv4, QStringLiteral("300.allowLAN"), {             // line 222
                                                            QStringLiteral("-d 10.0.0.0/8 -j ACCEPT"),
                                                            ...
                                                            QStringLiteral("-d 192.168.0.0/16 -j ACCEPT"),
                                                            ...
                                                        });
installAnchor(IPv6, QStringLiteral("300.allowLAN"), { ... });     // line 230

installAnchor() links each anchor's placeholder chain into the root amnvpn.anchors chain
in insertion order — installAnchor() calls linkChain(..., false), which appends with -A,
and the placeholder "stays locked into place in the root chain without being removed or recreated,
ensuring the intended precedence order" (comment at linuxfirewall.cpp:150-152).

Observed runtime precedence, filter table (iptables-save, amnvpn.anchors):

 1. amnvpn.a.000.allowLoopback
 2. amnvpn.a.320.allowDNS
 3. amnvpn.a.310.blockDNS     <- rejects udp/tcp dport 53
 4. amnvpn.a.300.allowLAN     <- permits 192.168.0.0/16 etc.
 5. amnvpn.a.290.allowDHCP
 6. amnvpn.a.200.allowVPN
 7. amnvpn.a.130.allowMarkedXray
 8. amnvpn.a.120.blockNets
 9. amnvpn.a.110.allowNets
10. amnvpn.a.400.allowPIA
11. amnvpn.a.100.blockAll

A DNS query to a LAN resolver (e.g. 192.168.1.1:53) is REJECTed at step 3 and never reaches
the -d 192.168.0.0/16 -j ACCEPT at step 4.

320.allowDNS only permits the resolver addresses from the connection profile, and only via the
tunnel interface — so a LAN resolver can never match it:

-A amnvpn.320.allowDNS -d 1.1.1.1/32 -o amn0+ -p udp -m udp --dport 53 -j ACCEPT
-A amnvpn.320.allowDNS -d 1.0.0.1/32 -o amn0+ -p tcp -m tcp --dport 53 -j ACCEPT

Evidence — live counters (filter table)

amnvpn.310.blockDNS   udp dpt:53  REJECT   320 pkts  24584 bytes
amnvpn.310.blockDNS   tcp dport:53  REJECT     3 pkts    188 bytes
amnvpn.320.allowDNS   1.1.1.1 via amn0+ tcp    337 pkts  27560 bytes
amnvpn.320.allowDNS   1.0.0.1 via amn0+ tcp    225 pkts  18336 bytes
amnvpn.300.allowLAN   192.168.0.0/16 ACCEPT  2792 pkts   351K bytes   <- non-DNS LAN traffic flows

The 320 rejected UDP packets are LAN/resolver queries that never left the machine.

Reproduction

  1. Connect with a profile whose DNS= points at a public resolver (e.g. 1.1.1.1).
  2. With the tunnel up: dig @1.1.1.1 example.com works; dig @192.168.1.1 example.com fails.
  3. A device on the same LAN with no VPN resolves the same name fine.

Suggested fix

Move the 310.blockDNS installAnchor call below the 300.allowLAN calls (both families), so the
LAN allow is evaluated before the DNS block. 320.allowDNS stays first so the profile's resolvers
keep working:

installAnchor(IPv4, QStringLiteral("320.allowDNS"), {});
installAnchor(IPv4, QStringLiteral("300.allowLAN"), { ... });
installAnchor(IPv6, QStringLiteral("300.allowLAN"), { ... });
installAnchor(Both,  QStringLiteral("310.blockDNS"), { ... });   // moved down

Equivalent alternative: add the LAN DNS destinations to 320.allowDNS.

Environment

  • AmneziaVPN Linux client, dev branch (linuxfirewall.cpp:216-229)
  • Tunnel interface amn0; kernel 7.0.0-38-generic
Originally created by @ilyapichugin78-hash on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/3283 ## Summary With the AmneziaVPN tunnel up, the client firewall rejects **all DNS traffic to LAN resolvers** (the router, a local Pi-hole, `192.168.x.x`, …). The `300.allowLAN` anchor is clearly intended to permit local-network traffic, but `310.blockDNS` runs ahead of it, so that intent is unreachable for port 53. ## Root cause `client/platforms/linux/daemon/linuxfirewall.cpp`, `LinuxFirewall::install()` (branch `dev`): ```cpp installAnchor(IPv4, QStringLiteral("320.allowDNS"), {}); // line 216 installAnchor(Both, QStringLiteral("310.blockDNS"), { // line 218 QStringLiteral("-p udp --dport 53 -j REJECT"), QStringLiteral("-p tcp --dport 53 -j REJECT"), }); installAnchor(IPv4, QStringLiteral("300.allowLAN"), { // line 222 QStringLiteral("-d 10.0.0.0/8 -j ACCEPT"), ... QStringLiteral("-d 192.168.0.0/16 -j ACCEPT"), ... }); installAnchor(IPv6, QStringLiteral("300.allowLAN"), { ... }); // line 230 ``` `installAnchor()` links each anchor's placeholder chain into the root `amnvpn.anchors` chain **in insertion order** — `installAnchor()` calls `linkChain(..., false)`, which appends with `-A`, and the placeholder "stays locked into place in the root chain without being removed or recreated, ensuring the intended precedence order" (comment at `linuxfirewall.cpp:150-152`). Observed runtime precedence, filter table (`iptables-save`, `amnvpn.anchors`): ``` 1. amnvpn.a.000.allowLoopback 2. amnvpn.a.320.allowDNS 3. amnvpn.a.310.blockDNS <- rejects udp/tcp dport 53 4. amnvpn.a.300.allowLAN <- permits 192.168.0.0/16 etc. 5. amnvpn.a.290.allowDHCP 6. amnvpn.a.200.allowVPN 7. amnvpn.a.130.allowMarkedXray 8. amnvpn.a.120.blockNets 9. amnvpn.a.110.allowNets 10. amnvpn.a.400.allowPIA 11. amnvpn.a.100.blockAll ``` A DNS query to a LAN resolver (e.g. `192.168.1.1:53`) is REJECTed at step 3 and never reaches the `-d 192.168.0.0/16 -j ACCEPT` at step 4. `320.allowDNS` only permits the resolver addresses from the connection profile, and only via the tunnel interface — so a LAN resolver can never match it: ``` -A amnvpn.320.allowDNS -d 1.1.1.1/32 -o amn0+ -p udp -m udp --dport 53 -j ACCEPT -A amnvpn.320.allowDNS -d 1.0.0.1/32 -o amn0+ -p tcp -m tcp --dport 53 -j ACCEPT ``` ## Evidence — live counters (filter table) ``` amnvpn.310.blockDNS udp dpt:53 REJECT 320 pkts 24584 bytes amnvpn.310.blockDNS tcp dport:53 REJECT 3 pkts 188 bytes amnvpn.320.allowDNS 1.1.1.1 via amn0+ tcp 337 pkts 27560 bytes amnvpn.320.allowDNS 1.0.0.1 via amn0+ tcp 225 pkts 18336 bytes amnvpn.300.allowLAN 192.168.0.0/16 ACCEPT 2792 pkts 351K bytes <- non-DNS LAN traffic flows ``` The 320 rejected UDP packets are LAN/resolver queries that never left the machine. ## Reproduction 1. Connect with a profile whose `DNS=` points at a public resolver (e.g. `1.1.1.1`). 2. With the tunnel up: `dig @1.1.1.1 example.com` works; `dig @192.168.1.1 example.com` fails. 3. A device on the same LAN with no VPN resolves the same name fine. ## Suggested fix Move the `310.blockDNS` `installAnchor` call below the `300.allowLAN` calls (both families), so the LAN allow is evaluated before the DNS block. `320.allowDNS` stays first so the profile's resolvers keep working: ```cpp installAnchor(IPv4, QStringLiteral("320.allowDNS"), {}); installAnchor(IPv4, QStringLiteral("300.allowLAN"), { ... }); installAnchor(IPv6, QStringLiteral("300.allowLAN"), { ... }); installAnchor(Both, QStringLiteral("310.blockDNS"), { ... }); // moved down ``` Equivalent alternative: add the LAN DNS destinations to `320.allowDNS`. ## Environment - AmneziaVPN Linux client, `dev` branch (`linuxfirewall.cpp:216-229`) - Tunnel interface `amn0`; kernel 7.0.0-38-generic
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#2375
No description provided.