Recently, I made an article about how to use Bind9 with LXC containers, setup including domain name spoofing/caching. This setup was using Bind9 views so only the caching LXC container would get real IP for cached domains (like ftp.fr.debian.org or packages.devuan.org) so nginx, on this system, could mirror accordingly.

Then Debian 9.0 was released and I found out two views were no longer allowed to share writing rights to a single same zone definition file.

You would then get error like “writeable file ‘/etc/bind/…’: already in use: /etc/bind/…”.

As sacrifial-spam-address wrote:

At first, I thought this was a bug (how can a config file line conflict

with itself?), then I realized that the conflict was between the

two views. There does not appear to be any simple workaround. The best solution

appears to be to use the new BIND 9.10 “in-view” feature, which allows a

zone in one view to be a reference to the same zone in a different view.

When this is done, both views may share the same cache file. The down side is that this violates one of the important principles of

programming: only specify something in one place. Instead, I have to have

a “master” definition and several “in-view” declarations referencing

the master. I wish BIND would either deal with the problem after noticing it (by

automatically doing the equivalent of the in-view), or provide a way to

import every zone in a view, avoiding the need for a long list of in-view

declarations.

Then I fixed my setup to work with in-view, updating the article already linked. But the experience was clearly unsatisfying, adding one more layer of complexity to something already quite dense.

Plus I got some error in this new setup: it seemed that in-view, at least the way I configured, cause the different views to behave as if they share a same cache. Say, after Bind9 startup, I pinged ftp.fr.debian.org from any LXC container but the cache one, I would get the IP of the LXC cache container as it should be. But, then, if I pinged the same domain from the LXC cache container, I would still get as answer its own IP, as if there was not two different views setup. Same with the opposited test, if the first ping was from within the LXC cache container, then from any other, I would get the result wanted only for the LXC cache container.

So it lead me to the point that I had to understand better the in-view feature that in first place I did not want to use, in order to get it to behave like view did.

You got it: I found much easier to user PowerDNS instead.

PowerDNS (pdns) is composed of an authoritative DNS server plus a DNS recursor. The first one I only need on this setup for the LAN domain name (quite used: LXC containers + connected devices). The recursor is doing some caching. And can be easily scripted.

apt-get install pdns-server pdns-recursor pdns-backend-sqlite3

Often, when both the name server and the recursor are installed on the same machine, people set up the name server to listen on port 53 on the network and to pass to the recursor, listening on another port, requests it cannot handle (that it is not authoritative for and need a recursor to resolve then).

Ok, why not. Except that I want specific answer to be given depending on the querier’s IP for domains outside of the LAN, so handled by the recursor. That would not work if the recursor get queries sent over loopback device by the authoritative server.

Aside from that, just as general principle, I like better the notion of, by default, soliciting a recursor that, only when necessary, ask the local DNS server instead of other DNS than the notion of asking a DNS server to handle queries that he is most of the time unlikely to be have authoritative answer for and that he’ll have to pass to a recursor.

So instead of the usual proposed :

client -> local DNS server -> DNS recursor if non authoritative -> distant authoritative DNS server

It’ll be:

client -> DNS recursor -> authoritative DNS server (local or distant).

authoritative PowerDNS server

First we deal with the DNS server to server YOURDOMAIN.LAN. The sqlite3 backend should be installed and set up (or else of your liking).

By default, the sqlite3 database is in /var/lib/powerdns/pdns.sqlite3

Easiest way is to convert Bind9 zone config to set it up:

zone2sql --named-conf=/etc/bind/named.conf.local --gsqlite | sqlite3 /var/lib/powerdns/pdns.sqlite3

That’s all!

As alternative, you can also create zone from scratch with pdnsutil:

cd /var/lib/powerdns sqlite3 pdns.sqlite3 < /usr/share/doc/pdns-backend-sqlite3/schema.sqlite3.sql chown pdns:pdns pdns.sqlite3 # main zone pdnsutil create-zone YOURDOMAIN.LAN ns1.YOURDOMAIN.LAN pdnsutil add-record YOURDOMAIN.LAN main A 192.168.1.1 pdnsutil add-record YOURDOMAIN.LAN @ MX "10 mx.YOURDOMAIN.LAN" [...] # first reverse zone 192.168.1 pdnsutil create-zone 1.168.192.in-addr.arpa ns1.YOURDOMAIN.LAN pdnsutil add-record 1.168.192.in-addr.arpa 1 PTR main.YOURDOMAIN.LAN [...] # to be continued

In our previous setup, we had DNS update automated by ISC DCPDd: we want any new host on the local network to be given an IP. Nothing changed regarding ISC DHCPd, read the relevant ISC DHCPd setup part. For the record, to generate the relevant update key:

cd /etc/dhcp dnssec-keygen -a hmac-md5 -b 256 -n USER ddns

The secret will be a string like XXXXX== within the ddns.key generated file.

Obviously, powerdns needs this data. You need to register the key+secret and give right on each zone (YOURDOMAIN.LAN plus the reverse for IP ranges, below for 192.168.1 and 10.0.0)

sqlite3 /var/lib/powerdns/pdns.sqlite3 # XXXXX== = the secret string insert into tsigkeys (name, algorithm, secret) values ('ddns', 'hmac-md5','XXXXX=='); # find out ids of zones select id from domains where name='YOURDOMAIN.LAN'; 2 select id from domains where name='1.168.192.in-addr.arpa'; 1 select id from domains where name='0.0.10.in-addr.arpa'; 3 # authorized the key for each insert into domainmetadata (domain_id, kind, content) values (1, 'TSIG-ALLOW-DNSUPDATE', 'ddns'); insert into domainmetadata (domain_id, kind, content) values (2, 'TSIG-ALLOW-DNSUPDATE', 'ddns'); insert into domainmetadata (domain_id, kind, content) values (3, 'TSIG-ALLOW-DNSUPDATE', 'ddns');

Finally, you need to configure powerdns itself. You can directly edit /etc/powerdns/pdns.conf but I think easier to create a specific /etc/powerdns/pdns.d/00-pdns.conf so you do not edit the default example:

# base daemon=yes local-address=127.0.0.1 local-port=53 local-address-nonexist-fail=no local-ipv6= # dynamic updates dnsupdate=yes allow-dnsupdate-from=127.0.0.1

Note it is an IPv4 only setup. It’ll listen only on loopback interface, since no one is supposed to contact him directly beside the recursor sitting on the same loopback.

You can restart the daemon (rc-service pdns restart with OpenRC, else depending on your init).

PowerDNS recursor :

It is quite straighforward to configure in /etc/powerdns/recursor.conf, this one will listen on LAN addresses (not the loopback):

# restrict netmask allowed to query allow-from=127.0.0.1, 10.0.0.0/24, 192.168.1.0/24 daemon=yes # for local domain, via loopback device, forward queries to the PowerDNS local authoritative server forward-zones=YOURDOMAIN.LAN=127.0.0.1, 1.168.192.in-addr.arpa=127.0.0.1, 0.0.10.in-addr.arpa=127.0.0.1 # list of IP to listen to local-address=10.0.0.1, 192.168.1.1 local-port=53 # that is how we will spoof/cache lua-dns-script=/etc/powerdns/redirect.lua

So all the magic will be done in the /etc/powerdns/redirect.lua script, where the cache LXC container IP is hardcoded (that could be change in future version if necessary):

-- (requires pdns-recursor 4 at least) -- cached servers cached = newDS() cachedest = "10.0.0.88" -- ads kill list ads = newDS() adsdest = "127.0.0.1" -- hand maintained black list blacklisted = newDS() blacklistdest = "127.0.0.1" function preresolve(dq) -- DEBUG --pdnslog("Got question for "..dq.qname:toString().." from "..dq.remoteaddr:toString().." to "..dq.localaddr:toString(), pdns.loglevels.Error) -- handmade domains blacklist if(blacklisted:check(dq.qname)) then if(dq.qtype == pdns.A) then dq:addAnswer(dq.qtype, blacklistdest) return true end end -- spam/ads domains if(ads:check(dq.qname)) then if(dq.qtype == pdns.A) then dq:addAnswer(dq.qtype, adsdest) return true end end -- cached domains if(not cached:check(dq.qname)) then -- not cached return false else -- cached: variable answer dq.variable = true -- request coming from the cache itself if(dq.remoteaddr:equal(newCA(cachedest))) then return false end -- redirect to the cache if(dq.qtype == pdns.A) then dq:addAnswer(dq.qtype, cachedest) end end return true end cached:add(dofile("/etc/powerdns/redirect-cached.lua")) ads:add(dofile("/etc/powerdns/redirect-ads.lua")) blacklisted:add(dofile("/etc/powerdns/redirect-blacklisted.lua"))

This script relies on three files to do its magic.

redirect-blacklisted.lua that is hand made blacklist, the default content is:

return{ --"gfe.nvidia.com", }

redirect-cached.lua is to be generated by redirect-cached-rebuild.sh that you should edit before running, to list which domains you want to cache:

#!/bin/sh DOMAINS="" # comment this if you dont cache steam # (note: nginx cache must also cover this) DOMAINS="$DOMAINS cs.steampowered.com content1.steampowered.com content2.steampowered.com content3.steampowered.com content4.steampowered.com content5.steampowered.com content6.steampowered.com content7.steampowered.com content8.steampowered.com content9.steampowered.com hsar.steampowered.com.edgesuite.net akamai.steamstatic.com content-origin.steampowered.com client-download.steampowered.com steampipe.steamcontent.com steamcontent.com" # comment this if you dont cache debian # (useful read: https://wiki.debian.org/DebianGeoMirror ) DOMAINS="$DOMAINS cdn-fastly.deb.debian.org ftp.fr.debian.org ftp.de.debian.org ftp.debian.org security.debian.org" # comment this if you dont cache devuan DOMAINS="$DOMAINS packages.devuan.org amprolla.devuan.org" # comment this if you dont cache ubuntu DOMAINS="$DOMAINS fr.archive.ubuntu.com security.ubuntu.com" out=redirect-cached.lua echo "-- build by ${0}" > $out echo "-- re-run it commenting relevant domains if you dont cache them all" >> $out echo "return{" >> $out for domain in $DOMAINS; do echo \"$domain\", >> $out done echo "}" >> $out # EOF

Finally, redirect-ads.lua is to be generated by redirect-ads-rebuild.pl that you put in a weekly cronjob (following by a pdns-recursor restart):

use strict; use Fcntl ':flock'; # disallow concurrent run open(LOCK, "< $0") or die "Failed to ask lock. Exiting"; flock(LOCK, LOCK_EX | LOCK_NB) or die "Unable to lock. This daemon is already alive. Exiting"; open(OUT, "> redirect-ads.lua"); # You can choose between wget or curl. Both rock! # my $snagger = "curl -q"; my $snagger = "wget -q -O - "; # List of URLs to find ad servers. my @urls = ("http://pgl.yoyo.org/adservers/serverlist.php?showintro=0;hostformat=one-line;mimetype=plaintext"); print OUT "return{

"; # Grab the list of domains and add them to the realm file foreach my $url (@urls) { # Open the curl command open(CURL, "$snagger \"$url\" |") || die "Cannot execute $snagger: $@

"; printf OUT ("--- Added domains on %s --

", scalar localtime); while () { next if /^#/; next if /^$/; chomp(); foreach my $domain (split(",")) { print OUT "\"$domain\",

"; } } } print OUT "}

";

So before starting the recursor, run redirect-ads-rebuild.pl and redirect-cached-rebuild.sh.

Then, after restart, everything should be up and running, with no concerns of inconsistent issues. And, as you can see for yourself, the LUA scripting possibility is as easy as extensible.