This repository has been archived by the owner on Oct 2, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathdocker_dns.py
executable file
·276 lines (213 loc) · 7.19 KB
/
docker_dns.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
#!/usr/bin/python
"""
A simple TwistD DNS server using custom TLD and Docker as the back end for IP
resolution.
To look up a container:
- 'A' record query container's hostname with no TLD. Must be an exact match
- 'A' record query an ID that will match a container with a docker inspect
command with '.docker' as the TLD. eg: 0949efde23b.docker
Code heavily modified from
http://stackoverflow.com/a/4401671/509043
Author: Ricky Cook <[email protected]>
"""
import docker
import re
from requests.exceptions import RequestException
from twisted.application import internet, service
from twisted.internet import defer
from twisted.names import common, dns, server
from twisted.names.error import DNSQueryTimeoutError, DomainError
from twisted.python import failure
from warnings import warn
# FIXME replace with a more generic solution like operator.attrgetter
def dict_lookup(dic, key_path, default=None):
"""
Look up value in a nested dict
Args:
dic: The dictionary to search
key_path: An iterable containing an ordered list of dict keys to
traverse
default: Value to return in case nothing is found
Returns:
Value of the dict at the nested location given, or default if no value
was found
"""
for k in key_path:
if k in dic:
dic = dic[k]
else:
return default
return dic
class DockerMapping(object):
"""
Look up docker container data
"""
id_re = re.compile(r'([a-z0-9]+)\.docker')
def __init__(self, api):
"""
Args:
api: Docker Client instance used to do API communication
"""
self.api = api
def _ids_from_prop(self, key_path, value):
"""
Get IDs of containers where their config matches a value
Args:
key_path: An iterable containing an ordered list of container
config keys to traverse
value: What the value at key_path must match to qualify
Returns:
Generator with a list of containers that match the config value
"""
return (
c['ID']
for c in (
self.api.inspect_container(c_lite['Id'])
for c_lite in self.api.containers(all=True)
)
if dict_lookup(c, key_path, None) == value
if 'ID' in c
)
def lookup_container(self, name):
"""
Gets the container config from a DNS lookup name, or returns None if
one could not be found
Args:
name: DNS query name to look up
Returns:
Container config dict for the first matching container
"""
match = self.id_re.match(name)
if match:
container_id = match.group(1)
else:
ids = self._ids_from_prop(('Config', 'Hostname'), unicode(name))
# FIXME Should be able to support multiple
try:
container_id = ids.next()
except StopIteration:
return None
try:
return self.api.inspect_container(container_id)
except docker.client.APIError as ex:
# 404 is valid, others aren't
if ex.response.status_code != 404:
warn(ex)
return None
except RequestException as ex:
warn(ex)
return None
def get_a(self, name):
"""
Get an IPv4 address from a query name to be used in A record lookups
Args:
name: DNS query name to look up
Returns:
IPv4 address for the query name given
"""
container = self.lookup_container(name)
if container is None:
return None
addr = container['NetworkSettings']['IPAddress']
if addr is '':
return None
return addr
# pylint:disable=too-many-public-methods
class DockerResolver(common.ResolverBase):
"""
DNS resolver to resolve queries with a DockerMapping instance.
"""
def __init__(self, mapping):
"""
Args:
mapping: DockerMapping instance for lookups
"""
self.mapping = mapping
# Change to this ASAP when Twisted uses object base
# super(DockerResolver, self).__init__()
common.ResolverBase.__init__(self)
self.ttl = 10
def _a_records(self, name):
"""
Get A records from a query name
Args:
name: DNS query name to look up
Returns:
Tuple of formatted DNS replies
"""
addr = self.mapping.get_a(name)
if not addr:
raise DomainError(name)
return tuple([
dns.RRHeader(name, dns.A, dns.IN, self.ttl,
dns.Record_A(addr, self.ttl),
CONFIG['authoritive'])
])
def lookupAddress(self, name, timeout=None):
try:
records = self._a_records(name)
return defer.succeed((records, (), ()))
# We need to catch everything. Uncaught exceptian will make the server
# stop responding
except: # pylint:disable=bare-except
if CONFIG['no_nxdomain']:
# FIXME surely there's a better way to give SERVFAIL
exception = DNSQueryTimeoutError(name)
else:
exception = DomainError(name)
return defer.fail(failure.Failure(exception))
def main():
"""
Set everything up
"""
# Create docker
if CONFIG['docker_url']:
docker_client = docker.Client(CONFIG['docker_url'])
else:
docker_client = docker.Client()
# Create our custom mapping and resolver
mapping = DockerMapping(docker_client)
resolver = DockerResolver(mapping)
# Create twistd stuff to tie in our custom components
factory = server.DNSServerFactory(clients=[resolver])
factory.noisy = False
# Protocols to bind
bind_list = []
if 'tcp' in CONFIG['bind_protocols']:
bind_list.append((internet.TCPServer, factory)) # noqa pylint:disable=no-member
if 'udp' in CONFIG['bind_protocols']:
proto = dns.DNSDatagramProtocol(factory)
proto.noisy = False
bind_list.append((internet.UDPServer, proto)) # noqa pylint:disable=no-member
# Register the service
ret = service.MultiService()
for (klass, arg) in bind_list:
svc = klass(
CONFIG['bind_port'],
arg,
interface=CONFIG['bind_interface']
)
svc.setServiceParent(ret)
# DO IT NOW
ret.setServiceParent(service.IServiceCollection(application))
# Load the config
try:
from config import CONFIG # pylint:disable=no-name-in-module,import-error
except ImportError:
CONFIG = {}
# Merge user config over defaults
DEFAULT_CONFIG = {
'docker_url': None,
'bind_interface': '',
'bind_port': 53,
'bind_protocols': ['tcp', 'udp'],
'no_nxdomain': True,
'authoritive': True,
}
CONFIG = dict(DEFAULT_CONFIG.items() + CONFIG.items())
application = service.Application('dnsserver', 1, 1) # noqa pylint:disable=invalid-name
main()
# Doin' it wrong
if __name__ == '__main__':
import sys
print "Usage: twistd -y %s" % sys.argv[0]