From b2c0f1ab86eab4651ac89b5e0f0c2f284ced382a Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 22 Jul 2018 23:38:17 +0200 Subject: [PATCH] Removing BIND9. --- .dockerignore | 1 + Dockerfile | 11 ++----- README.md | 2 +- acme_tlsalpn.py | 1 + bind.conf | 18 ----------- controller.py | 83 +++++++----------------------------------------- dns_server.py | 75 +++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + run.sh | 3 +- 9 files changed, 93 insertions(+), 102 deletions(-) delete mode 100644 bind.conf create mode 100644 dns_server.py diff --git a/.dockerignore b/.dockerignore index 1519337..41e96b4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,6 +4,7 @@ !requirements.txt !run.sh !controller.py +!dns_server.py !acme_tlsalpn.py !README.md !LICENSE diff --git a/Dockerfile b/Dockerfile index d2cf591..2660e15 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,23 +7,16 @@ RUN go get -u github.com/letsencrypt/pebble/... && \ git checkout ${PEBBLE_CHECKOUT} && \ go install ./... -FROM debian:stretch-slim +FROM python:3.6-slim-stretch # Install software -RUN apt-get update \ - && apt-get install -y bind9 python3 python3-pip \ - && apt-get clean all \ - && rm -rf /var/lib/apt/lists/*; ADD requirements.txt /root/ RUN pip3 install -r /root/requirements.txt -# Setup bind9 -ADD bind.conf /etc/bind/named.conf -RUN mkdir /etc/bind/zones # Install pebble COPY --from=builder /go/bin /go/bin COPY --from=builder /go/pkg /go/pkg COPY --from=builder /go/src/github.com/letsencrypt/pebble/test /go/src/github.com/letsencrypt/pebble/test ADD pebble-config.json /go/src/github.com/letsencrypt/pebble/test/config/pebble-config.json # Setup controller.py and run.sh -ADD run.sh controller.py acme_tlsalpn.py LICENSE LICENSE-acme README.md /root/ +ADD run.sh controller.py dns_server.py acme_tlsalpn.py LICENSE LICENSE-acme README.md /root/ EXPOSE 5000 14000 CMD [ "/bin/sh", "-c", "/root/run.sh" ] diff --git a/README.md b/README.md index ed147a7..9fb13d5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A container for integration testing ACME protocol modules. -Uses [Pebble](https://github.com/letsencrypt/Pebble) and [BIND 9](https://www.isc.org/downloads/bind/). +Uses [Pebble](https://github.com/letsencrypt/Pebble). ## Usage diff --git a/acme_tlsalpn.py b/acme_tlsalpn.py index 5fad2d5..31a028d 100644 --- a/acme_tlsalpn.py +++ b/acme_tlsalpn.py @@ -212,6 +212,7 @@ def update(self): self.log_callback('Launching TLS ALPN challenge server...') self.server = TLSALPN01Server(("", self.port), certs=self.certs, challenge_certs=self.challenge_certs, log_callback=self.log_callback) self.thread = threading.Thread(target=self.server.serve_forever) + self.thread.daemon = True self.thread.start() diff --git a/bind.conf b/bind.conf deleted file mode 100644 index c0da075..0000000 --- a/bind.conf +++ /dev/null @@ -1,18 +0,0 @@ -options { - directory "/var/cache/bind"; - auth-nxdomain no; - listen-on-v6 { any; }; - listen-on { any; }; - statistics-file "/var/cache/bind/named.stats"; - rrset-order {order cyclic;}; - allow-transfer { none; }; - recursion no; -}; -zone "example.com." { - type master; - file "/etc/bind/zones/example.com"; -}; -zone "example.org." { - type master; - file "/etc/bind/zones/example.org"; -}; diff --git a/controller.py b/controller.py index 6499b82..b59ff09 100755 --- a/controller.py +++ b/controller.py @@ -22,27 +22,26 @@ import codecs import logging import os -import subprocess import sys +from functools import partial + from flask import Flask from flask import request from acme_tlsalpn import ALPNChallengeServer, gen_ss_cert from OpenSSL import crypto +from dns_server import DNSServer + app = Flask(__name__) app.config['LOGGER_HANDLER_POLICY'] = 'always' -ZONES_PATH = os.path.abspath(os.environ.get('ZONES_PATH', '.')) - PEBBLE_PATH = os.path.join(os.path.abspath(os.environ.get('GOPATH', '.')), 'src', 'github.com', 'letsencrypt', 'pebble') -zones = set(['example.com', 'example.org']) challenges = {} -txt_records = {} def log(message, data=None, program='Controller'): @@ -83,45 +82,6 @@ def emit(self, record): setup_loggers() -def execute(what, command): - try: - p = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - (so, se) = p.communicate() - data = [] - if so: - data.append('*** STDOUT:') - data.extend(so.decode('utf8').split('\n')) - if se: - data.append('*** STDERR:') - data.extend(se.decode('utf8').split('\n')) - log(what, data) - except Exception as e: - log('FAILED: {0}'.format(what), e) - - -def update_zone(zone, restart=True): - result = R"""$TTL 1 -@ IN SOA {0}. localhost. (1 1 1 1 1) -@ IN NS localhost. -@ IN A 127.0.0.1 -@ IN AAAA ::1 -* IN A 127.0.0.1 -* IN AAAA ::1 -""".format(zone) - for record, values in txt_records.get(zone, {}).items(): - for value in values: - result += '{0} IN TXT {1}\n'.format(record if record else '@', value) - log('Updating zone {0}'.format(zone), result.split('\n')) - with open(os.path.join(ZONES_PATH, zone), "wb") as f: - f.write(result.encode('utf-8')) - if restart: - execute('Restarting BIND', ['service', 'bind9', 'restart']) - - @app.route('/') def m_index(): return 'ACME test environment controller' @@ -144,32 +104,18 @@ def http_challenge(host, filename): return 'ok' +dns_server = DNSServer(port=53, log_callback=partial(log, program='DNS Server')) + + @app.route('/dns/', methods=['PUT', 'DELETE']) def dns_challenge(record): - i = record.rfind('.') - j = record.rfind('.', 0, i - 1) - if i >= 0 and j >= 0: - zone = record[j + 1:] - record = record[:j] - elif i >= 0: - zone = record - record = '' - else: - return 'cannot parse record', 400 - if zone not in zones: - return 'unknown zone "{0}"; must be one of {1}'.format(zone, ', '.join(zones)), 404 if request.method == 'PUT': - if zone not in txt_records: - txt_records[zone] = {} values = request.get_json(force=True) - log('Adding TXT records for zone {0}, record {1}'.format(zone, record), values) - txt_records[zone][record] = values + log('Adding TXT records for {0}'.format(record), values) + dns_server.set_txt_records(record, values) else: - if zone not in txt_records or record not in txt_records[zone]: - return 'not found', 404 - log('Removing TXT records for zone {0}, record {1}'.format(zone, record)) - del txt_records[zone][record] - update_zone(zone) + log('Removing TXT records for {0}'.format(record)) + dns_server.clear_txt_records(record) return 'ok' @@ -219,12 +165,5 @@ def get_root_certificate(): return f.read() -def setup_zones(): - for zone in zones: - update_zone(zone, restart=False) - execute('Starting BIND', ['service', 'bind9', 'start']) - - if __name__ == "__main__": - setup_zones() app.run(debug=False, host='0.0.0.0', port=int(os.environ.get('CONTROLLER_PORT', 5000))) diff --git a/dns_server.py b/dns_server.py new file mode 100644 index 0000000..ca0a440 --- /dev/null +++ b/dns_server.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +from dnslib import A, TXT, QTYPE, RCODE, server + + +class DNSLogger(object): + def __init__(self, log_callback): + self.log_callback = log_callback + + def log_pass(self, *args): + pass + + def log_recv(self, handler, data): + pass + + def log_send(self, handler, data): + pass + + def log_request(self, handler, request): + self.log_callback("DNS Request: [{0}:{1}] ({2}) <{3}> : {4}".format(handler.client_address[0], handler.client_address[1], handler.protocol, request.q.qname, QTYPE[request.q.qtype]), data=str(request.toZone("")).split('\n')) + + def log_reply(self, handler, reply): + if reply.header.rcode == RCODE.NOERROR: + self.log_callback("DNS Reply: [{0}:{1}] ({2}) / '{3}' ({4}) / RRs: {5}".format(handler.client_address[0], handler.client_address[1], handler.protocol, reply.q.qname, QTYPE[reply.q.qtype], ",".join([QTYPE[a.rtype] for a in reply.rr])), data=str(reply.toZone("")).split('\n')) + else: + self.log_callback("DNS Reply: [{0}:{1}] ({2}) / '{3}' ({4}) / {5}".format(handler.client_address[0], handler.client_address[1], handler.protocol, reply.q.qname, QTYPE[reply.q.qtype], RCODE[reply.header.rcode]), data=str(reply.toZone("")).split('\n')) + + def log_truncated(self, handler, reply): + self.log_callback("DNS Truncated Reply: [{0}:{1}] ({2}) / '{3}' ({4}) / RRs: {5}".format(handler.client_address[0], handler.client_address[1], handler.protocol, reply.q.qname, QTYPE[reply.q.qtype], ",".join([QTYPE[a.rtype] for a in reply.rr])), data=str(reply.toZone("")).split('\n')) + + def log_error(self, handler, e): + self.log_callback("DNS Invalid Request: [{0}:{1}] ({2}) :: {3}".format(handler.client_address[0], handler.client_address[1], handler.protocol, e)) + + +class DNSServer(object): + def resolve(self, request, handler): + reply = request.reply() + if request.q.qtype == QTYPE.ANY or request.q.qtype == QTYPE.A: + reply.add_answer(server.RR(rname=request.q.qname, rtype=QTYPE.A, rdata=A("127.0.0.1"), ttl=10)) + if request.q.qtype == QTYPE.ANY or request.q.qtype == QTYPE.TXT: + records = self.txt_records.get(str(request.q.qname), []) + for record in records: + reply.add_answer(server.RR(rname=request.q.qname, rtype=QTYPE.TXT, rdata=TXT(record), ttl=10)) + return reply + + def __init__(self, port, log_callback=None): + if log_callback is None: + def f(msg, data=None): + print(msg) + if data is not None: + print(data) + + log_callback = f + + self.txt_records = {} + self.log_callback = log_callback + self.port = port + self.logger = DNSLogger(self.log_callback) + self.servers = [ + server.DNSServer(self, address="localhost", port=self.port, tcp=False, logger=self.logger), + server.DNSServer(self, address="localhost", port=self.port, tcp=True, logger=self.logger), + ] + for ds in self.servers: + ds.start_thread() + + def _cleanup(self, zone): + if not zone.endswith('.'): + zone = zone + '.' + return zone + + def set_txt_records(self, zone, values): + self.txt_records[self._cleanup(zone)] = values + + def clear_txt_records(self, zone): + self.txt_records[self._cleanup(zone)] = [] diff --git a/requirements.txt b/requirements.txt index 4c47167..8dbc7ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Flask==1.0.2 Werkzeug==0.14.1 pyOpenSSL==18.0.0 +dnslib==0.9.7 diff --git a/run.sh b/run.sh index eb8b955..8652244 100755 --- a/run.sh +++ b/run.sh @@ -4,8 +4,7 @@ set -e echo nameserver 127.0.0.1 > /etc/resolv.conf # Start controller in background export CONTROLLER_PORT=5000 -export ZONES_PATH=/etc/bind/zones -/usr/bin/python3 /root/controller.py & +/usr/local/bin/python /root/controller.py & # Make Pebble sleep at most 5 seconds between auth checks (default is 15 seconds) export PEBBLE_VA_SLEEPTIME=5 # Start Pebble