├── .gitignore ├── dnsrest ├── __init__.py ├── logger.py ├── nodez_test.py ├── namesrv.py ├── monitor.py ├── nodez.py ├── restapi.py └── registry.py ├── .dockerignore ├── requirements.txt ├── Dockerfile ├── bootstrap ├── docker_dnsrest └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | .vscode 4 | -------------------------------------------------------------------------------- /dnsrest/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = '0.1' 3 | 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | build 2 | .git 3 | .gitignore 4 | *.swp 5 | 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | dnslib==0.9.7 3 | docker-py==1.10.6 4 | falcon==1.4.1 5 | gevent==1.3.7 6 | 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.8 2 | LABEL maintainer="Patrick Hensley " 3 | COPY . /data 4 | RUN /data/bootstrap 5 | EXPOSE 80 6 | ENTRYPOINT ["/data/docker_dnsrest"] 7 | 8 | -------------------------------------------------------------------------------- /bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PY=3.6.6-r0 4 | apk add --update python3=$PY python3-dev=$PY gcc libgcc libc-dev py3-pip libev 5 | pip3 install --upgrade pip 6 | pip3 install -r /data/requirements.txt 7 | if [ ! -e /usr/bin/pip ]; then ln -s pip3 /usr/bin/pip ; fi && \ 8 | if [[ ! -e /usr/bin/python ]]; then ln -sf /usr/bin/python3 /usr/bin/python; fi && \ 9 | apk del python3-dev gcc libgcc libc-dev py-pip libev 10 | rm -rf /tmp/* 11 | rm -rf /var/cache/apk/* 12 | 13 | -------------------------------------------------------------------------------- /dnsrest/logger.py: -------------------------------------------------------------------------------- 1 | 2 | # core 3 | from datetime import datetime 4 | import sys 5 | 6 | 7 | class Logger(object): 8 | 9 | def __init__(self): 10 | self._process = sys.argv[0] 11 | self._quiet = 0 12 | self._verbose = 0 13 | 14 | def set_process_name(self, name): 15 | self._process = name 16 | 17 | def set_quiet(self, quiet): 18 | self._quiet = quiet 19 | 20 | def set_verbose(self, verbose): 21 | self._verbose = verbose 22 | 23 | def info(self, msg, *args): 24 | if not self._quiet: 25 | self._log(msg, *args) 26 | 27 | def debug(self, msg, *args): 28 | if not self._quiet and self._verbose: 29 | self._log(msg, *args) 30 | 31 | def error(self, msg, *args): 32 | self._log(msg, *args) 33 | 34 | def _log(self, msg, *args): 35 | now = datetime.now().isoformat() 36 | line = '%s [%s] %s\n' % (now, self._process, msg % args) 37 | sys.stderr.buffer.write(line.encode('utf-8')) 38 | sys.stderr.flush() 39 | 40 | 41 | def init_logger(process=None, quiet=0, verbose=0): 42 | if process: 43 | log.set_process_name(process) 44 | if quiet: 45 | log.set_quiet(quiet) 46 | if verbose: 47 | log.set_verbose(verbose) 48 | 49 | 50 | log = Logger() 51 | 52 | -------------------------------------------------------------------------------- /dnsrest/nodez_test.py: -------------------------------------------------------------------------------- 1 | 2 | # core 3 | import json 4 | import unittest 5 | 6 | # local 7 | from nodez import Node 8 | 9 | 10 | HOST1 = 'www.foo.com' 11 | WILD1 = '*.foo.com' 12 | ADDR1 = '1.2.3.4' 13 | ADDR2 = '6.7.8.9' 14 | TAG1 = 'name:/foo1' 15 | TAG2 = 'name:/foo2' 16 | 17 | 18 | def dump(n): 19 | print 'TREE:\n' + json.dumps(n.to_dict(), indent=4, sort_keys=1) 20 | 21 | 22 | class NodeTest(unittest.TestCase): 23 | 24 | def test_tagging(self): 25 | n = Node() 26 | n.put(HOST1, ADDR1, TAG1) 27 | res = n.get(HOST1) 28 | self.assertEquals(res, [(ADDR1, TAG1)]) 29 | 30 | def test_multiple_addresses(self): 31 | n = Node() 32 | 33 | # add a normal domain mapping for 2 nodes 34 | n.put(HOST1, ADDR1, TAG1) 35 | n.put(HOST1, ADDR2, TAG2) 36 | res = n.get(HOST1) 37 | self.assertEquals(res[0], (ADDR1, TAG1)) 38 | self.assertEquals(res[1], (ADDR2, TAG2)) 39 | 40 | # remove one tag 41 | n.remove(HOST1, TAG1) 42 | res = n.get(HOST1) 43 | self.assertEquals(res, [(ADDR2, TAG2)]) 44 | 45 | def test_multiple_wildcards(self): 46 | n = Node() 47 | 48 | # add a wildcard mapping 49 | n.put(WILD1, ADDR1, TAG1) 50 | n.put(WILD1, ADDR2, TAG2) 51 | res = n.get('xyz.foo.com') 52 | self.assertEquals(res[0], (ADDR1, TAG1)) 53 | self.assertEquals(res[1], (ADDR2, TAG2)) 54 | 55 | # remove the wildcard mapping for one tagged node 56 | n.remove(WILD1, TAG2) 57 | res = n.get('abc.foo.com') 58 | self.assertEquals(res, [(ADDR1, TAG1)]) 59 | 60 | # remove the remaining wildcard mapping 61 | n.remove(WILD1, TAG1) 62 | res = n.get('abc.foo.com') 63 | self.assertEquals(res, None) 64 | 65 | 66 | if __name__ == '__main__': 67 | unittest.main() 68 | 69 | -------------------------------------------------------------------------------- /dnsrest/namesrv.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | # libs 4 | from dnslib import A, DNSHeader, DNSLabel, DNSRecord, QTYPE, RR 5 | from gevent import socket 6 | from gevent.server import DatagramServer 7 | from gevent.resolver_ares import Resolver 8 | from dnsrest.logger import log 9 | 10 | DNS_RESOLVER_TIMEOUT = 3.0 11 | 12 | 13 | def contains(txt, *subs): 14 | return any(s in txt for s in subs) 15 | 16 | 17 | class DnsServer(DatagramServer): 18 | 19 | ''' 20 | Answers DNS queries against the registry, falling back to the recursive 21 | resolver (if present). 22 | ''' 23 | 24 | def __init__(self, bindaddr, registry, dns_servers=None): 25 | DatagramServer.__init__(self, bindaddr) 26 | self._registry = registry 27 | self._resolver = None 28 | if dns_servers: 29 | self._resolver = Resolver(servers=dns_servers, timeout=DNS_RESOLVER_TIMEOUT, tries=1) 30 | 31 | def handle(self, data, peer): 32 | rec = DNSRecord.parse(data) 33 | addr = None 34 | if rec.q.qtype in (QTYPE.A, QTYPE.AAAA): 35 | log.debug('Quering DNS') 36 | addr = self._registry.resolve(rec.q.qname.idna()) 37 | if not addr: 38 | strArr = [] 39 | for l in rec.q.qname.label: 40 | strArr.append(str(l,'utf-8')) 41 | domainName = '.'.join(strArr) 42 | addr = self._resolve(domainName) 43 | log.debug('Sending request to %s', addr) 44 | self.socket.sendto(self._reply(rec, addr), peer) 45 | 46 | def _reply(self, rec, addrs=None): 47 | reply = DNSRecord(DNSHeader(id=rec.header.id, qr=1, aa=1, ra=1), q=rec.q) 48 | if addrs: 49 | if not isinstance(addrs, list): 50 | addrs = [addrs] 51 | for addr in addrs: 52 | if addr: 53 | reply.add_answer(RR(rec.q.qname, QTYPE.A, rdata=A(addr))) 54 | return reply.pack() 55 | 56 | def _resolve(self, name): 57 | log.debug('resolve name=%s', name) 58 | if not self._resolver: 59 | return None 60 | try: 61 | return self._resolver.gethostbyname(name) 62 | except socket.gaierror as e: 63 | log.debug('Host by name could not be resolved') 64 | msg = str(e) 65 | if not contains(msg, 'ETIMEOUT', 'ENOTFOUND'): 66 | print(msg) 67 | 68 | -------------------------------------------------------------------------------- /dnsrest/monitor.py: -------------------------------------------------------------------------------- 1 | 2 | # core 3 | from collections import namedtuple 4 | from functools import reduce 5 | import json 6 | import re 7 | 8 | from dnsrest.logger import log 9 | 10 | 11 | 12 | RE_VALIDNAME = re.compile('[^\w\d.-]') 13 | 14 | 15 | Container = namedtuple('Container', 'id, name, running, addr') 16 | 17 | 18 | def get(d, *keys): 19 | empty = {} 20 | return reduce(lambda d, k: d.get(k, empty), keys, d) or None 21 | 22 | 23 | class DockerMonitor(object): 24 | 25 | ''' 26 | Reads events from Docker and activates/deactivates container domain names 27 | ''' 28 | 29 | def __init__(self, client, registry): 30 | self._docker = client 31 | self._registry = registry 32 | 33 | def run(self): 34 | # start the event poller, but don't read from the stream yet 35 | events = self._docker.events() 36 | 37 | # bootstrap by activating all running containers 38 | for container in self._docker.containers(): 39 | rec = self._inspect(container['Id']) 40 | if rec.running: 41 | self._registry.activate(rec) 42 | 43 | # read the docker event stream and update the name table 44 | for raw in events: 45 | evt = json.loads(raw) 46 | cid = evt.get('id') 47 | if cid is None: 48 | continue 49 | status = evt.get('status') 50 | if status in ('start', 'die'): 51 | try: 52 | rec = self._inspect(cid) 53 | if rec: 54 | if status == 'start': 55 | self._registry.activate(rec) 56 | else: 57 | self._registry.deactivate(rec) 58 | except Exception as e: 59 | print("exception occured: {0}".format(e)) 60 | 61 | def _inspect(self, cid): 62 | # get full details on this container from docker 63 | rec = self._docker.inspect_container(cid) 64 | 65 | # ensure name is valid, and append our domain 66 | name = get(rec, 'Name') 67 | if not name: 68 | return None 69 | name = RE_VALIDNAME.sub('', name).rstrip('.') 70 | 71 | # fetch IP address from "networks" section 72 | addr = None 73 | for nw in get(rec, 'NetworkSettings', 'Networks'): 74 | addr = get(rec, 'NetworkSettings', 'Networks', nw, 'IPAddress') 75 | 76 | # fall back to legacy mode 77 | if (addr == None): 78 | addr = get(rec, 'NetworkSettings', 'IPAddress') 79 | 80 | return Container( 81 | get(rec, 'Id'), 82 | name, 83 | get(rec, 'State', 'Running'), 84 | addr 85 | ) 86 | 87 | -------------------------------------------------------------------------------- /dnsrest/nodez.py: -------------------------------------------------------------------------------- 1 | 2 | # libs 3 | from dnslib import DNSLabel 4 | 5 | 6 | class Node(object): 7 | 8 | 'Stores a tree of domain names with wildcard support' 9 | 10 | def __init__(self): 11 | self._subs = {} 12 | self._wildcard = 0 13 | self._addr = [] 14 | self._addr_index = 0 15 | 16 | def get(self, name): 17 | return self._get(self._label(name)) 18 | 19 | def put(self, name, addr, tag=None): 20 | return self._put(self._label(name), addr, tag) 21 | 22 | def remove(self, name, tag=None): 23 | return self._remove(self._label(name), tag) 24 | 25 | def to_dict(self): 26 | r = {} 27 | r[':addr'] = self._addr 28 | r[':wild'] = self._wildcard 29 | for key, sub in self._subs.items(): 30 | r[key] = sub.to_dict() 31 | return r 32 | 33 | def _label(self, name): 34 | return list(DNSLabel(name).label) 35 | 36 | def _get(self, label): 37 | if not label: 38 | self._addr_index += 1 39 | if len(self._addr) != 0: 40 | self._addr_index %= len(self._addr) 41 | return self._addr[self._addr_index:] + self._addr[:self._addr_index] 42 | part = label.pop() 43 | sub = self._subs.get(part) 44 | if sub: 45 | res = sub._get(label) 46 | if res: 47 | return res 48 | return self._addr if self._wildcard else None 49 | 50 | def _put(self, label, addr, tag=None): 51 | part = label.pop() 52 | 53 | if not label and part == b'*': 54 | self._wildcard = 1 55 | self._addr.append((addr, tag)) 56 | return 57 | 58 | sub = self._subs.get(part) 59 | if sub is None: 60 | sub = Node() 61 | self._subs[part] = sub 62 | 63 | if not label: 64 | sub._addr.append((addr, tag)) 65 | return 66 | 67 | sub._put(label, addr, tag) 68 | 69 | def _remove(self, label, tag=None): 70 | part = label.pop() 71 | sub = self._subs.get(part) 72 | if not label: 73 | if part == '*': 74 | tagged = self._tagged_addr(self._addr, tag) 75 | self._addr = [(a, t) for a, t in self._addr if a not in tagged] 76 | self._wildcard = 0 if not self._addr else 1 77 | return tagged 78 | elif sub: 79 | tagged = self._tagged_addr(sub._addr, tag) 80 | sub._addr = [(a, t) for a, t in sub._addr if a not in tagged] 81 | return tagged 82 | elif sub: 83 | sub._remove(label, tag) 84 | 85 | if sub and sub._is_empty(): 86 | del self._subs[part] 87 | return [] 88 | 89 | def _is_empty(self): 90 | return not self._subs and not self._addr 91 | 92 | def _tagged_addr(self, addr, tag): 93 | return set([a for a, t in addr if t == tag or tag is None]) 94 | 95 | -------------------------------------------------------------------------------- /docker_dnsrest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # dnsrest - restful dns service for associating domain names with docker containers 4 | 5 | 6 | # monkey patch 7 | from gevent import monkey 8 | monkey.patch_all() 9 | 10 | # core 11 | import argparse 12 | import os 13 | import signal 14 | import sys 15 | from urllib.parse import urlparse 16 | 17 | # local 18 | from dnsrest.logger import init_logger, log 19 | from dnsrest.monitor import DockerMonitor 20 | from dnsrest.namesrv import DnsServer 21 | from dnsrest.registry import Registry 22 | from dnsrest.restapi import StaticApi, ContainerApi, DebugApi 23 | 24 | # libs 25 | import docker 26 | import falcon 27 | import gevent 28 | from gevent.pywsgi import WSGIServer 29 | 30 | 31 | PROCESS = 'dnsrest' 32 | DOCKER_SOCK = 'unix:///docker.sock' 33 | DOCKER_VERSION = '1.29' 34 | DNS_BINDADDR = '0.0.0.0:53' 35 | DNS_RESOLVER = ['8.8.8.8'] 36 | REST_BINDADDR = '0.0.0.0:80' 37 | EPILOG = '' 38 | 39 | 40 | def check(args): 41 | url = urlparse(args.docker) 42 | if url.scheme in ('unix','unix+http'): 43 | # check if the socket file exists 44 | if not os.path.exists(url.path): 45 | log.error('unix socket %r does not exist', url.path) 46 | sys.exit(1) 47 | 48 | 49 | def parse_args(): 50 | docker_url = os.environ.get('DOCKER_HOST') 51 | if not docker_url: 52 | docker_url = DOCKER_SOCK 53 | parser = argparse.ArgumentParser(PROCESS, epilog=EPILOG, 54 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 55 | parser.add_argument('--docker', default=docker_url, 56 | help='Url to docker TCP/UNIX socket') 57 | parser.add_argument('--dns-bind', default=DNS_BINDADDR, 58 | help='Bind address for DNS server') 59 | parser.add_argument('--rest-bind', default=REST_BINDADDR, 60 | help='Bind address for REST API server') 61 | parser.add_argument('--resolver', default=DNS_RESOLVER, nargs='*', 62 | help='Servers for recursive DNS resolution') 63 | parser.add_argument('--no-recursion', action='store_const', const=1, 64 | help='Disables recursive DNS queries') 65 | parser.add_argument('--verbose', action='store_const', const=1, 66 | help='Be more verbose') 67 | parser.add_argument('-q', '--quiet', action='store_const', const=0, 68 | help='Quiet mode') 69 | return parser.parse_args() 70 | 71 | 72 | def stop(*servers): 73 | for svr in servers: 74 | if svr.started: 75 | svr.stop() 76 | sys.exit(signal.SIGINT) 77 | 78 | 79 | def main(): 80 | args = parse_args() 81 | check(args) 82 | init_logger(process=PROCESS, quiet=args.quiet, verbose=args.verbose) 83 | resolver = () if args.no_recursion else args.resolver 84 | 85 | registry = Registry() 86 | 87 | app = falcon.API() 88 | app.add_route('/container/{label}/{arg}', ContainerApi(registry)) 89 | app.add_route('/domain/{domain}', StaticApi(registry)) 90 | app.add_route('/debug', DebugApi(registry)) 91 | 92 | api = WSGIServer(args.rest_bind, app) 93 | dns = DnsServer(args.dns_bind, registry, resolver) 94 | 95 | api.start() 96 | dns.start() 97 | for signum in (signal.SIGTERM, signal.SIGINT): 98 | gevent.signal(signum, stop, api, dns) 99 | 100 | client = docker.Client(args.docker, version=DOCKER_VERSION) 101 | monitor = DockerMonitor(client, registry) 102 | gevent.wait([gevent.spawn(monitor.run)]) 103 | 104 | 105 | if __name__ == '__main__': 106 | main() 107 | 108 | -------------------------------------------------------------------------------- /dnsrest/restapi.py: -------------------------------------------------------------------------------- 1 | 2 | # core 3 | import json 4 | 5 | # libs 6 | from dnslib import A, DNSLabel 7 | import falcon 8 | 9 | 10 | class BaseApi(object): 11 | 12 | def __init__(self, registry): 13 | self.registry = registry 14 | 15 | def _ok(self, res, data, indent=0): 16 | res.status = falcon.HTTP_200 17 | if indent: 18 | res.body = json.dumps(data, indent=4, sort_keys=1) 19 | else: 20 | res.body = json.dumps(data) 21 | 22 | def _fail(self, msg): 23 | raise falcon.HTTPError(falcon.HTTP_500, 'Error', msg) 24 | 25 | def _parse(self, req): 26 | try: 27 | return json.loads(req.stream.read()) 28 | except Exception as ex: 29 | raise falcon.HTTPError(falcon.HTTP_400, 'Error', ex.message) 30 | 31 | def _validate_type(self, key, val, *types): 32 | if not isinstance(val, types): 33 | self._fail('The %s must be of type %r, not %r' % \ 34 | (key, types, type(val))) 35 | 36 | def _validate_domain(self, domain): 37 | self._validate_type('domain name', domain, (str)) 38 | try: 39 | return DNSLabel(domain) 40 | except Exception as e: 41 | self._fail('Domain name parsing failed %s' % e) 42 | 43 | def _validate_ips(self, ips): 44 | if not isinstance(ips, list): 45 | self._fail('Missing an "ips" array') 46 | for ip in ips: 47 | try: 48 | ip = A(ip) 49 | except Exception as e: 50 | self._fail('Address parsing failed %s' % e) 51 | 52 | 53 | class StaticApi(BaseApi): 54 | 55 | 'Expose an API to create and manage static domain to ip mappings' 56 | 57 | def __init__(self, registry): 58 | BaseApi.__init__(self, registry) 59 | 60 | def on_get(self, req, res, domain): 61 | self._ok(res, {'code': 0}) 62 | 63 | def on_put(self, req, res, domain): 64 | data = self._parse(req) 65 | domain, ips = self._validate(domain, data) 66 | for ip in ips: 67 | self.registry.activate_static(domain, ip) 68 | self._ok(res, {'code': 0}) 69 | 70 | def on_delete(self, req, res, domain): 71 | domain = self._validate_domain(domain) 72 | self.registry.deactivate_static(domain) 73 | self._ok(res, {'code': 0}) 74 | 75 | def _validate(self, domain, data): 76 | if not isinstance(data, dict): 77 | self._fail('Expected a dict, got %s' % type(data)) 78 | ips = data.get('ips') 79 | self._validate_ips(ips) 80 | domain = self._validate_domain(domain) 81 | return domain, ips 82 | 83 | 84 | class ContainerApi(BaseApi): 85 | 86 | 'Expose an API to create and manage container name/id domain mappings' 87 | 88 | VALID = set(['id', 'name']) 89 | 90 | def __init__(self, registry): 91 | BaseApi.__init__(self, registry) 92 | 93 | def on_get(self, req, res, label, arg): 94 | key = self._key(label, arg) 95 | record = self.registry.get(key) 96 | self._ok(res, {'code': 0, 'record': record}) 97 | 98 | def on_put(self, req, res, label, arg): 99 | data = self._parse(req) 100 | data = self._validate(data) 101 | key = self._key(label, arg) 102 | self.registry.add(key, data) 103 | self._ok(res, {'code': 0}) 104 | 105 | def on_delete(self, req, res, label, arg): 106 | key = self._key(label, arg) 107 | self.registry.remove(key) 108 | self._ok(res, {'code': 0}) 109 | 110 | def _key(self, label, arg): 111 | if label not in self.VALID: 112 | self._fail('Unsuppported label %r' % label) 113 | return label + ':/' + arg 114 | 115 | def _validate(self, data): 116 | 'Ensure that the data being PUT is valid' 117 | if not isinstance(data, dict): 118 | self._fail('Expected a dict, not %r' % type(data)) 119 | domains = data.get('domains') 120 | if not isinstance(domains, list): 121 | self._fail('Missing a "domains" array') 122 | res = [] 123 | for name in domains: 124 | res.append(self._validate_domain(name)) 125 | return res 126 | 127 | 128 | class DebugApi(BaseApi): 129 | 130 | def __init__(self, registry): 131 | BaseApi.__init__(self, registry) 132 | 133 | def on_get(self, req, res): 134 | self._ok(res, self.registry._domains.to_dict(), indent=1) 135 | 136 | 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | docker-dns-rest 3 | --------------- 4 | 5 | A RESTful DNS service for Docker containers. 6 | 7 | This service is used to cover a specific use cases for testing applications 8 | which rely on DNS for service discovery, and come bundled with configurations 9 | for certain environments. 10 | 11 | For example, to fool a service into thinking its running in a staging 12 | environment, we can create several named containers and map one or more domain 13 | names to them. When the containers come online, the dnsrest service maps the 14 | domain names to the container IP addresses and answers DNS queries from the 15 | other containers. 16 | 17 | Usage 18 | ----- 19 | 20 | 21 | First, start docker-dns-rest container. The docker-dns-rest container listens 22 | on port 80 by default, so depending on how you run Docker you may need to map 23 | a host port: 24 | 25 | % docker run -d -p 5080:80 -v /var/run/docker.sock:/docker.sock --name dns \ 26 | phensley/docker-dns-rest --verbose 27 | 28 | Tail the logs: 29 | 30 | % docker logs -f dns 31 | 32 | Ensure you have routing from your local machine to the `docker-dns-rest` 33 | container. Assuming you're running Docker under a Vagrant VM on the local 34 | host, add a route to the VM's IP (`192.168.222.5` in this example): 35 | 36 | % route add -net 172.17.0.0 192.168.222.5 37 | 38 | Get the IP of the DNS container: 39 | 40 | % docker inspect -f '{{.NetworkSettings.IPAddress}}' dns 41 | 172.17.0.2 42 | 43 | The previous command will fail if a user defined network is present. In this case use: 44 | 45 | % docker inspect -f '{{.NetworkSettings.Networks.yournetwork.IPAddress}}' dns 46 | 172.17.0.2 47 | 48 | Next, add some names to the DNS registry. We can associate one or more names 49 | with a container by `id` or `name`. We'll associate some domain names with 50 | the container name `www`: 51 | 52 | % curl -X PUT -H 'Content-Type: application/json' \ 53 | -d '{"domains": ["*.example.com", "www.staging.internal.com"]}' \ 54 | http://172.17.0.2:80/container/name/www 55 | {"code": 0} 56 | 57 | Now, start up a container with that name: 58 | 59 | % docker run -it --name www ubuntu bash 60 | root@db8fabbaf1d6:/# 61 | 62 | You should see some output in the DNS log: 63 | 64 | 192.168.222.1 - - [2014-10-11 15:25:34] "PUT /container/name/www HTTP/1.1" 200 134 0.000366 65 | 2014-10-11T15:26:29.198673 [dnsrest] setting www (83854cf229) as active 66 | 2014-10-11T15:26:29.198821 [dnsrest] added *.example.com. -> 172.17.0.3 67 | 2014-10-11T15:26:29.198900 [dnsrest] added www.staging.internal.com. -> 172.17.0.3 68 | 69 | Confirm the `www` container's IP address: 70 | 71 | % docker inspect -f '{{.NetworkSettings.IPAddress}}' www 72 | 172.17.0.3 73 | 74 | 75 | Now you can query some names against the DNS server: 76 | 77 | % host test.example.com 172.17.0.2 78 | Using domain server: 79 | Name: 172.17.0.2 80 | Address: 172.17.0.2#53 81 | Aliases: 82 | 83 | test.example.com has address 172.17.0.3 84 | test.example.com has address 172.17.0.3 85 | 86 | When you stop the `www` container, the names will be unregistered: 87 | 88 | % docker stop www 89 | 90 | ... dns logs ... 91 | 2014-10-11T15:28:35.050232 [dnsrest] setting www (83854cf229) as inactive 92 | 2014-10-11T15:28:35.050378 [dnsrest] removed *.example.com. -> 172.17.0.3 93 | 2014-10-11T15:28:35.050462 [dnsrest] removed www.staging.internal.com. -> 172.17.0.3 94 | 95 | Now start the `www` container again and the names will be registered again under the new IP address: 96 | 97 | % docker start www 98 | 99 | ... dns logs ... 100 | 2014-10-11T15:29:37.374072 [dnsrest] setting www (83854cf229) as active 101 | 2014-10-11T15:29:37.374209 [dnsrest] added *.example.com. -> 172.17.0.4 102 | 2014-10-11T15:29:37.374286 [dnsrest] added www.staging.internal.com. -> 172.17.0.4 103 | 104 | ... confirm the ip is correct ... 105 | % docker inspect -f '{{.NetworkSettings.IPAddress}}' www 106 | 172.17.0.4 107 | 108 | You can use the DNS server from your containers using: 109 | 110 | % docker run -it --name shell --dns 172.17.0.2 --dns-search example.com ubuntu bash 111 | root@e776fff8d971:/# ping foo 112 | PING foo.example.com (172.17.0.4) 56(84) bytes of data. 113 | 64 bytes from 172.17.0.4: icmp_seq=1 ttl=64 time=0.087 ms 114 | 64 bytes from 172.17.0.4: icmp_seq=2 ttl=64 time=0.102 ms 115 | 64 bytes from 172.17.0.4: icmp_seq=3 ttl=64 time=0.106 ms 116 | ^C 117 | 118 | root@e776fff8d971:/# ping www.staging.internal.com 119 | PING www.staging.internal.com (172.17.0.57) 56(84) bytes of data. 120 | 64 bytes from 172.17.0.4: icmp_seq=1 ttl=64 time=0.056 ms 121 | 64 bytes from 172.17.0.4: icmp_seq=2 ttl=64 time=0.106 ms 122 | ^C 123 | 124 | ... dns logs ... 125 | 2014-10-11T15:32:54.874238 [dnsrest] resolved foo.example.com. -> 172.17.0.4 126 | 2014-10-11T15:36:40.487780 [dnsrest] resolved www.staging.internal.com. -> 172.17.0.4 127 | 128 | The DNS server will also forward any names which do not match, to the resolver you specify (default is `8.8.8.8`). This can be disabled by setting the `--no-recursion` command line option: 129 | 130 | root@e776fff8d971:/# ping github.com 131 | PING github.com (192.30.252.130) 56(84) bytes of data. 132 | 64 bytes from 192.30.252.130: icmp_seq=1 ttl=61 time=33.4 ms 133 | 64 bytes from 192.30.252.130: icmp_seq=2 ttl=61 time=31.8 ms 134 | 135 | 136 | -------------------------------------------------------------------------------- /dnsrest/registry.py: -------------------------------------------------------------------------------- 1 | 2 | # core 3 | import json 4 | 5 | # local 6 | from dnsrest.logger import log 7 | from dnsrest.nodez import Node 8 | 9 | # libs 10 | from gevent import threading 11 | 12 | 13 | class Mapping(object): 14 | 15 | def __init__(self, names, key): 16 | self.names = names 17 | self.key = key 18 | 19 | 20 | class Registry(object): 21 | 22 | '''' 23 | Maps a container by id/name to a list of domain names and addresses. 24 | When the container is started, the list of domain names can be activated, 25 | and when the container is stopped the list of domain names can be 26 | deactivated. 27 | ''' 28 | 29 | def __init__(self): 30 | self._mappings = {} 31 | self._active = {} 32 | self._domains = Node() 33 | self._lock = threading.Lock() 34 | 35 | def add(self, key, names): 36 | '''' 37 | Adds a mapping from the given key to a list of names. The names 38 | will be registered when the container is activated (running) and 39 | unregistered when the container is deactivated (stopped). 40 | ''' 41 | # first, remove the old names, if any 42 | self.remove(key) 43 | 44 | with self._lock: 45 | # persist the mappings 46 | self._mappings[key] = Mapping(names, key) 47 | 48 | # check if these pertain to any already-active containers and 49 | # activate the domain names 50 | activate = [] 51 | for container in self._active.values(): 52 | if key in ('name:/' + container.name, 'id:/' + container.id): 53 | desc = self._desc(container) 54 | self._activate(names, container.addr, tag=key) 55 | 56 | def get(self, key): 57 | with self._lock: 58 | mapping = self._mappings.get(key) 59 | if mapping: 60 | return [n.idna().rstrip('.') for n in mapping.names] 61 | return [] 62 | 63 | def remove(self, key): 64 | with self._lock: 65 | old_mapping = self._mappings.get(key) 66 | if old_mapping: 67 | self._deactivate(old_mapping.names, tag=old_mapping.key) 68 | del self._mappings[old_mapping.key] 69 | 70 | def activate_static(self, domain, addr): 71 | with self._lock: 72 | self._activate([domain], addr, tag='domain:/%s' % domain) 73 | 74 | def deactivate_static(self, domain): 75 | with self._lock: 76 | self._deactivate([domain], tag='domain:/%s' % domain) 77 | 78 | def activate(self, container): 79 | 'Activate all rules associated with this container' 80 | desc = self._desc(container) 81 | with self._lock: 82 | self._active[container.id] = container 83 | mapping = self._get_mapping_by_container(container) 84 | if mapping: 85 | log.info('setting %s as active' % desc) 86 | key, names = mapping.key, mapping.names 87 | self._activate(names, container.addr, tag=key) 88 | 89 | def deactivate(self, container): 90 | 'Deactivate all rules associated with this container' 91 | with self._lock: 92 | old_container = self._active.get(container.id) 93 | if old_container is None: 94 | return 95 | del self._active[container.id] 96 | 97 | # since this container is active, get the old address so we can log 98 | # exactly which names/addresses are being deactivated 99 | desc = self._desc(container) 100 | mapping = self._get_mapping_by_container(container) 101 | if mapping: 102 | log.info('setting %s as inactive' % desc) 103 | self._deactivate(mapping.names, tag=mapping.key) 104 | 105 | def resolve(self, name): 106 | 'Resolves the address for this name, if any' 107 | log.debug('Resolve %s', name) 108 | with self._lock: 109 | res = self._domains.get(name) 110 | if res: 111 | addrs = [a for a, _ in res] 112 | log.debug('resolved %s -> %s', name, ', '.join(map(str,addrs))) 113 | return addrs 114 | else: 115 | log.debug('no mapping for %s' % name) 116 | 117 | def dump(self): 118 | return json.dumps(self._domains.to_dict(), indent=4, sort_keys=1) 119 | 120 | def _activate(self, names, addr, tag=None): 121 | for name in names: 122 | self._domains.put(name, addr, tag) 123 | log.info('added %s -> %s key=%s', name.idna(), addr, tag) 124 | #log.debug('tree %s', self.dump()) 125 | 126 | def _deactivate(self, names, tag=None): 127 | for name in names: 128 | if self._domains.get(name): 129 | addrs = self._domains.remove(name, tag) 130 | if addrs: 131 | for addr in addrs: 132 | log.info('removed %s -> %s', name.idna(), addr) 133 | #log.debug('tree %s', self.dump()) 134 | 135 | def _get_mapping_by_container(self, container): 136 | # try name and id-based keys 137 | res = self._mappings.get('name:/%s' % container.name) 138 | if not res: 139 | res = self._mappings.get('id:/%s' % container.id) 140 | return res 141 | 142 | def _desc(self, container): 143 | return '%s (%s)' % (container.name, container.id[:10]) 144 | 145 | --------------------------------------------------------------------------------