1

I am attempting to redirect all outgoing DNS queries on my Linux box to my local caching stub resolver (unbound).

iptables -t nat -A OUTPUT -p tcp --dport 53 -j DNAT --to 1.1.1.1:53
iptables -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to 1.1.1.1:53
iptables -t nat -A POSTROUTING -j MASQUERADE

When I use the above rules, all the outgoing DNS queries are intercepted and redirected to the DNS server at 1.1.1.1

However, if I replace the '1.1.1.1' with '127.0.0.1', all DNS queries fail and are not directed to my local stub resolver.

I did pass the below sysctl parameter

sysctl -w net.ipv4.conf.eth0.route_localnet=1

but my problem remained the same. Any pointers?

  • 2
    That is not a stub resolver. unix.stackexchange.com/a/500565/5132 – JdeBP Apr 5 at 19:20
  • Why use NAT and not just block all outgoing DNS traffic except from the single host (or two hosts, for redundancy) and configure all clients to use that single host? A correctly configured network sounds so much better than NAT'ing traffic. – Tommiie Apr 15 at 14:55
1

What becomes clear if debugging with strace, and nc/socat, is that nat/POSTROUTING's MASQUERADE didn't alter the initially chosen address intended to be used for going out. Probably because it's still considered a local address about to be "routed" to lo so doesn't need alteration: the MASQUERADE rule has no effect here.

Anyway, that's what happened. So when replying an UDP query, the server actually connects back to the source which sent data, now use as destination. Naturally the best source is chosen to be used for this destination, the same local address, which is not 127.0.0.1. So here's what's happening if following this with conntrack -E, with an example local IP of 192.0.2.2 and a destination of 198.51.100.1 UDP port 53:

    [NEW] udp      17 30 src=192.0.2.2 dst=198.51.100.1 sport=40037 dport=53 [UNREPLIED] src=127.0.0.1 dst=192.0.2.2 sport=53 dport=40037
    [NEW] udp      17 30 src=192.0.2.2 dst=192.0.2.2 sport=53 dport=40037 [UNREPLIED] src=172.16.0.22 dst=172.16.0.22 sport=40037 dport=53

The reply is not correlated to the initial query (because the source IP isn't 127.0.0.1) so conntrack is handling this as a 2nd flow. Meanwhile the client put its UDP socket in connected mode, meaning an UDP packet received from the wrong source IP (even if correct ports) will be rejected, and the server receives an ICMP error (this can be witnessed with tcpdump -i lo).

The correction is quite simple: don't use MASQUERADE but SNAT. Of course it now has to be specialized for this specific flow (you don't want to SNAT everything to 127.0.0.1), so replace the MASQUERADE line with this instead:

iptables -t nat -A POSTROUTING -p udp --dport 53 -j SNAT --to-source 127.0.0.1

With the corrected flow, the local server now replies using conntrack's expected address which now associate it in the previous flow and de-SNATs it correctly:

    [NEW] udp      17 30 src=192.0.2.2 dst=198.51.100.1 sport=38871 dport=53 [UNREPLIED] src=127.0.0.1 dst=127.0.0.1 sport=53 dport=38871
 [UPDATE] udp      17 30 src=192.0.2.2 dst=198.51.100.1 sport=38871 dport=53 src=127.0.0.1 dst=127.0.0.1 sport=53 dport=38871

The client receives the expected source 198.51.100.1 and all works as intended.

TCP doesn't suffer the same result, because once the connection is established between 192.0.2.2 and 127.0.0.1, the reply is within the same established connection, it's not a new connection as with UDP, so will have already the expected source and is handled correctly by conntrack. Better anyway add this for consistency:

iptables -t nat -A POSTROUTING -p tcp --dport 53 -j SNAT --to-source 127.0.0.1

Two notes:

  • for your specific case, route_localnet is not needed because all packets are local and stay on lo. The opposite: forwarding elsewhere packets sent to 127.0.0.1 would require it (as well as other tricks).

  • You will probably need additional exception rules if your DNS server is also a DNS client (which would be the case for a recursive DNS server) sending queries outside, or its own queries will be rerouted to itself creating a loop. Usually solved by having the server running with a specific user and using iptables' -m owner match. Something like inserting before each group of rules (in nat/OUTPUT and nat/POSTROUTING) this:

    iptables -t nat -I .... -m owner --uid-owner unbound -j RETURN
    

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.