CNAME records pointing to pi.hole return 0.0.0.0

I have added the CNAME record pihole.mydomain.net pointing to pi.hole to receive a localized IP response based on the interface origin.

The record is correctly resolved by tools making basic DNS requests like ping:

$ ping pihole.mydomain.net
PING pi.hole (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.026 ms

Tools requesting advanced DNS information like dig receive 0.0.0.0, no matter the origin interface:

$ dig pihole.mydomain.net @127.0.0.1

; <<>> DiG 9.20.9-1-Debian <<>> pihole.mydomain.net
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 48616
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;pihole.mydomain.net.		IN	A

;; ANSWER SECTION:
pihole.mydomain.net.	0	IN	CNAME	pi.hole.
pi.hole.		0	IN	A	0.0.0.0

;; Query time: 0 msec
;; SERVER: 127.0.0.1#53(127.0.0.1) (UDP)
;; WHEN: Sun Jul 20 21:40:29 CEST 2025
;; MSG SIZE  rcvd: 85

$ dig pihole.mydomain.net @172.30.0.1

; <<>> DiG 9.20.9-1-Debian <<>> pihole.mydomain.net @172.30.0.1
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 65359
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;pihole.mydomain.net.		IN	A

;; ANSWER SECTION:
pihole.mydomain.net.	0	IN	CNAME	pi.hole.
pi.hole.		0	IN	A	0.0.0.0

;; Query time: 0 msec
;; SERVER: 172.30.0.1#53(172.30.0.1) (UDP)
;; WHEN: Sun Jul 20 21:40:37 CEST 2025
;; MSG SIZE  rcvd: 85

$ dig pihole.mydomain.net @192.168.178.158

; <<>> DiG 9.20.9-1-Debian <<>> pihole.mydomain.net @192.168.178.158
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 44482
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;pihole.mydomain.net.		IN	A

;; ANSWER SECTION:
pihole.mydomain.net.	0	IN	CNAME	pi.hole.
pi.hole.		0	IN	A	0.0.0.0

;; Query time: 4 msec
;; SERVER: 192.168.178.158#53(192.168.178.158) (UDP)
;; WHEN: Sun Jul 20 21:40:48 CEST 2025
;; MSG SIZE  rcvd: 85

Requesting pi.hole normally works as expected:

$ dig pi.hole @127.0.0.1

; <<>> DiG 9.20.9-1-Debian <<>> pi.hole
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 11674
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; EDE: 29: (synthesized)
;; QUESTION SECTION:
;pi.hole.			IN	A

;; ANSWER SECTION:
pi.hole.		0	IN	A	127.0.0.1

;; Query time: 0 msec
;; SERVER: 127.0.0.1#53(127.0.0.1) (UDP)
;; WHEN: Sun Jul 20 21:42:08 CEST 2025
;; MSG SIZE  rcvd: 69

$ dig pi.hole @172.30.0.1

; <<>> DiG 9.20.9-1-Debian <<>> pi.hole @172.30.0.1
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 54116
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; EDE: 29: (synthesized)
;; QUESTION SECTION:
;pi.hole.			IN	A

;; ANSWER SECTION:
pi.hole.		0	IN	A	172.30.0.1

;; Query time: 0 msec
;; SERVER: 172.30.0.1#53(172.30.0.1) (UDP)
;; WHEN: Sun Jul 20 21:42:14 CEST 2025
;; MSG SIZE  rcvd: 69

$ dig pi.hole @192.168.178.158

; <<>> DiG 9.20.9-1-Debian <<>> pi.hole @192.168.178.158
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 50076
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; EDE: 29: (synthesized)
;; QUESTION SECTION:
;pi.hole.			IN	A

;; ANSWER SECTION:
pi.hole.		0	IN	A	192.168.178.158

;; Query time: 0 msec
;; SERVER: 192.168.178.158#53(192.168.178.158) (UDP)
;; WHEN: Sun Jul 20 21:42:20 CEST 2025
;; MSG SIZE  rcvd: 69

My guess is that the 0.0.0.0 in host-record=pi.hole,0.0.0.0 in CNAME responses(the actual CNAME request, not an A request that happens to be a CNAME) is wrongly interpreted as actual 0.0.0.0 and not as the IP of the machine running dnsmasq/pihole-FTL.

Im not sure if this is an issue or not, I am ready to be corrected by people understanding CNAME records and dnsmasq logic better than me.

Please upload a debug log and post just the token URL that is generated after the log is uploaded by running the following command from the Pi-hole host terminal:

pihole -d

or if you run your Pi-hole as a Docker container:

docker exec -it <pihole-container-name-or-id> pihole -d

where you substitute <pihole-container-name-or-id> as required.

https://tricorder.pi-hole.net/ykJaFkB2/

Am not sure but I believe that 0.0.0.0 IP as a substitute for the own IP only applies for DHCP options.
Its only mentioned in the DHCP section on the man page:

$ man dnsmasq
[..]
       -O,    --dhcp-option=[tag:<tag>,[tag:<tag>,]][encap:<opt>,][vi-en‐
       cap:<enterprise>,][vendor:[<vendor-class>],][<opt>|option:<opt-
       name>|option6:<opt>|option6:<opt-name>],[<value>[,<value>]]
[..]
              The  special  address 0.0.0.0 is taken to mean
              "the address of the machine running dnsmasq".

So I think you'll have to supply the actual IP for host records etc.

For this you are correct.
I mistook the 0.0.0.0 for its meaning in DHCP flags.

This seems to be more closely related to the handling of the special record pi.hole by Pi-hole specific code:

From /etc/pihole/dnsmasq.conf:

[...]
# Local domain for Pi-hole
# This domain is purely local and should never be forwarded to any
# upstream servers. We add a false A-record to this domain to prevent
# NXDOMAIN responses for queries on this domain. The actual response
# is handled by FTL at runtime
local=/pi.hole/
host-record=pi.hole,0.0.0.0
[...]

Im guessing that when returning the CNAME response dnsmasq doesnt get the internal IP provided with Pi-holes code.

1 Like
$ sudo pihole-FTL --config dns.cnameRecords '[ "pihole.mydomain.net,pi.hole" ]'
[ pihole.mydomain.net,pi.hole ]
$ dig +short @localhost pihole.mydomain.net cname
pi.hole.
$ sudo pihole tail
[..]
23:35:32: query[CNAME] pihole.mydomain.net from 127.0.0.1
23:35:32: config pihole.mydomain.net is <CNAME>

Aha I see:

$ dig +short @localhost pihole.mydomain.net
pi.hole.
0.0.0.0
$ dig +short @localhost pi.hole
127.0.0.1

EDIT: It did used to work via the API:

I was just about to answer this, you beat me by a minute.
This is also logged as 0.0.0.0

$ sudo pihole tail
23:49:12: query[A] pihole.mydomain.net from ::1
23:49:12: config pihole.mydomain.net is <CNAME>
23:49:12: config pi.hole is 0.0.0.0

After deleting the one I created via shell:

$ curl -s --request PUT localhost/api/config/dns/cnameRecords/pihole.mydomain.net,pi.hole | jq
{
  "took": 0.011423826217651367
}
$ dig +short @localhost pihole.mydomain.net
pi.hole.
0.0.0.0
$ sudo pihole tail
[..]
23:56:21: query[A] pihole.mydomain.net from 127.0.0.1
23:56:21: config pihole.mydomain.net is <CNAME>
23:56:21: config pi.hole is 0.0.0.0

You confused me here with the API, I thought it was an API issue. But this is the same no matter how the record is added to pihole

The API is this one:

http://pi.hole/api/docs/

Or HTTPS:

https://pi.hole/api/docs/

I know how the API works, but how is the API involved in this issue?
This issue occurs even if you modify it directly in pihole.toml

Yes so now we know all three methods gives the same results 0.0.0.0 :wink:
Now wait for a dev/mod to reply.

Oh bc of this:

I think it might be good if you could lodge this as a bug report on github.

Definitely. When it's queried directly (A or AAAA) it is answered with the local IP before it is even handed over to the internal dnsmasq.

        // If domain is "pi.hole" or the local hostname we skip analyzing this query
        // and, instead, immediately reply with the IP address - these queries are not further analyzed
        if(querytype != TYPE_NONE && is_pihole_domain(name))
        {
                if(querytype == TYPE_A || querytype == TYPE_AAAA || querytype == TYPE_ANY)
                {
                        // "Block" this query by sending the interface IP address
                        // Send NODATA when the current interface doesn't have
                        // the requested IP address, for instance AAAA on an
                        // virtual interface that has only an IPv4 address

dig pi.hole

2025-07-22 10:47:08.051 DEBUG_QUERIES Replying to pi.hole with interface-local IP address
2025-07-22 10:47:08.051 DEBUG_QUERIES Preparing reply for "pi.hole"
2025-07-22 10:47:08.051 DEBUG_QUERIES Forced DNS reply to IP
2025-07-22 10:47:08.051 DEBUG_QUERIES Setting EDE: synthesized (29) + "synthesized"
2025-07-22 10:47:08.051 DEBUG_QUERIES Adding RR: "pi.hole A 192.168.1.120"

But if it comes up within a CNAME, there is no corresponding code, and it is treated the same as any other domain, and the ip 0.0.0.0 from the host record applies.

dig test.lan

2025-07-22 10:48:22.032 DEBUG_QUERIES **** new UDP IPv4 query[A] query "test.lan" from enp3s0/192.168.1.120#34552 (ID 12, FTL 23, src/dnsmasq/forward.c:1899)
2025-07-22 10:48:22.032 DEBUG_STATUS query type 1 set (new query), ID = 12, new count = 10
2025-07-22 10:48:22.032 DEBUG_STATUS Query 12: status initialized: UNKNOWN (0) in _FTL_new_query() (src/dnsmasq_interface.c:872)
2025-07-22 10:48:22.032 DEBUG_STATUS status 0 set, ID = 12, new count = 1
2025-07-22 10:48:22.032 DEBUG_STATUS reply type 0 set (new query), ID = 12, new count = 1
2025-07-22 10:48:22.032 DEBUG_QUERIES Set global cache status to 3
2025-07-22 10:48:22.032 DEBUG_QUERIES test.lan is known as not to be blocked
2025-07-22 10:48:22.032 DEBUG_QUERIES **** got cache reply: test.lan is (CNAME) (ID 12, src/dnsmasq/rfc1035.c:1729)
2025-07-22 10:48:22.032 DEBUG_STATUS Query 12: status changed: UNKNOWN (0) -> CACHE (3) in FTL_reply() (src/dnsmasq_interface.c:2367)
2025-07-22 10:48:22.033 DEBUG_STATUS status 0 removed (!init), ID = 12, new count = 0
2025-07-22 10:48:22.033 DEBUG_STATUS status 3 set, ID = 12, new count = 23
2025-07-22 10:48:22.033 DEBUG_QUERIES Set reply to CNAME (3) in src/dnsmasq_interface.c:2370
2025-07-22 10:48:22.033 DEBUG_STATUS reply type 0 removed (set_reply), ID = 12, new count = 0
2025-07-22 10:48:22.033 DEBUG_STATUS reply type 3 added (set_reply), ID = 12, new count = 9
2025-07-22 10:48:22.033 DEBUG_QUERIES FTL_CNAME called with: src = pi.hole, dst = pi.hole, id = 12
2025-07-22 10:48:22.033 DEBUG_QUERIES Set global cache status to 0
2025-07-22 10:48:22.033 DEBUG_QUERIES pi.hole is not known
2025-07-22 10:48:22.033 DEBUG_QUERIES Checking if "pi.hole" is in antigravity (exact): no
2025-07-22 10:48:22.033 DEBUG_QUERIES Checking if "@@||hole^" is in antigravity (ABP): no
2025-07-22 10:48:22.033 DEBUG_QUERIES Checking if "@@||pi.hole^" is in antigravity (ABP): no
2025-07-22 10:48:22.033 DEBUG_QUERIES Checking if "pi.hole" is in gravity (exact): no
2025-07-22 10:48:22.033 DEBUG_QUERIES Checking if "||hole^" is in gravity (ABP): no
2025-07-22 10:48:22.033 DEBUG_QUERIES Checking if "||pi.hole^" is in gravity (ABP): no
2025-07-22 10:48:22.033 DEBUG_QUERIES DNS cache: A/192.168.1.120/pi.hole is not blocked (domainlist ID: -1)
2025-07-22 10:48:22.033 DEBUG_QUERIES Query 12: CNAME pi.hole ---> pi.hole

I have submitted CNAME records pointing to pi.hole return 0.0.0.0 · Issue #2581 · pi-hole/FTL · GitHub

1 Like

Ah, yes. Interesting that it took such a long time for this to come up :slight_smile: And thank you for summoning me via the Github ticket. I think it's easier to discuss on this here as all the relevant details are here, too.

What happens here is actually very interesting and by no means a "basic DNS request". ping requests A pihole.mydomain.net and receives both CNAME pi.hole and A 0.0.0.0. It then decides A 0.0.0.0 is not what it wants and issues another request for A pi.hole which then correctly resolves to A 127.0.0.1.

@robgill is absolutely right, we need to add special handling code in the CNAME path. I think I will make this by short-circuiting the CNAME lookup in a similar fashion so you will not receive the intermediate CNAME pi.hole but only the expected A/AAAA records.

I might have mislead you with the ping example. ping is not doing any special DNS evaluation. Most ping implementations will interpret 0.0.0.0 as 127.0.0.1:

$ ping 0.0.0.0
PING 0.0.0.0 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.050 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.039 ms
^C
--- 0.0.0.0 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 0.039/0.044/0.050/0.005 ms

You did not. I can confirm ping is doing what I said because I ran it on my laptop and watched it doing what I described in wireshark. It obtained the correct IP address of my Pi-hole on the network on a remote machine.

I pushed a first idea to the branch tweak/cname_pihole_pointer but it will need some more work concerning AAAA queries. This is rather tricky to get right in the deep CNAME inspection where we are not really concerned with query types but rather only with CNAME pointers.

Hmm.

I seem to observe ping just interpreting 0.0.0.0 as 127.0.0.1:

From a remote machine running debian:


$ ping -v pihole.mydomain.net

ping: sock4.fd: 3 (socktype: SOCK_DGRAM), sock6.fd: 4 (socktype: SOCK_DGRAM), hints.ai_family: AF_UNSPEC

ai->ai_family: AF_INET, ai->ai_canonname: 'pi.hole'

PING pi.hole (127.0.0.1) 56(84) bytes of data.

64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.034 ms

64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.040 ms

64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.038 ms

^C

--- pi.hole ping statistics ---

3 packets transmitted, 3 received, 0% packet loss, time 2052ms

rtt min/avg/max/mdev = 0.034/0.037/0.040/0.002 ms

$ ping -4 -v pi.hole

ping: sock4.fd: 3 (socktype: SOCK_DGRAM), sock6.fd: -1 (socktype: 0), hints.ai_family: AF_INET

ai->ai_family: AF_INET6, ai->ai_canonname: 'pi.hole'

ai->ai_family: AF_INET, ai->ai_canonname: ''

PING pi.hole (192.168.178.88) 56(84) bytes of data.

64 bytes from pi.hole (192.168.178.88): icmp_seq=1 ttl=64 time=0.537 ms

64 bytes from pi.hole (192.168.178.88): icmp_seq=2 ttl=64 time=0.617 ms

^C

--- pi.hole ping statistics ---

2 packets transmitted, 2 received, 0% packet loss, time 1001ms

rtt min/avg/max/mdev = 0.537/0.577/0.617/0.040 ms

With busybox ping:


$ ping pihole.mydomain.net

PING pihole.mydomain.net (0.0.0.0): 56 data bytes

64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.038 ms

64 bytes from 127.0.0.1: seq=1 ttl=64 time=0.063 ms

64 bytes from 127.0.0.1: seq=2 ttl=64 time=0.076 ms

^C

--- pihole.mydomain.net ping statistics ---

3 packets transmitted, 3 packets received, 0% packet loss

round-trip min/avg/max = 0.038/0.059/0.076 ms

$ ping pi.hole

PING pi.hole (192.168.178.88): 56 data bytes

64 bytes from 192.168.178.88: seq=0 ttl=64 time=0.633 ms

64 bytes from 192.168.178.88: seq=1 ttl=64 time=0.596 ms

64 bytes from 192.168.178.88: seq=2 ttl=64 time=0.518 ms

^C

--- pi.hole ping statistics ---

3 packets transmitted, 3 packets received, 0% packet loss

round-trip min/avg/max = 0.518/0.582/0.633 ms