WARNING: 98% of this was written by an LLM
A tiny Python DNS proxy that makes IPv6-only servers work correctly with software that queries A records first and never falls back to AAAA.
Some DNS resolvers (e.g. Rust's hickory, older getaddrinfo configs)
query A records first. On a server with only outbound IPv6 (or NAT64 that
blocks non-web ports), connecting to an IPv4 address simply fails — and the
badly coded client never retries with AAAA.
Run this proxy on 127.0.0.1:53 as the first nameserver. For every A
query, it fires a parallel AAAA probe:
- Host has both A and AAAA → return NODATA for the A query (caller is forced onto IPv6)
- Host has only A records → pass the A record through (IPv4-only hosts still work, e.g. via NAT64)
- Any other query type (AAAA, SRV, MX, …) → forwarded unchanged
- Python 3.10+
python3-dnslib(orpip install dnslib)- A Linux system running systemd
- Port 53 on localhost must be free (see step 3)
apt install python3-dnslib # Debian/Ubuntu
# or: pip install dnslibmv dns-prefer-ipv6.py /usr/local/bin/dns-prefer-ipv6.py
chmod +x /usr/local/bin/dns-prefer-ipv6.pyEdit the UPSTREAMS list near the top of the script to point at your
actual resolver IPs. Use cat /etc/resolv.conf to find them, then replace
the placeholder addresses.
Something else is probably listening on 127.0.0.1:53. Common culprits:
systemd-resolved stub listener (most common):
# /etc/systemd/resolved.conf
[Resolve]
DNSStubListener=no
systemctl restart systemd-resolvedAnother DNS server (e.g. PowerDNS, bind) bound to 0.0.0.0 or 127.0.0.1: Restrict it to your public IP only. For PowerDNS:
# /etc/powerdns/pdns.conf – add:
local-address=YOUR.PUBLIC.IP.HERE # your public IP
systemctl restart pdnsVerify the port is free:
ss -ulnp sport = 53
# Should show nothing on 127.0.0.1 or ::1cp dns-prefer-ipv6.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now dns-prefer-ipv6.service
systemctl status dns-prefer-ipv6.serviceDebian/Ubuntu with resolvconf:
echo "nameserver 127.0.0.1" > /etc/resolvconf/resolv.conf.d/head
resolvconf -uOr edit /etc/resolv.conf directly (will be overwritten on reboot
unless managed):
nameserver 127.0.0.1
nameserver <your-upstream-1>
nameserver <your-upstream-2>
Containers get their own mount namespace. Even with --network=host, the
container's /etc/resolv.conf is a snapshot copied at start time — it does
not track live changes to the host file.
Add this flag to your container run command:
-v /etc/resolv.conf:/etc/resolv.conf:roThis bind-mounts the live host file into the container so it always sees
127.0.0.1 as the first nameserver, regardless of when the container was
started.
# A record suppressed (matrix.org has both A and AAAA)
dig A matrix.org @127.0.0.1
# Expected: NOERROR, zero answers (NODATA)
# AAAA record returned normally
dig AAAA matrix.org @127.0.0.1
# Expected: NOERROR, one or more AAAA answers
# IPv4-only host passes A through unchanged
dig A example.com @127.0.0.1
# Expected: NOERROR, one or more A answers
# Watch suppression live
journalctl -u dns-prefer-ipv6.service -f
# Lines like: suppress A matrix.org (AAAA exists)systemctl disable --now dns-prefer-ipv6.service
rm /usr/local/bin/dns-prefer-ipv6.py /etc/systemd/system/dns-prefer-ipv6.service
# Remove "nameserver 127.0.0.1" from /etc/resolv.conf (or resolvconf head)
# Re-bind your previous DNS server to 127.0.0.1 if needed