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.internet import defer
8 from twisted.python import failure
10 from invirt.common import InvirtConfigError
11 from invirt.config import structs as config
12 import invirt.database
18 class DatabaseAuthority(common.ResolverBase):
19 """An Authority that is loaded from a file."""
23 def __init__(self, domains=None, database=None):
24 common.ResolverBase.__init__(self)
25 if database is not None:
26 invirt.database.connect(database)
28 invirt.database.connect()
29 if domains is not None:
30 self.domains = domains
32 self.domains = config.dns.domains
33 ns = config.dns.nameservers[0]
34 self.soa = dns.Record_SOA(mname=ns.hostname,
35 rname=config.dns.contact.replace('@','.',1),
36 serial=1, refresh=3600, retry=900,
37 expire=3600000, minimum=21600, ttl=3600)
38 self.ns = dns.Record_NS(name=ns.hostname, ttl=3600)
39 record = dns.Record_A(address=ns.ip, ttl=3600)
40 self.ns1 = dns.RRHeader(ns.hostname, dns.A, dns.IN,
41 3600, record, auth=True)
44 def _lookup(self, name, cls, type, timeout = None):
47 value = self._lookup_unsafe(name, cls, type, timeout = None)
48 except (psycopg2.OperationalError, sqlalchemy.exceptions.SQLError):
51 print "Reloading database"
57 def _lookup_unsafe(self, name, cls, type, timeout):
58 invirt.database.clear_cache()
63 if name in self.domains:
66 # Look for the longest-matching domain. (This works because domain
67 # will remain bound after breaking out of the loop.)
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 best_domain = name # Act authoritative for the IP address for reverse resolution requests
76 return defer.fail(failure.Failure(dns.DomainError(name)))
80 additional = [self.ns1]
81 authority.append(dns.RRHeader(domain, dns.NS, dns.IN,
82 3600, self.ns, auth=True))
85 host = name[:-len(domain)-1]
86 if not host and type != dns.PTR: # Request for the domain itself.
87 if type in (dns.A, dns.ALL_RECORDS):
88 record = dns.Record_A(config.dns.nameservers[0].ip, ttl)
89 results.append(dns.RRHeader(name, dns.A, dns.IN,
90 ttl, record, auth=True))
92 results.append(dns.RRHeader(domain, dns.NS, dns.IN,
93 ttl, self.ns, auth=True))
96 results.append(dns.RRHeader(domain, dns.SOA, dns.IN,
97 ttl, self.soa, auth=True))
98 else: # Request for a subdomain.
99 if name.endswith(".in-addr.arpa"): # Reverse resolution here
100 if type in (dns.PTR, dns.ALL_RECORDS):
101 ip = '.'.join(reversed(name.split('.')[:-2]))
102 value = invirt.database.NIC.query.filter_by(ip=ip).first()
103 if value and value.hostname:
104 hostname = value.hostname
105 if '.' not in hostname:
106 hostname = hostname + "." + config.dns.domains[0]
107 record = dns.Record_PTR(hostname, ttl)
108 results.append(dns.RRHeader(name, dns.PTR, dns.IN,
109 ttl, record, auth=True))
110 else: # IP address doesn't point to an active host
111 return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
112 # FIXME: Should only return success with no records if the name actually exists
113 else: # Forward resolution here
114 value = invirt.database.NIC.query.filter_by(hostname=host).first()
118 value = invirt.database.Machine.query().filter_by(name=host).first()
120 ip = value.nics[0].ip
122 return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
125 return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
127 if type in (dns.A, dns.ALL_RECORDS):
128 record = dns.Record_A(ip, ttl)
129 results.append(dns.RRHeader(name, dns.A, dns.IN,
130 ttl, record, auth=True))
131 elif type == dns.SOA:
132 results.append(dns.RRHeader(domain, dns.SOA, dns.IN,
133 ttl, self.soa, auth=True))
134 if len(results) == 0:
137 return defer.succeed((results, authority, additional))
140 return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
142 class QuotingBindAuthority(authority.BindAuthority):
144 A BindAuthority that (almost) deals with quoting correctly
146 This will catch double quotes as marking the start or end of a
147 quoted phrase, unless the double quote is escaped by a backslash
149 # Match either a quoted or unquoted string literal followed by
150 # whitespace or the end of line. This yields two groups, one of
151 # which has a match, and the other of which is None, depending on
152 # whether the string literal was quoted or unquoted; this is what
153 # necessitates the subsequent filtering out of groups that are
156 re.compile(r'"((?:[^"\\]|\\.)*)"|((?:[^\\\s]|\\.)+)(?:\s+|\s*$)')
158 # For interpreting escapes.
159 escape_pat = re.compile(r'\\(.)')
161 def collapseContinuations(self, lines):
166 if line.find('(') == -1:
169 L.append(line[:line.find('(')])
172 if line.find(')') != -1:
173 L[-1] += ' ' + line[:line.find(')')]
183 for m in self.string_pat.finditer(line):
184 [x] = [x for x in m.groups() if x is not None]
185 split_line.append(self.escape_pat.sub(r'\1', x))
187 return filter(None, L)
189 if '__main__' == __name__:
192 for zone in config.dns.zone_files:
193 for origin in config.dns.domains:
194 r = QuotingBindAuthority(zone)
195 # This sucks, but if I want a generic zone file, I have to
196 # reload the information by hand
198 lines = open(zone).readlines()
199 lines = r.collapseContinuations(r.stripComments(lines))
203 except InvirtConfigError:
204 # Don't care if zone_files isn't defined
206 resolvers.append(DatabaseAuthority())
209 f = server.DNSServerFactory(authorities=resolvers, verbose=verbosity)
210 p = dns.DNSDatagramProtocol(f)
211 f.noisy = p.noisy = verbosity
213 reactor.listenUDP(53, p)
214 reactor.listenTCP(53, f)