From 8d7e928ddbb2f9960f760e2634e1dd21d9c96585 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Wed, 9 May 2018 17:23:04 +0200 Subject: [PATCH] Initial Version --- .gitignore | 2 + Corefile | 10 ++++ Dockerfile | 42 +++++++++++++++ Makefile | 6 +++ build.sh | 22 ++++++++ build_stubs.py | 124 +++++++++++++++++++++++++++++++++++++++++++ docker-entrypoint.sh | 8 +++ named.conf | 15 ++++++ named.stubs.j2 | 8 +++ requirements.txt | 3 ++ 10 files changed, 240 insertions(+) create mode 100644 .gitignore create mode 100644 Corefile create mode 100644 Dockerfile create mode 100644 Makefile create mode 100755 build.sh create mode 100755 build_stubs.py create mode 100755 docker-entrypoint.sh create mode 100644 named.conf create mode 100644 named.stubs.j2 create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..922f11e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +blacklist +named.stubs diff --git a/Corefile b/Corefile new file mode 100644 index 0000000..c9389f1 --- /dev/null +++ b/Corefile @@ -0,0 +1,10 @@ +. { + hosts /etc/bind/blacklist { + fallthrough + } + + forward . 127.0.0.1:1053 + + errors + log +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a89cd5c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +FROM python:3-alpine as builder + +COPY . /src +WORKDIR /src + +RUN set -ex \ + && apk --no-cache add make \ + && pip install -r requirements.txt \ + && python build_stubs.py \ + && make blacklist + +# ------ + +FROM alpine:latest + +ENV DNSMASQ_HOSTSFILE=/etc/bind/blacklist \ + DNSMASQ_POLL=60 + +LABEL maintainer Knut Ahlers + +COPY build.sh /usr/local/bin/ + +RUN set -ex \ + && apk --no-cache add \ + bash \ + bind \ + bind-tools \ + && /usr/local/bin/build.sh + +COPY --from=builder /src/named.stubs /etc/bind/ +COPY --from=builder /src/blacklist /etc/bind/ + +COPY named.conf /etc/bind/ +COPY Corefile /etc/ +COPY docker-entrypoint.sh /usr/local/bin/ + +EXPOSE 53/udp 53 + +HEALTHCHECK --interval=30s --timeout=5s \ + CMD dig +short @localhost health.server.test A || exit 1 + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..75dfa60 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +default: + +blacklist: + curl -sSfL https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts | awk '/^(#.*|0.0.0.0.*|)$$/' > blacklist + # Add health check response + echo "0.0.0.0 health.server.test" >> blacklist diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..b2a9aa3 --- /dev/null +++ b/build.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -euxo pipefail + +# Install build utilities +apk --no-cache add curl + +# Get latest versions of tools using latestver +COREDNS_VERSION=$(curl -sSfL 'https://lv.luzifer.io/catalog-api/coredns/latest.txt?p=version') +DUMB_INIT_VERSION=$(curl -sSfL 'https://lv.luzifer.io/catalog-api/dumb-init/latest.txt?p=version') + +[ -z "${COREDNS_VERSION}" ] && { exit 1; } +[ -z "${DUMB_INIT_VERSION}" ] && { exit 1; } + +# Install tools +curl -sSfL https://github.com/coredns/coredns/releases/download/v${COREDNS_VERSION}/coredns_${COREDNS_VERSION}_linux_amd64.tgz | \ + tar -x -z -C /usr/local/bin + +curl -sSfLo /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v${DUMB_INIT_VERSION}/dumb-init_${DUMB_INIT_VERSION}_amd64 +chmod +x /usr/local/bin/dumb-init + +# Cleanup +apk --no-cache del curl diff --git a/build_stubs.py b/build_stubs.py new file mode 100755 index 0000000..c3e5290 --- /dev/null +++ b/build_stubs.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python + +import random +import re + +import requests +import jinja2 +import dns.resolver + + +BLACKLIST_FILE = 'https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts' +IANA_TLD_LIST = 'https://data.iana.org/TLD/tlds-alpha-by-domain.txt' +INTERNIC_ROOT_FILE = 'https://www.internic.net/domain/named.root' +OPENNIC_ROOT = '75.127.96.89' +OPENNIC_FILTER = ["..", "opennic.glue."] +IANA_FILTERS = ['arpa.'] + +roots = None + + +def get_generic_ip(fqdn): + res = dns.resolver.Resolver() + ans = res.query(fqdn, 'A') + + return ans.rrset.items[0].to_text() + + +def get_iana_tlds(): + tlds = requests.get(IANA_TLD_LIST).text.split("\n") + return [t.lower()+'.' for t in tlds if len(t) > 0 and t[0] != "#"] + + +def get_internic_roots(): + global roots + + if roots is not None: + return roots + + named_file = requests.get(INTERNIC_ROOT_FILE).text + roots = [] + for line in named_file.split("\n"): + match = re.search(r"\s+A\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$", line) + if match is None: + continue + roots.append(match.group(1)) + return roots + + +def get_iana_zone_master(zone): + query = dns.message.make_query(zone, dns.rdatatype.NS) + ans = dns.query.tcp(query, random.choice(get_internic_roots())) + + glue = {i.name.__str__(): i.items[0].address + for i in ans.additional + if i.rdtype == dns.rdatatype.A} + + auth = [i.to_text() for i in ans.authority[0].items] + + return [glue[i] for i in auth if i in glue] + + +def opennic_ip_from_master(master): + res = dns.resolver.Resolver() + res.nameservers = [OPENNIC_ROOT] + ans = res.query(master, 'A') + + return [i.to_text() for i in ans.rrset.items] + + +def get_opennic_zone_master(zone): + res = dns.resolver.Resolver() + res.nameservers = [OPENNIC_ROOT] + ans = res.query('{}.opennic.glue.'.format(zone.strip('.')), 'CNAME') + + masters = [t.strip('"') + for t in ans.rrset.items[0].to_text().split(" ") + if t.startswith('ns')] + masters.append('ns0.opennic.glue.') + master_ips = [opennic_ip_from_master(m) for m in masters] + return [item for sublist in master_ips for item in sublist] + + +def get_opennic_tlds(): + res = dns.resolver.Resolver() + res.nameservers = [OPENNIC_ROOT] + ans = res.query('tlds.opennic.glue.', 'TXT') + + return [t.strip('"')+'.' for t in ans.rrset.items[0].to_text().split(" ")] + + +def main(): + entries = { + "opennic.glue.": [OPENNIC_ROOT], + } + + iana_tlds = get_iana_tlds() + if len(iana_tlds) == 0: + raise Exception("No IANA TLDs found") + for tld in get_iana_tlds(): + if tld in IANA_FILTERS: + continue + print("Working on IANA TLD '{}'...".format(tld)) + entries[tld] = get_iana_zone_master(tld) + + opennic_tlds = get_opennic_tlds() + if len(opennic_tlds) == 0: + raise Exception("No OpenNIC TLDs found") + for tld in get_opennic_tlds(): + if tld in OPENNIC_FILTER: + continue + print("Working on OpenNIC TLD '{}'...".format(tld)) + entries[tld] = get_opennic_zone_master(tld) + + with open('named.stubs', 'w') as f: + f.write(render_corefile(entries)) + + +def render_corefile(entries): + template = jinja2.Template(open("named.stubs.j2").read()) + return template.render(entries=entries) + + +if __name__ == '__main__': + main() diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..0909aa3 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,8 @@ +#!/usr/local/bin/dumb-init /bin/bash +set -euxo pipefail + +# Start bind in background +named -p 1053 -c /etc/bind/named.conf -g & + +# Start coredns to filter blacklist +coredns -conf /etc/Corefile diff --git a/named.conf b/named.conf new file mode 100644 index 0000000..f55d05d --- /dev/null +++ b/named.conf @@ -0,0 +1,15 @@ +options { + directory "/var/bind"; + + allow-recursion { + 127.0.0.1/32; + }; + + listen-on port 1053 { 127.0.0.1; }; + + pid-file "/var/run/named/named.pid"; + + allow-transfer { none; }; +}; + +include "/etc/bind/named.stubs"; diff --git a/named.stubs.j2 b/named.stubs.j2 new file mode 100644 index 0000000..acef563 --- /dev/null +++ b/named.stubs.j2 @@ -0,0 +1,8 @@ +#{% for tld, to in entries.items() %} +zone "{{tld}}" in { + type static-stub; + server-addresses { {{ to | join('; ') }}; }; +}; +#{% endfor %} + +# vim: set ft=named: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0630b72 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +requests +dnspython +jinja2