about:drewcsillag

Musings of a programmer, musician, photographer, and Christian.

Website Whitelisting

For my Church’s public wifi network, my pastor wanted to only allow access to sites from a whitelist. Doing this turned out to be a bit trickier than we thought.

The wifi router we have, like many others, has ways to catch keywords and things like that in website addresses and such. The catch is that it doesn’t work as soon as you put https:// in front of the address. We could do something like SonicWall reputedly, but they’re more money than we wanted to spend.

Two other guys and I went around a few times trying to figure out what to do when I came up with the idea that if we used DNS whitelisting, that might just be the ticket. While yes, you can change the DNS setting of your laptop, most don’t know how, and that may or may not even be an option for tablets (we presume that phones can access whatever). In reality, we realize that if it’s someone like us (IT professionals), they can probably find a way around it, our plan was really just to inhibit people from getting where it was decided they shouldn’t be if they’re on our wifi.

Digging around for whitelisting for DNS was surprisingly unfruitful, until one of the guys suggested taking something like bind or PowerDNS and just creating zone files for it. Not the most elegant of solutions, but it sounded workable.

We didn’t have any existing servers kicking around for this DNS whitelisting thing to run on, but I had an old Raspberry Pi kicking around, which for DNS, seemed reasonable enough. I gave it to one of the other guys who set it up using PowerDNS and created some zone files for the websites we wanted to whitelist.

It turned out, after some testing that it really didn’t work, and couldn’t for what we wanted to do. But I did notice that PowerDNS did have a lua interface which seemed like some place we could plug in. I tried to get it going, but for some reason it didn’t seem like it was even loading the lua file, so I compiled it myself from source, and tried again, to no avail. The documentation is clear, why doesn’t this thing work!?!?!

After a bit of digging around, I then realized that we had been using (and I had rebuilt) the authoritative server. The PowerDNS server comes in two flavors, the authoritative server, and the recursive resolver. The authoritative server is the one you’d use to advertise your DNS entries to the world, and the recursive resolver is the one you’d use to look up entries out in the world at large.

Now realizing this, I built the recursive resolver, and wrote my lua script and Eureka! It worked exactly like I hoped. My lua script basically said, “if it’s one of these domains, do what you’d normally do, otherwise say it doesn’t exist”. It also turns out our wifi router can force all DNS traffic to this box, so even if someone changes their DNS, we can still intercept and redirect to the Raspberry Pi!

So again, is it bullet proof? No, but it satisfies pastor’s requirements and does a pretty good job of it so far.

We did wind up whitelisting a bunch of random things, as websites use CDNs and load javascript libraries from various places, so the whitelist isn’t as “clean” as I would like, but as a solution, I’m pretty happy.

So if you’re looking to do DNS whitelisting, the code below should do it. Alternatively, you can modify the code below slightly to do blacklisting instead if you chose; just swap the returns of -1, {} to 3, {}. I don’t know at all if it’s idiomatic lua, so apologies if I do things weird – I spent all of 10 minutes learning the lua I used to write this.

At some point I might add regexes or use tables as tables instead of as an array, but for now the use case is pretty simple. I’m thinking of doing a blacklist for my home network using an excludes list like from here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
domains = {
    "biblegateway.com",
    "ifca.org",
    -- many others here
}

function string.ends(String,End)
   return End=='' or string.sub(String,-string.len(End))==End
end

function preresolve ( remoteip, domain, qtype )
  print ("preresolve called for: ", ip, domain, qtype)

  -- if the domain is either == to one of the domain above, or a subdomain
  -- of it, let it pass
  for k, wldomain in pairs(domains)
  do
      domainToCheck = wldomain .. "."
      if domainToCheck == domain
      then
          return -1, {}
      end

      subDomainToCheck = "." .. domainToCheck

      if string.ends(domain, subDomainToCheck)
      then
          return -1, {}
      end
  end

  -- return NXDOMAIN which means not exists
  return 3, {}
end