Finding out musl's DNS resolution implementation
2026-07-09
There are moments you can hardly explain yourself what’s going on, especially when dealing with vastly known, solid tools like ping, dig and curl. This was literally me while trying to understand unexpected local DNS resolution issues on a containerised application based on Alpine.
I took the opportunity to debug further and share what, imo, is a pretty known scenario to those who carefully and consciously pick one technology over another. I did not and many other respected collaborators of mine did not either, apparently.
In a scenario where your system uses dnsmasq and the setup includes the -A, --address= in its configuration, it becomes possible that this address will not be correctly translated on a container running on Alpine, and that’s because you’re likely going to miss something in /etc/dnsmasq.conf. In short, this happens as getaddrinfo() in musl is implemented differently than the glibc implementation. An interesting article treats the same topic with good depth, while this article also shares a similar experience to mine. The musl wiki explains the differences between both implementations. Spoiler, you need to also enter a (NULL, if you want) IPv6 address (::) along with the specified IPv4. You can even stop reading here.
Let’s reproduce with containers. docker, docker-compose, dnsmasq, tcpdump (on host) will be needed for this.
/etc/dnsmasq.conf:
bind-interfaces
domain-needed
bogus-priv
address=/custom-address/10.0.0.1
user=dnsmasq
group=dnsmasq
interface=*
server=1.0.0.1
server=1.1.1.1
docker-compose.yaml:
services:
dnsmasq:
image: dockurr/dnsmasq
container_name: dnsmasq
environment:
DNS1: "1.0.0.1"
DNS2: "1.1.1.1"
ports:
- "192.168.1.18:53:53/udp"
- "192.168.1.18:53:53/tcp"
cap_add:
- NET_RAW
- NET_ADMIN
restart: always
volumes:
- ./dnsmasq.conf:/etc/dnsmasq.conf:Z
Explicitly set the local IP listening on 53 in case of port conflict, as mentioned in dockurr/dnsmasq.
Start docker compose and wait for dnsmasq to run on a container.
docker compose up:
[dnsmasq] | dnsmasq: started, version 2.93 cachesize 150
[dnsmasq] | dnsmasq: compile time options: IPv6 GNU-getopt no-DBus no-UBus no-i18n no-IDN DHCP DHCPv6 no-Lua TFTP no-conntrack ipset no-nftset auth DNSSEC loop-detect inotify dumpfile
[dnsmasq] | dnsmasq: using nameserver 1.0.0.1#53
[dnsmasq] | dnsmasq: using nameserver 1.1.1.1#53
[dnsmasq] | dnsmasq: read /etc/hosts - 14 names
Also start an Alpine container, I will use Node 22.22.0 as this was the one I was working with while troubleshooting.
docker run -it --dns=192.168.1.18 node:22.22.0
# / apk update && apk add bind-tools
Perhaps install dig to see a working and a non-working DNS resolution. dig uses its own mechanism for resolving DNS, while ping (pre-installed) uses musl routines. From inside the Node container:
dig custom-address:
Dig successful response:
; <<>> DiG 9.20.23 <<>> custom-address
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 24967
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;custom-address. IN A
;; ANSWER SECTION:
custom-address. 0 IN A 10.0.0.1
;; Query time: 1 msec
;; SERVER: 192.168.1.18#53(192.168.1.18) (UDP)
;; WHEN: Wed Jul 08 21:51:43 UTC 2026
;; MSG SIZE rcvd: 58
ping custom-address:
ping: bad address 'custom-address'
Let’s bring some more information from the container before starting analyzing the traffic with tcpdump on the host. If you do not already know the network interface from your app container (not dnsmasq):
cat /sys/class/net/eth0/iflink
36
Note 36 and then, from the host where your containers are running:
ip link | grep '^36:'
Note the interface ID, in my case veth1. Start tcpdump:
tcpdump -i veth1 -n -vv port 53
Recap:
dnsmasqrunning as a container, mapping to the host as191.168.1.18:53- App (
node:22.22.0) running and usingdnsmasqas its own resovler/forwarder (via--dns=192.168.1.18directive indockercommand) tcpdumpon host listening on the Docker’s virtual network interface
From the node container run ping custom-address and observe in tcpdump:
23:51:11.841250 IP (tos 0x0, ttl 64, id 45117, offset 0, flags [DF], proto UDP (17), length 59)
10.88.0.7.37992 > 192.168.1.18.domain: [bad udp cksum 0xcc51 -> 0xf88a!] 4254+ A? custom-address. (31)
23:51:11.841402 IP (tos 0x0, ttl 64, id 45118, offset 0, flags [DF], proto UDP (17), length 59)
10.88.0.7.37992 > 192.168.1.18.domain: [bad udp cksum 0xcc51 -> 0xda01!] 5159+ AAAA? custom-address. (31)
23:51:11.841609 IP (tos 0x0, ttl 63, id 30246, offset 0, flags [DF], proto UDP (17), length 75)
192.168.1.18.domain > 10.88.0.7.37992: [bad udp cksum 0xcc61 -> 0x601f!] 4254* q: A? custom-address. 1/0/0 custom-address. A 10.0.0.1 (47)
23:51:11.841744 IP (tos 0x0, ttl 63, id 30247, offset 0, flags [DF], proto UDP (17), length 59)
192.168.1.18.domain > 10.88.0.7.37992: [bad udp cksum 0xcc51 -> 0x597e!] 5159 NXDomain q: AAAA? custom-address. 0/0/0 (31)
Last line above is the problem to the ping: bad address 'custom-address' (NXDOMAIN). Let’s retry with dig custom-address:
23:52:07.635969 IP (tos 0x0, ttl 64, id 58182, offset 0, flags [none], proto UDP (17), length 82)
10.88.0.7.59643 > 192.168.1.18.domain: [bad udp cksum 0xcc68 -> 0x9e7b!] 57293+ [1au] A? custom-address. ar: . OPT UDPsize=1232 [COOKIE bcf4899c1135d91f] (54)
23:52:07.636125 IP (tos 0x0, ttl 63, id 50946, offset 0, flags [DF], proto UDP (17), length 86)
192.168.1.18.domain > 10.88.0.7.59643: [bad udp cksum 0xcc6c -> 0x374c!] 57293* q: A? custom-address. 1/0/1 custom-address. A 10.0.0.1 ar: . OPT UDPsize=1232 (58)
No problem with dig, there is no reference to IPv6. It resolves the local IPv4 address fine.
Adding address=/custom-address/:: to /etc/dnsmasq.conf should be enough to fix ping. So:
docker compose down
echo 'address=/custom-address/::' >> ./dnsmasq.conf
docker compose up
Retry ping from the app container:
PING custom-address (10.0.0.1): 56 data bytes
From tcpdump:
23:49:18.644383 IP (tos 0x0, ttl 64, id 18878, offset 0, flags [DF], proto UDP (17), length 59)
10.88.0.7.60943 > 192.168.1.18.domain: [bad udp cksum 0xcc51 -> 0x9ab4!] 5325+ A? custom-address. (31)
23:49:18.644527 IP (tos 0x0, ttl 64, id 18879, offset 0, flags [DF], proto UDP (17), length 59)
10.88.0.7.60943 > 192.168.1.18.domain: [bad udp cksum 0xcc51 -> 0x7c5a!] 6183+ AAAA? custom-address. (31)
23:49:18.644682 IP (tos 0x0, ttl 63, id 55552, offset 0, flags [DF], proto UDP (17), length 75)
192.168.1.18.domain > 10.88.0.7.60943: [bad udp cksum 0xcc61 -> 0x0249!] 5325* q: A? custom-address. 1/0/0 custom-address. A 10.0.0.1 (47)
23:49:18.644841 IP (tos 0x0, ttl 63, id 55553, offset 0, flags [DF], proto UDP (17), length 87)
192.168.1.18.domain > 10.88.0.7.60943: [bad udp cksum 0xcc6d -> 0xbde0!] 6183* q: AAAA? custom-address. 1/0/0 custom-address. AAAA :: (59)
There’s no more NXDOMAIN when querying for AAAA.
It finally resolves.
Links
- https://alpinelinux.org/
- https://thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html
- https://hub.docker.com/r/dockurr/dnsmasq
- https://wiki.musl-libc.org/functional-differences-from-glibc.html
- https://bell-sw.com/blog/how-to-deal-with-alpine-dns-issues/
- https://git.musl-libc.org/cgit/musl/tree/src/network/getaddrinfo.c