Forwarding and recording external IPs when using dnsdist

The issue I am facing:

I have set up an open DNS-over-TCP resolver on a VPS. Port 53 is closed. dnsdist is capturing TLS traffic on 853 and forwarding it to pihole’s 53. My client devices connect to it from various IPs. Problem: I cannot setup dnsdist with pihole so that pihole’s logs would show external client IPs. All requests are coming from 127.0.0.1 according to pihole’s query log. Is that possible to implement, and if so, how?

Details about my system:

dnsdist 1.9.10

dnsdist.conf:
-- dnsdist configuration file

-- Allow queries from anywhere
setACL({'0.0.0.0/0', '::/0'})

-- disable security status polling via DNS
setSecurityPollSuffix("")

-- Backend Pi-hole server
newServer({address="127.0.0.1:53", name="pihole", useClientSubnet=true})

-- SSL certificate
addTLSLocal("0.0.0.0:853", "/etc/ssl/myssl.crtchain", "/etc/ssl/myssl.key", {
provider="openssl"
})

-- ECS for client IP preservation (this one should supposedly forward external IPs but it's not working in any form)
-- addAction(AllRule(), SetECSAction("0.0.0.0/32", "::/128")) – cannot start with this
-- addAction(AllRule(), SetECSAction(32, 128)) – cannot start with this
addAction(AllRule(), SetECSPrefixLengthAction(32,128))

-- Rate limiting
addAction(MaxQPSIPRule(50), DropAction())

-- Logging
setServerPolicy(firstAvailable)

/etc/pihole/pihole-FTL.conf:
EDNS0_ECS=true

What version of Pi-hole are you running?
/etc/pihole/pihole-FTL.conf is no longer used in Pi-hole v6.

All configuration is unified in /etc/pihole/pihole.toml. The new configuration option is dns.EDNS0ECS(enabled by default).

I am also running a dnsdist DoT server, but my version is dnsdist 2.0.1 (Lua 5.1.4 [LuaJIT 2.1.1737090214]). My configuration might not work for dnsdist 1.9.10. If you want to upgrade to version 2.x you may use the 2.x repositories for your distribution at https://repo.powerdns.com/ and upgrade the dnsdistpackage.

My full configuration:

-- dnsdist configuration file, an example can be found in /usr/share/doc/dnsdist/examples/

-- disable security status polling via DNS
setSecurityPollSuffix("")

-- listen for DoT on :853
addTLSLocal('0.0.0.0:853', '/etc/ssl/dnsdist/fullchain.pem', '/etc/ssl/dnsdist/privkey.pem')
addTLSLocal('[::]:853', '/etc/ssl/dnsdist/fullchain.pem', '/etc/ssl/dnsdist/privkey.pem')


newServer({
  address = "127.0.0.1:53",
  name = "pihole",
  pool = "pihole",
  healthCheckMode='lazy', checkInterval=1, lazyHealthCheckFailedInterval=30, rise=2, maxCheckFailures=3, lazyHealthCheckThreshold=30, lazyHealthCheckSampleSize=100,  lazyHealthCheckMinSampleCount=10, lazyHealthCheckMode='TimeoutOnly',
  mustResolve = true,        -- keep resolving if address is a hostname
  useClientSubnet=true
})

newServer({
  address = "127.0.0.1:5335",
  name = "unbound",
  pool = "unbound",
  healthCheckMode='lazy', checkInterval=1, lazyHealthCheckFailedInterval=30, rise=2, maxCheckFailures=3, lazyHealthCheckThreshold=30, lazyHealthCheckSampleSize=100,  lazyHealthCheckMinSampleCount=10, lazyHealthCheckMode='TimeoutOnly',
  mustResolve = true,        -- keep resolving if address is a hostname
  useClientSubnet=false
})

-- bypass pihole for certain queries (most of them are local and SHOULD return NXDOMAIN instead of the local record in Pi-hole)
addAction(QNameSuffixRule("fritz.box."), PoolAction("unbound"))
addAction(QNameSuffixRule("168.192.in-addr.arpa."), PoolAction("unbound"))
addAction(QNameSuffixRule("0.8.e.f.ip6.arpa."), PoolAction("unbound"))
addAction(QNameSuffixRule("d.f.ip6.arpa."), PoolAction("unbound"))
addAction(QNameSuffixRule("c.f.ip6.arpa."), PoolAction("unbound"))


-- forward all queries to the local backend
addAction(AllRule(), PoolAction("pihole"))

-- set ECS source prefix length to /32 for IPv4 and /128 for IPv6 (Full IP)
setECSSourcePrefixV4(32)
setECSSourcePrefixV6(128)
setECSOverride(true)

-- permit all origins
setACL({'0.0.0.0/0', '::/0'})

addAction(MaxQPSIPRule(20, 24, 64), DropAction())  -- 100 qps per /24 v4, /64 v6

-- setVerbose(true)

Relevant for ECS:

-- set ECS source prefix length to /32 for IPv4 and /128 for IPv6 (Full IP)
setECSSourcePrefixV4(32)
setECSSourcePrefixV6(128)
setECSOverride(true)

You already have useClientSubnet=true inside newServer()

Hey, thanks a lot! It was hard for me to figure it all out. Worked even with 1.9.10.

Now I have to wonder if there’s a way to tag outgoing requests, so that they’re not only identified by an IP address but also by some tag, like a custom device name. So it would look like “1.2.3.4 - home PC” in the log.