Per device DNS settings: selective parental control

This is the second post in a series that highlights interesting new features of the PowerDNS 4.x.x and dnsdist 1.x.x releases.

As noted previously, in some organizations it is desirable to filter access to some domains (malware, advertising, ‘adult’ etc) for some users or subscribers. In our previous post we showed how to use common DNS blocking lists to offer adblocking for users of your resolver.

A problemacm however for other kinds of filtering is that subscribers typically share a single IP address among all their devices. This makes it hard to provide ‘parental control’ to the kids’ tablets, but not to other computers in the household.

Now, it would be great if we could do selective filtering based on the device’s MAC address, but this MAC address is not visible from the nameserver. All we see is the MAC address of our router, not that of your devices.

It turns out however that various home routers and cable modems (‘CPEs’) have the option to embed requestor MAC addresses into DNS queries themselves. This allows a service provider to conditionally filter DNS based on this MAC address.

A quick survey discovered that different vendors embed the MAC address or other ‘user identifiers’ differently, and for this reason PowerDNS support for this feature is dynamic.

First, we’ll use dnsdist to embed the MAC address in queries. The natural and universally chosen mechanism for this is an EDNS option, and we can configure dnsdist to add the MAC address like this:


addLocal("0.0.0.0")
newServer("192.168.5.123:5300")
addAction(AllRule(), MacAddrAction(65001))
— using LuaAction, the MAC address could be hashed or truncated, for increased privacy

This makes dnsdist listen on port 53 and forward all queries to 127.0.0.1:5300. With the last line, we add the MAC address of the requestor to all queries using EDNS option code point 5 (65001 is also used). We could restrict this to certain IP addresses if we wanted. In a typical setup, this is how dnsdist would run on a subscriber’s CPE.

Next up we need to configure the PowerDNS Recursor with our filtering instructions:


filter={}
filter["192.168.5.24"]={["b8:27:eb:0c:88:27"]=1, ["00:0d:b9:36:6f:79"]= 1}
filter["10.0.0.1"]={["06:31:25:7a:84:6b"]=1}
note that the filtering could be more than binary, but specify lots of categories
see https://i.imgur.com/wGwNHl7.png for inspiration
baddomains=newDS()
baddomains:add("xxx")

view raw

macfilter.lua

hosted with ❤ by GitHub

These lines implement a set of bad domains, in this case everything within the TLD “xxx”. Secondly, for two IP addresses we list some MAC addresses that desire filtering. 127.0.0.1 is in this case for the dnsdist configuration listed above.

Now to make use of these filters:


load this and the previous snippet as 'lua-dns-script=macfilter.lua'
function macPrint(a)
return string.format("%02x:%02x:%02x:%02x:%02x:%02x", a:byte(1), a:byte(2), a:byte(3), a:byte(4), a:byte(5), a:byte(6))
end
function preresolve(dq)
print("Got question for "..dq.qname:toString().." from "..dq.remoteaddr:toString().." to "..dq.localaddr:toString())
local a=dq:getEDNSOption(65001)
if(a ~= nil) then
print("There is an EDNS option 65001 present: "..macPrint(a))
if(filter[dq.remoteaddr:toString()][macPrint(a)] and baddomains:check(dq.qname)) then
print("Wanted filtering")
dq:addAnswer(pdns.CNAME, "blockingserver.powerdns.com")
return true
end
an obvious enhancement is to implement a 'default' mac address describing the default
filtering an IP address wants
end
return false
end

view raw

macfilter-2.lua

hosted with ❤ by GitHub

This first defines macPrint() to pretty print the binary encoded MAC address. Secondly, we use the PowerDNS Recursor Lua hook ‘preresolve’ to intercept incoming queries. First we see if there is an embedded MAC address in EDNS option 5, and if there is, we look up if for that IP address, we have that MAC address listed.

 

If so, and the query is part of our ‘baddomains’ list, we return a CNAME to a server which would then inform the user their request got blocked:


pi@raspberrypi ~ $ /sbin/ifconfig eth0 | head -1
eth0 Link encap:Ethernet HWaddr b8:27:eb:0c:88:27
pi@raspberrypi ~ $ dig http://www.ds9a.xxx @192.168.5.24 +short
blockingserver.powerdns.com.
ahu@ahucer:~$ /sbin/ifconfig eth0 | head -1
eth0 Link encap:Ethernet HWaddr 90:fb:e9:3b:61:dc
ahu@ahucer:~$ dig http://www.ds9a.xxx @192.168.5.24
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 10412

view raw

try-it-out

hosted with ❤ by GitHub

The last two lines are from a host with a different MAC address which is not filtered.

Takeaway

With the configuration outlined above, it is easily possible to provide per-device DNS filtering instructions. As shown, this configuration is not very dynamic. PowerDNS however has multiple ways to retrieve such filtering instructions, either from remote servers or from local indexed files.

And please do not hesitate to contact us if you need help integrating PowerDNS with your existing customer database or CPE deployed base!

Enjoy!

9 comments

  1. johnd1313

    Can anyone explain this LUA error I am receiving?

    pdns_recursor[2517]: Failed to load ‘lua’ script from ‘/etc/pdns-recursor/macfilter.lua’: Error loading Lua file ‘/etc/pdns-recursor/macfilter.lua’: /etc/pdns-recursor/macfilter.lua:7: attempt to call global ‘newDS’ (a nil value)

    # cat /etc/pdns-recursor/macfilter.lua
    filter={}
    –filter[“192.168.5.24”]={[“b8:27:eb:0c:88:27”]=1, [“00:0d:b9:36:6f:79”]= 1}
    filter[“135.23.221.8”]={[“34:02:86:19:2A:3F”]=1}

    — note that the filtering could be more than binary, but specify lots of categories
    — see https://i.imgur.com/wGwNHl7.png for inspiration
    baddomains=newDS()
    baddomains:add(“xxx”)

    function macPrint(a)
    return string.format(“%02x:%02x:%02x:%02x:%02x:%02x”, a:byte(1), a:byte(2), a:byte(3), a:byte(4), a:byte(5), a:byte(6))
    end

    function preresolve(dq)
    print(“Got question for “..dq.qname:toString()..” from “..dq.remoteaddr:toString()..” to “..dq.localaddr:toString())

    local a=dq:getEDNSOption(65001)
    if(a ~= nil) then
    print(“There is an EDNS option 65001 present: “..macPrint(a))
    if(filter[dq.remoteaddr:toString()][macPrint(a)] and baddomains:check(dq.qname)) then
    print(“Wanted filtering”)
    dq:addAnswer(pdns.CNAME, “blockingserver.powerdns.com”)
    return true
    end
    — an obvious enhancement is to implement a ‘default’ mac address describing the default
    — filtering an IP address wants
    end
    return false
    end

  2. johnd1313

    Ok but now I am receiving a different error when starting the pdns recursor

    Failed to load ‘lua’ script from ‘/etc/pdns-recursor/macfilter.lua’: Attempt to load a Lua script in a PowerDNS binary without Lua support

    I compiled with LUA support by performing the following steps, perhaps I am missing something?

    # ./bootstrap
    # ./configure
    # LUA=1 make
    # make install

  3. Pingback: PowerDNS Recursor 4.0.0 Alpha 2 released | PowerDNS Blog
  4. habbie

    dnsmasq’s –add-mac option is implemented identically to the 65001 dnsdist/recursor example in this post – so pointing dnsmasq –add-mac at a Recursor that expects a 6 byte MAC stuffed in option 65001 just works with the example code above!

  5. dykh

    Hi,
    just trying to implement ECS for getting the mac address of my devices which are connected to my AP. But never have mac adresses while checking the trace of my recursor.
    any help please…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s