2 from twisted.internet import reactor
3 from twisted.names import server
4 from twisted.names import dns
5 from twisted.names import common
6 from twisted.names import authority
7 from twisted.names import resolve
8 from twisted.internet import defer
9 from twisted.python import failure
11 from invirt.common import InvirtConfigError
12 from invirt.config import structs as config
13 import invirt.database
19 class DatabaseAuthority(common.ResolverBase):
20 """An Authority that is loaded from a file."""
24 def __init__(self, domains=None, database=None):
25 common.ResolverBase.__init__(self)
26 if database is not None:
27 invirt.database.connect(database)
29 invirt.database.connect()
30 if domains is not None:
31 self.domains = domains
33 self.domains = config.dns.domains
34 ns = config.dns.nameservers[0]
35 self.soa = dns.Record_SOA(mname=ns.hostname,
36 rname=config.dns.contact.replace('@','.',1),
37 serial=1, refresh=3600, retry=900,
38 expire=3600000, minimum=21600, ttl=3600)
39 self.ns = dns.Record_NS(name=ns.hostname, ttl=3600)
40 record = dns.Record_A(address=ns.ip, ttl=3600)
41 self.ns1 = dns.RRHeader(ns.hostname, dns.A, dns.IN,
42 3600, record, auth=True)
45 def _lookup(self, name, cls, type, timeout = None):
48 value = self._lookup_unsafe(name, cls, type, timeout = None)
49 except (psycopg2.OperationalError, sqlalchemy.exceptions.DBAPIError):
52 print "Reloading database"
58 def _lookup_unsafe(self, name, cls, type, timeout):
59 invirt.database.clear_cache()
64 if name in self.domains:
67 # Look for the longest-matching domain.
69 for domain in self.domains:
70 if name.endswith('.'+domain) and len(domain) > len(best_domain):
73 if name.endswith('.in-addr.arpa'):
74 # Act authoritative for the IP address for reverse resolution requests
77 return defer.fail(failure.Failure(dns.DomainError(name)))
81 additional = [self.ns1]
82 authority.append(dns.RRHeader(domain, dns.NS, dns.IN,
83 3600, self.ns, auth=True))
87 # - What domain: in-addr.arpa, domain root, or subdomain?
88 # - What query type: A, PTR, NS, ...?
92 return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
94 if name.endswith(".in-addr.arpa"):
95 if type in (dns.PTR, dns.ALL_RECORDS):
96 ip = '.'.join(reversed(name.split('.')[:-2]))
97 value = invirt.database.NIC.query.filter_by(ip=ip).first()
98 if value and value.hostname:
99 hostname = value.hostname
100 if '.' not in hostname:
101 hostname = hostname + "." + config.dns.domains[0]
102 record = dns.Record_PTR(hostname, ttl)
103 results.append(dns.RRHeader(name, dns.PTR, dns.IN,
104 ttl, record, auth=True))
105 else: # IP address doesn't point to an active host
106 return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
107 elif type == dns.SOA:
108 results.append(dns.RRHeader(domain, dns.SOA, dns.IN,
109 ttl, self.soa, auth=True))
110 # FIXME: Should only return success with no records if the name actually exists
112 elif name == domain or name == '.'+domain:
113 if type in (dns.A, dns.ALL_RECORDS):
114 record = dns.Record_A(config.dns.nameservers[0].ip, ttl)
115 results.append(dns.RRHeader(name, dns.A, dns.IN,
116 ttl, record, auth=True))
118 results.append(dns.RRHeader(domain, dns.NS, dns.IN,
119 ttl, self.ns, auth=True))
121 elif type == dns.SOA:
122 results.append(dns.RRHeader(domain, dns.SOA, dns.IN,
123 ttl, self.soa, auth=True))
126 host = name[:-len(domain)-1]
127 value = invirt.database.NIC.query.filter_by(hostname=host).first()
131 value = invirt.database.Machine.query.filter_by(name=host).first()
133 ip = value.nics[0].ip
135 return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
137 return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
138 if type in (dns.A, dns.ALL_RECORDS):
139 record = dns.Record_A(ip, ttl)
140 results.append(dns.RRHeader(name, dns.A, dns.IN,
141 ttl, record, auth=True))
142 elif type == dns.SOA:
143 results.append(dns.RRHeader(domain, dns.SOA, dns.IN,
144 ttl, self.soa, auth=True))
146 if len(results) == 0:
149 return defer.succeed((results, authority, additional))
151 class DelegatingQuotingBindAuthority(authority.BindAuthority):
153 A delegating BindAuthority that (almost) deals with quoting correctly
155 This will catch double quotes as marking the start or end of a
156 quoted phrase, unless the double quote is escaped by a backslash
158 # Match either a quoted or unquoted string literal followed by
159 # whitespace or the end of line. This yields two groups, one of
160 # which has a match, and the other of which is None, depending on
161 # whether the string literal was quoted or unquoted; this is what
162 # necessitates the subsequent filtering out of groups that are
165 re.compile(r'"((?:[^"\\]|\\.)*)"|((?:[^\\\s]|\\.)+)(?:\s+|\s*$)')
167 # For interpreting escapes.
168 escape_pat = re.compile(r'\\(.)')
170 def collapseContinuations(self, lines):
175 if line.find('(') == -1:
178 L.append(line[:line.find('(')])
181 if line.find(')') != -1:
182 L[-1] += ' ' + line[:line.find(')')]
192 for m in self.string_pat.finditer(line):
193 [x] = [x for x in m.groups() if x is not None]
194 split_line.append(self.escape_pat.sub(r'\1', x))
196 return filter(None, L)
198 def _lookup(self, name, cls, type, timeout = None):
199 maybeDelegate = False
200 deferredResult = authority.BindAuthority._lookup(self, name, cls,
202 # If we didn't find an exact match for the name we were seeking,
203 # check if it's within a subdomain we're supposed to delegate to
204 # some other DNS server.
205 while (isinstance(deferredResult.result, failure.Failure)
208 name = name[name.find('.') + 1 :]
209 deferredResult = authority.BindAuthority._lookup(self, name, cls,
211 # If we found somewhere to delegate the query to, our _lookup()
212 # for the NS record resulted in it being in the 'results' section.
213 # We need to instead return that information in the 'authority'
214 # section to delegate, and return an empty 'results' section
215 # (because we didn't find the name we were asked about). We
216 # leave the 'additional' section as we received it because it
217 # may contain A records for the DNS server we're delegating to.
218 if maybeDelegate and not isinstance(deferredResult.result,
220 (nsResults, nsAuthority, nsAdditional) = deferredResult.result
221 deferredResult = defer.succeed(([], nsResults, nsAdditional))
222 return deferredResult
224 class TypeLenientResolverChain(resolve.ResolverChain):
226 This is a ResolverChain which is more lenient in its handling of
227 queries requesting unimplemented record types.
230 def query(self, query, timeout = None):
232 return self.typeToMethod[query.type](str(query.name), timeout)
234 # We don't support the requested record type. Twisted would
235 # have us return SERVFAIL. Instead, we'll check whether the
236 # name exists in our zone at all and return NXDOMAIN or an empty
237 # result set with NOERROR as appropriate.
238 deferredResult = self.lookupAllRecords(str(query.name), timeout)
239 if isinstance(deferredResult.result, failure.Failure):
240 return deferredResult
241 (results, authority, additional) = deferredResult.result
242 return defer.succeed(([], authority, additional))
244 if '__main__' == __name__:
247 for zone in config.dns.zone_files:
248 for origin in config.dns.domains:
249 r = DelegatingQuotingBindAuthority(zone)
250 # This sucks, but if I want a generic zone file, I have to
251 # reload the information by hand
253 lines = open(zone).readlines()
254 lines = r.collapseContinuations(r.stripComments(lines))
258 except InvirtConfigError:
259 # Don't care if zone_files isn't defined
261 resolvers.append(DatabaseAuthority())
264 f = server.DNSServerFactory(verbose=verbosity)
265 f.resolver = TypeLenientResolverChain(resolvers)
266 p = dns.DNSDatagramProtocol(f)
267 f.noisy = p.noisy = verbosity
269 reactor.listenUDP(53, p)
270 reactor.listenTCP(53, f)