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.config import structs as config
11 import invirt.database
17 class DatabaseAuthority(common.ResolverBase):
18 """An Authority that is loaded from a file."""
22 def __init__(self, domains=None, database=None):
23 common.ResolverBase.__init__(self)
24 if database is not None:
25 invirt.database.connect(database)
27 invirt.database.connect()
28 if domains is not None:
29 self.domains = domains
31 self.domains = config.dns.domains
32 ns = config.dns.nameservers[0]
33 self.soa = dns.Record_SOA(mname=ns.hostname,
34 rname=config.dns.contact.replace('@','.',1),
35 serial=1, refresh=3600, retry=900,
36 expire=3600000, minimum=21600, ttl=3600)
37 self.ns = dns.Record_NS(name=ns.hostname, ttl=3600)
38 record = dns.Record_A(address=ns.ip, ttl=3600)
39 self.ns1 = dns.RRHeader(ns.hostname, dns.A, dns.IN,
40 3600, record, auth=True)
43 def _lookup(self, name, cls, type, timeout = None):
46 value = self._lookup_unsafe(name, cls, type, timeout = None)
47 except (psycopg2.OperationalError, sqlalchemy.exceptions.SQLError):
50 print "Reloading database"
56 def _lookup_unsafe(self, name, cls, type, timeout):
57 invirt.database.clear_cache()
62 if name in self.domains:
65 # Look for the longest-matching domain. (This works because domain
66 # will remain bound after breaking out of the loop.)
68 for domain in self.domains:
69 if name.endswith('.'+domain) and len(domain) > len(best_domain):
72 return defer.fail(failure.Failure(dns.DomainError(name)))
76 additional = [self.ns1]
77 authority.append(dns.RRHeader(domain, dns.NS, dns.IN,
78 3600, self.ns, auth=True))
81 host = name[:-len(domain)-1]
82 if not host: # Request for the domain itself.
83 if type in (dns.A, dns.ALL_RECORDS):
84 record = dns.Record_A(config.dns.nameservers[0].ip, ttl)
85 results.append(dns.RRHeader(name, dns.A, dns.IN,
86 ttl, record, auth=True))
88 results.append(dns.RRHeader(domain, dns.NS, dns.IN,
89 ttl, self.ns, auth=True))
92 results.append(dns.RRHeader(domain, dns.SOA, dns.IN,
93 ttl, self.soa, auth=True))
94 else: # Request for a subdomain.
95 value = invirt.database.Machine.query().filter_by(name=host).first()
96 if value is None or not value.nics:
97 return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
99 if ip is None: #Deactivated?
100 return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
102 if type in (dns.A, dns.ALL_RECORDS):
103 record = dns.Record_A(ip, ttl)
104 results.append(dns.RRHeader(name, dns.A, dns.IN,
105 ttl, record, auth=True))
106 elif type == dns.SOA:
107 results.append(dns.RRHeader(domain, dns.SOA, dns.IN,
108 ttl, self.soa, auth=True))
109 if len(results) == 0:
112 return defer.succeed((results, authority, additional))
115 return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
117 class QuotingBindAuthority(authority.BindAuthority):
119 A BindAuthority that (almost) deals with quoting correctly
121 This will catch double quotes as marking the start or end of a
122 quoted phrase, unless the double quote is escaped by a backslash
124 # Grab everything up to the first whitespace character or
125 # quotation mark not proceeded by a backslash
126 whitespace_re = re.compile(r'(.*?)([\t\n\x0b\x0c\r ]+|(?<!\\)")')
127 def collapseContinuations(self, lines):
132 if line.find('(') == -1:
135 L.append(line[:line.find('(')])
138 if line.find(')') != -1:
139 L[-1] += ' ' + line[:line.find(')')]
149 match = self.whitespace_re.match(line)
151 # If there's no match, that means that there's no
152 # whitespace in the rest of the line, so it should
153 # be treated as a single entity, quoted or not
155 # This also means that a closing quote isn't
156 # strictly necessary if the line ends the quote
160 substr, end = match.groups()
163 # If we're in the middle of the quote, the string
164 # we just grabbed belongs at the end of the
167 # Including the whitespace! Unless it's not
168 # whitespace and is actually a closequote instead
169 split_line[-1] += substr + (end if end != '"' else '')
171 # If we're not in the middle of a quote, than this
172 # is the next new string
173 split_line.append(substr)
176 in_quote = not in_quote
178 # Then strip off what we just processed
179 line = line[len(substr + end):]
181 return filter(None, L)
183 if '__main__' == __name__:
185 for zone in config.dns.zone_files:
186 for origin in config.dns.domains:
187 r = QuotingBindAuthority(zone)
188 # This sucks, but if I want a generic zone file, I have to
189 # reload the information by hand
191 lines = open(zone).readlines()
192 lines = r.collapseContinuations(r.stripComments(lines))
196 resolvers.append(DatabaseAuthority())
199 f = server.DNSServerFactory(authorities=resolvers, verbose=verbosity)
200 p = dns.DNSDatagramProtocol(f)
201 f.noisy = p.noisy = verbosity
203 reactor.listenUDP(53, p)
204 reactor.listenTCP(53, f)