Make Changes

Improving on a good thing

Kyle Harding's Docker image and Oznu's zone list are great, and a wealth of information when you scan through their Dockerfile, and javascript. I'll bet it inspires your own ideas.

Javascript

My first thought was to port the javascript to Golang. I liked the template usage and knew Golang supports templates. Also, having 3 output formats made me wonder whether I could make each into a transform step of a pipeline.

Let's focus on the templates. Right away, we learn that there are two ways to designate a domain in our sinkhole:

  1. redirect - the domain is mapped to a local IP address. This can give you the option to spin up a web server to respond with a pixel or your own content in place of the ad. The IP 0.0.0.0 can be chosen when running without the web server.
  2. nxdomain - the domain is nonexistent. This is the response to signal clients that the domain is not defined.

This explains why for each of the three formats (BIND / Unbound / Dnsmasq), there are always two …templates. For Unbound, the documentation shows local-zone examples that use a dot terminator. So the port from javascript to Golang becomes:

const (
unbtempl = `
local-zone: "{{.}}." redirect
local-data: "{{.}}. A 0.0.0.0"`

unbnxdomain = `
local-zone: "{{.}}." always_nxdomain`
)

Next, the question is whether subdomains get folded under the parent. In other words, will blocking the parent automatically suppress its descendants?

According to the unbound.conf.5 man page, the local-zone definition already includes subdomains:

       redirect
            The query is answered from the local data for the zone  name.
            There  may  be  no  local  data  beneath the zone name.  This
            answers queries for the zone, and all subdomains of the  zone
            with the local data for the zone.  It can be used to redirect
            a domain to return a different  address  record  to  the  end
            user,    with   local-zone:   "example.com."   redirect   and
            local-data: "example.com. A 127.0.0.1" queries for  www.exam-
            ple.com and www.foo.example.com are redirected, so that users
            with web browsers  cannot  access  sites  with  suffix  exam-
            ple.com.

Which makes sense intuitively, and behavior we definitely want to take advantage of. Our approach is to track each domain inside a map.

Then the way we detect a subdomain is:

  1. Chop off string to the left of leftmost dot.
  2. Use the result from step 1 as the parent.
  3. Check whether the parent exists in our tracking map.
  4. If parent exists, then this subdomain is already covered. Continue to next host name.
  5. If parent doesn't exist, record the host name in tracking map.
// calc parent domain
var i = strings.IndexRune(host, '.')
if i != -1 && i+1 < len(host) {
var parent = host[i+1:]
if _, ok := m[parent]; ok {
// The parent domain has already been recorded.
// We can skip this subdomain.
return ""
}
}
if _, ok := m[host]; !ok {
// This is the first instance of the host name.
m[host] = true
return host
}

Those are key highlights of the Golang port. Some implementation we skipped, like sorting and whitelisting. So we’ll revisit again in the future.

The whole port is available on the Github repo

Docker

Going into the Docker image, the first change is usually to switch base to Alpine. On examining the Dockerfile, we see that it already uses Alpine. So our real modification is to incorporate our Golang port (redirect zones) which configures Unbound into a sinkhole.

Wishlist:

  • Use Alpine Linux base image
  • Include redirect zones
  • Install Unbound via the package manager, apk

redirect zones

To create the file of redirect zones (unbound.bl), we compile our Golang port and run it inside the container. Most of the work is done for us because we start with the Golang base image.

RUN go build -o /go/bin/sinkhole cmd/*.go ; \
/go/bin/sinkhole ; \

Also, note that we separate the Dockerfile into two stages. The first stage is build-env dedicated to the preparation of the redirect zones. It's very convenient for doing tasks and discarding unnecessary artifacts.

Unbound

With Unbound, we choose the package manager (apk) install. This is the major difference from Kyle's Dockerfile. So it was a relief that the apk package works without any tinkering. I think it's also worth pointing out that the package includes the root cache (roots.hint) which saves a step in one of the guides we followed (recursive DNS).

# final stage
FROM alpine:3.11
RUN apk update && apk --no-cache add unbound ; \
rm -rf /apk /tmp/* /var/cache/apk/* 

Conclusion

Learning from the work of others sparked questions. Answering questions meant experiments with redirect and nxdomain configuration. Doing experiments reminded us how Docker saves a lot of repitition.

Again the Github repo contains our Golang port, and Dockerfile. The CI is also configured to auto build on Docker hub:

docker run -ti -p 5300:5300/udp patterns/sinkhole