1f337475aSchristos#!/usr/bin/env python3
2f337475aSchristos#
3f337475aSchristos# A plugin for the Unbound DNS resolver to resolve DNS records in
4f337475aSchristos# multicast DNS [RFC 6762] via Avahi.
5f337475aSchristos#
6f337475aSchristos# Copyright (C) 2018-2019 Internet Real-Time Lab, Columbia University
7f337475aSchristos# http://www.cs.columbia.edu/irt/
8f337475aSchristos#
9f337475aSchristos# Written by Jan Janak <janakj@cs.columbia.edu>
10f337475aSchristos#
11f337475aSchristos# Permission is hereby granted, free of charge, to any person
12f337475aSchristos# obtaining a copy of this software and associated documentation files
13f337475aSchristos# (the "Software"), to deal in the Software without restriction,
14f337475aSchristos# including without limitation the rights to use, copy, modify, merge,
15f337475aSchristos# publish, distribute, sublicense, and/or sell copies of the Software,
16f337475aSchristos# and to permit persons to whom the Software is furnished to do so,
17f337475aSchristos# subject to the following conditions:
18f337475aSchristos#
19f337475aSchristos# The above copyright notice and this permission notice shall be
20f337475aSchristos# included in all copies or substantial portions of the Software.
21f337475aSchristos#
22f337475aSchristos# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23f337475aSchristos# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24f337475aSchristos# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25f337475aSchristos# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
26f337475aSchristos# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
27f337475aSchristos# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
28f337475aSchristos# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29f337475aSchristos# SOFTWARE.
30f337475aSchristos#
31f337475aSchristos#
32f337475aSchristos# Dependendies:
33f337475aSchristos#   Unbound with pythonmodule configured for Python 3
34f337475aSchristos#   dnspython [http://www.dnspython.org]
35f337475aSchristos#   pydbus [https://github.com/LEW21/pydbus]
36f337475aSchristos#
37f337475aSchristos# To enable Python 3 support, configure Unbound as follows:
38f337475aSchristos#   PYTHON_VERSION=3 ./configure --with-pythonmodule
39f337475aSchristos#
40f337475aSchristos# The plugin in meant to be used as a fallback resolver that resolves
41f337475aSchristos# records in multicast DNS if the upstream server cannot be reached or
42f337475aSchristos# provides no answer (NXDOMAIN).
43f337475aSchristos#
44f337475aSchristos# mDNS requests for negative records, i.e., records for which Avahi
45f337475aSchristos# returns no answer (NXDOMAIN), are expensive. Since there is no
46f337475aSchristos# single authoritative server in mDNS, such requests terminate only
47f337475aSchristos# via a timeout. The timeout is about a second (if MDNS_TIMEOUT is not
48f337475aSchristos# configured), or the value configured via MDNS_TIMEOUT. The
49f337475aSchristos# corresponding Unbound thread will be blocked for this amount of
50f337475aSchristos# time. For this reason, it is important to configure an appropriate
51f337475aSchristos# number of threads in unbound.conf and limit the RR types and names
52f337475aSchristos# that will be resolved via Avahi via the environment variables
53f337475aSchristos# described later.
54f337475aSchristos#
55f337475aSchristos# An example unbound.conf with the plugin enabled:
56f337475aSchristos#
57f337475aSchristos# | server:
58f337475aSchristos# |   module-config: "validator python iterator"
59f337475aSchristos# |   num-threads: 32
60f337475aSchristos# |   cache-max-negative-ttl: 60
61f337475aSchristos# |   cache-max-ttl: 60
62*561252a2Schristos# | python:
63*561252a2Schristos# |   python-script: path/to/this/file
64f337475aSchristos#
65f337475aSchristos#
66f337475aSchristos# The plugin can also be run interactively. Provide the name and
67f337475aSchristos# record type to be resolved as command line arguments and the
68f337475aSchristos# resolved record will be printed to standard output:
69f337475aSchristos#
70f337475aSchristos#   $ ./avahi-resolver.py voip-phx4.phxnet.org A
71f337475aSchristos#   voip-phx4.phxnet.org. 120 IN A 10.4.3.2
72f337475aSchristos#
73f337475aSchristos#
74f337475aSchristos# The behavior of the plugin can be controlled via the following
75f337475aSchristos# environment variables:
76f337475aSchristos#
77f337475aSchristos# DBUS_SYSTEM_BUS_ADDRESS
78f337475aSchristos#
79f337475aSchristos# The address of the system DBus bus, in the format expected by DBus,
80f337475aSchristos# e.g., unix:path=/run/avahi/system-bus.sock
81f337475aSchristos#
82f337475aSchristos#
83f337475aSchristos# DEBUG
84f337475aSchristos#
85f337475aSchristos# Set this environment variable to "yes", "true", "on", or "1" to
86f337475aSchristos# enable debugging. In debugging mode, the plugin will output a lot
87f337475aSchristos# more information about what it is doing either to the standard
88f337475aSchristos# output (when run interactively) or to Unbound via log_info and
89f337475aSchristos# log_error.
90f337475aSchristos#
91f337475aSchristos# By default debugging is disabled.
92f337475aSchristos#
93f337475aSchristos#
94f337475aSchristos# MDNS_TTL
95f337475aSchristos#
96f337475aSchristos# Avahi does not provide the TTL value for the records it returns.
97f337475aSchristos# This environment variable can be used to configure the TTL value for
98f337475aSchristos# such records.
99f337475aSchristos#
100f337475aSchristos# The default value is 120 seconds.
101f337475aSchristos#
102f337475aSchristos#
103f337475aSchristos# MDNS_TIMEOUT
104f337475aSchristos#
105f337475aSchristos# The maximum amount of time (in milliseconds) an Avahi request is
106f337475aSchristos# allowed to run. This value sets the time it takes to resolve
107f337475aSchristos# negative (non-existent) records in Avahi. If unset, the request
108f337475aSchristos# terminates when Avahi sends the "AllForNow" signal, telling the
109f337475aSchristos# client that more records are unlikely to arrive. This takes roughly
110f337475aSchristos# about one second. You may need to configure a longer value here on
111f337475aSchristos# slower networks, e.g., networks that relay mDNS packets such as
112f337475aSchristos# MANETs.
113f337475aSchristos#
114f337475aSchristos#
115f337475aSchristos# MDNS_GETONE
116f337475aSchristos#
117f337475aSchristos# If set to "true", "1", or "on", an Avahi request will terminate as
118f337475aSchristos# soon as at least one record has been found. If there are multiple
119f337475aSchristos# nodes in the mDNS network publishing the same record, only one (or
120f337475aSchristos# subset) will be returned.
121f337475aSchristos#
122f337475aSchristos# If set to "false", "0", or "off", the plugin will gather records for
123f337475aSchristos# MDNS_TIMEOUT and return all records found. This is only useful in
124f337475aSchristos# networks where multiple nodes are known to publish different records
125f337475aSchristos# under the same name and the client needs to be able to obtain them
126f337475aSchristos# all. When configured this way, all Avahi requests will always take
127f337475aSchristos# MDNS_TIMEOUT to complete!
128f337475aSchristos#
129f337475aSchristos# This option is set to true by default.
130f337475aSchristos#
131f337475aSchristos#
132f337475aSchristos# MDNS_REJECT_TYPES
133f337475aSchristos#
134f337475aSchristos# A comma-separated list of record types that will NOT be resolved in
135f337475aSchristos# mDNS via Avahi. Use this environment variable to prevent specific
136f337475aSchristos# record types from being resolved via Avahi. For example, if your
137f337475aSchristos# network does not support IPv6, you can put AAAA on this list.
138f337475aSchristos#
139f337475aSchristos# The default value is an empty list.
140f337475aSchristos#
141f337475aSchristos# Example: MDNS_REJECT_TYPES=aaaa,mx,soa
142f337475aSchristos#
143f337475aSchristos#
144f337475aSchristos# MDNS_ACCEPT_TYPES
145f337475aSchristos#
146f337475aSchristos# If set, a record type will be resolved via Avahi if and only if it
147f337475aSchristos# is present on this comma-separated list. In other words, this is a
148f337475aSchristos# whitelist.
149f337475aSchristos#
150f337475aSchristos# The default value is an empty list which means all record types will
151f337475aSchristos# be resolved via Avahi.
152f337475aSchristos#
153f337475aSchristos# Example: MDNS_ACCEPT_TYPES=a,ptr,txt,srv,aaaa,cname
154f337475aSchristos#
155f337475aSchristos#
156f337475aSchristos# MDNS_REJECT_NAMES
157f337475aSchristos#
158f337475aSchristos# If the name being resolved matches the regular expression in this
159f337475aSchristos# environment variable, the name will NOT be resolved via Avahi. In
160f337475aSchristos# other words, this environment variable provides a blacklist.
161f337475aSchristos#
162f337475aSchristos# The default value is empty--no names will be reject.
163f337475aSchristos#
164f337475aSchristos# Example: MDNS_REJECT_NAMES=(^|\.)example\.com\.$
165f337475aSchristos#
166f337475aSchristos#
167f337475aSchristos# MDNS_ACCEPT_NAMES
168f337475aSchristos#
169f337475aSchristos# If set to a regular expression, a name will be resolved via Avahi if
170f337475aSchristos# and only if it matches the regular expression. In other words, this
171f337475aSchristos# variable provides a whitelist.
172f337475aSchristos#
173f337475aSchristos# The default value is empty--all names will be resolved via Avahi.
174f337475aSchristos#
175f337475aSchristos# Example: MDNS_ACCEPT_NAMES=^.*\.example\.com\.$
176f337475aSchristos#
177f337475aSchristos
178f337475aSchristosimport os
179f337475aSchristosimport re
180f337475aSchristosimport array
181f337475aSchristosimport threading
182f337475aSchristosimport traceback
183f337475aSchristosimport dns.rdata
184f337475aSchristosimport dns.rdatatype
185f337475aSchristosimport dns.rdataclass
186f337475aSchristosfrom queue import Queue
187f337475aSchristosfrom gi.repository import GLib
188f337475aSchristosfrom pydbus import SystemBus
189f337475aSchristos
190f337475aSchristos
191f337475aSchristosIF_UNSPEC    = -1
192f337475aSchristosPROTO_UNSPEC = -1
193f337475aSchristos
194f337475aSchristossysbus = None
195f337475aSchristosavahi = None
196f337475aSchristostrampoline = dict()
197f337475aSchristosthread_local = threading.local()
198f337475aSchristosdbus_thread = None
199f337475aSchristosdbus_loop = None
200f337475aSchristos
201f337475aSchristos
202f337475aSchristosdef str2bool(v):
203f337475aSchristos    if v.lower() in ['false', 'no', '0', 'off', '']:
204f337475aSchristos        return False
205f337475aSchristos    return True
206f337475aSchristos
207f337475aSchristos
208f337475aSchristosdef dbg(msg):
209f337475aSchristos    if DEBUG != False:
210f337475aSchristos        log_info('avahi-resolver: %s' % msg)
211f337475aSchristos
212f337475aSchristos
213f337475aSchristos#
214f337475aSchristos# Although pydbus has an internal facility for handling signals, we
215f337475aSchristos# cannot use that with Avahi. When responding from an internal cache,
216f337475aSchristos# Avahi sends the first signal very quickly, before pydbus has had a
217f337475aSchristos# chance to subscribe for the signal. This will result in lost signal
218f337475aSchristos# and missed data:
219f337475aSchristos#
220f337475aSchristos# https://github.com/LEW21/pydbus/issues/87
221f337475aSchristos#
222f337475aSchristos# As a workaround, we subscribe to all signals before creating a
223f337475aSchristos# record browser and do our own signal matching and dispatching via
224f337475aSchristos# the following function.
225f337475aSchristos#
226f337475aSchristosdef signal_dispatcher(connection, sender, path, interface, name, args):
227f337475aSchristos    o = trampoline.get(path, None)
228f337475aSchristos    if o is None:
229f337475aSchristos        return
230f337475aSchristos
231f337475aSchristos    if   name == 'ItemNew':    o.itemNew(*args)
232f337475aSchristos    elif name == 'ItemRemove': o.itemRemove(*args)
233f337475aSchristos    elif name == 'AllForNow':  o.allForNow(*args)
234f337475aSchristos    elif name == 'Failure':    o.failure(*args)
235f337475aSchristos
236f337475aSchristos
237f337475aSchristosclass RecordBrowser:
238f337475aSchristos    def __init__(self, callback, name, type_, timeout=None, getone=True):
239f337475aSchristos        self.callback = callback
240f337475aSchristos        self.records = []
241f337475aSchristos        self.error = None
242f337475aSchristos        self.getone = getone
243f337475aSchristos
244f337475aSchristos        self.timer = None if timeout is None else GLib.timeout_add(timeout, self.timedOut)
245f337475aSchristos
246f337475aSchristos        self.browser_path = avahi.RecordBrowserNew(IF_UNSPEC, PROTO_UNSPEC, name, dns.rdataclass.IN, type_, 0)
247f337475aSchristos        trampoline[self.browser_path] = self
248f337475aSchristos        self.browser = sysbus.get('.Avahi', self.browser_path)
249f337475aSchristos        self.dbg('Created RecordBrowser(name=%s, type=%s, getone=%s, timeout=%s)'
250f337475aSchristos                   % (name, dns.rdatatype.to_text(type_), getone, timeout))
251f337475aSchristos
252f337475aSchristos    def dbg(self, msg):
253f337475aSchristos        dbg('[%s] %s' % (self.browser_path, msg))
254f337475aSchristos
255f337475aSchristos    def _done(self):
256f337475aSchristos        del trampoline[self.browser_path]
257f337475aSchristos        self.dbg('Freeing')
258f337475aSchristos        self.browser.Free()
259f337475aSchristos
260f337475aSchristos        if self.timer is not None:
261f337475aSchristos            self.dbg('Removing timer')
262f337475aSchristos            GLib.source_remove(self.timer)
263f337475aSchristos
264f337475aSchristos        self.callback(self.records, self.error)
265f337475aSchristos
266f337475aSchristos    def itemNew(self, interface, protocol, name, class_, type_, rdata, flags):
267f337475aSchristos        self.dbg('Got signal ItemNew')
268f337475aSchristos        self.records.append((name, class_, type_, rdata))
269f337475aSchristos        if self.getone:
270f337475aSchristos            self._done()
271f337475aSchristos
272f337475aSchristos    def itemRemove(self, interface, protocol, name, class_, type_, rdata, flags):
273f337475aSchristos        self.dbg('Got signal ItemRemove')
274f337475aSchristos        self.records.remove((name, class_, type_, rdata))
275f337475aSchristos
276f337475aSchristos    def failure(self, error):
277f337475aSchristos        self.dbg('Got signal Failure')
278f337475aSchristos        self.error = Exception(error)
279f337475aSchristos        self._done()
280f337475aSchristos
281f337475aSchristos    def allForNow(self):
282f337475aSchristos        self.dbg('Got signal AllForNow')
283f337475aSchristos        if self.timer is None:
284f337475aSchristos            self._done()
285f337475aSchristos
286f337475aSchristos    def timedOut(self):
287f337475aSchristos        self.dbg('Timed out')
288f337475aSchristos        self._done()
289f337475aSchristos        return False
290f337475aSchristos
291f337475aSchristos
292f337475aSchristos#
293f337475aSchristos# This function runs the main event loop for DBus (GLib). This
294f337475aSchristos# function must be run in a dedicated worker thread.
295f337475aSchristos#
296f337475aSchristosdef dbus_main():
297f337475aSchristos    global sysbus, avahi, dbus_loop
298f337475aSchristos
299f337475aSchristos    dbg('Connecting to system DBus')
300f337475aSchristos    sysbus = SystemBus()
301f337475aSchristos
302f337475aSchristos    dbg('Subscribing to .Avahi.RecordBrowser signals')
303f337475aSchristos    sysbus.con.signal_subscribe('org.freedesktop.Avahi',
304f337475aSchristos        'org.freedesktop.Avahi.RecordBrowser',
305f337475aSchristos        None, None, None, 0, signal_dispatcher)
306f337475aSchristos
307f337475aSchristos    avahi = sysbus.get('.Avahi', '/')
308f337475aSchristos
309f337475aSchristos    dbg("Connected to Avahi Daemon: %s (API %s) [%s]"
310f337475aSchristos             % (avahi.GetVersionString(), avahi.GetAPIVersion(), avahi.GetHostNameFqdn()))
311f337475aSchristos
312f337475aSchristos    dbg('Starting DBus main loop')
313f337475aSchristos    dbus_loop = GLib.MainLoop()
314f337475aSchristos    dbus_loop.run()
315f337475aSchristos
316f337475aSchristos
317f337475aSchristos#
318f337475aSchristos# This function must be run in the DBus worker thread. It creates a
319f337475aSchristos# new RecordBrowser instance and once it has finished doing it thing,
320f337475aSchristos# it will send the result back to the original thread via the queue.
321f337475aSchristos#
322f337475aSchristosdef start_resolver(queue, *args, **kwargs):
323f337475aSchristos    try:
324f337475aSchristos        RecordBrowser(lambda *v: queue.put_nowait(v), *args, **kwargs)
325f337475aSchristos    except Exception as e:
326f337475aSchristos        queue.put_nowait((None, e))
327f337475aSchristos
328f337475aSchristos    return False
329f337475aSchristos
330f337475aSchristos
331f337475aSchristos#
332f337475aSchristos# To resolve a request, we setup a queue, post a task to the DBus
333f337475aSchristos# worker thread, and wait for the result (or error) to arrive over the
334f337475aSchristos# queue. If the worker thread reports an error, raise the error as an
335f337475aSchristos# exception.
336f337475aSchristos#
337f337475aSchristosdef resolve(*args, **kwargs):
338f337475aSchristos    try:
339f337475aSchristos        queue = thread_local.queue
340f337475aSchristos    except AttributeError:
341f337475aSchristos        dbg('Creating new per-thread queue')
342f337475aSchristos        queue = Queue()
343f337475aSchristos        thread_local.queue = queue
344f337475aSchristos
345f337475aSchristos    GLib.idle_add(lambda: start_resolver(queue, *args, **kwargs))
346f337475aSchristos
347f337475aSchristos    records, error = queue.get()
348f337475aSchristos    queue.task_done()
349f337475aSchristos
350f337475aSchristos    if error is not None:
351f337475aSchristos        raise error
352f337475aSchristos
353f337475aSchristos    return records
354f337475aSchristos
355f337475aSchristos
356f337475aSchristosdef parse_type_list(lst):
357f337475aSchristos    return list(map(dns.rdatatype.from_text, [v.strip() for v in lst.split(',') if len(v)]))
358f337475aSchristos
359f337475aSchristos
360f337475aSchristosdef init(*args, **kwargs):
361f337475aSchristos    global dbus_thread, DEBUG
362f337475aSchristos    global MDNS_TTL, MDNS_GETONE, MDNS_TIMEOUT
363f337475aSchristos    global MDNS_REJECT_TYPES, MDNS_ACCEPT_TYPES
364f337475aSchristos    global MDNS_REJECT_NAMES, MDNS_ACCEPT_NAMES
365f337475aSchristos
366f337475aSchristos    DEBUG = str2bool(os.environ.get('DEBUG', str(False)))
367f337475aSchristos
368f337475aSchristos    MDNS_TTL = int(os.environ.get('MDNS_TTL', 120))
369f337475aSchristos    dbg("TTL for records from Avahi: %d" % MDNS_TTL)
370f337475aSchristos
371f337475aSchristos    MDNS_REJECT_TYPES = parse_type_list(os.environ.get('MDNS_REJECT_TYPES', ''))
372f337475aSchristos    if MDNS_REJECT_TYPES:
373f337475aSchristos        dbg('Types NOT resolved via Avahi: %s' % MDNS_REJECT_TYPES)
374f337475aSchristos
375f337475aSchristos    MDNS_ACCEPT_TYPES = parse_type_list(os.environ.get('MDNS_ACCEPT_TYPES', ''))
376f337475aSchristos    if MDNS_ACCEPT_TYPES:
377f337475aSchristos        dbg('ONLY resolving the following types via Avahi: %s' % MDNS_ACCEPT_TYPES)
378f337475aSchristos
379f337475aSchristos    v = os.environ.get('MDNS_REJECT_NAMES', None)
380f337475aSchristos    MDNS_REJECT_NAMES = re.compile(v, flags=re.I | re.S) if v is not None else None
381f337475aSchristos    if MDNS_REJECT_NAMES is not None:
382f337475aSchristos        dbg('Names NOT resolved via Avahi: %s' % MDNS_REJECT_NAMES.pattern)
383f337475aSchristos
384f337475aSchristos    v = os.environ.get('MDNS_ACCEPT_NAMES', None)
385f337475aSchristos    MDNS_ACCEPT_NAMES = re.compile(v, flags=re.I | re.S) if v is not None else None
386f337475aSchristos    if MDNS_ACCEPT_NAMES is not None:
387f337475aSchristos        dbg('ONLY resolving the following names via Avahi: %s' % MDNS_ACCEPT_NAMES.pattern)
388f337475aSchristos
389f337475aSchristos    v = os.environ.get('MDNS_TIMEOUT', None)
390f337475aSchristos    MDNS_TIMEOUT = int(v) if v is not None else None
391f337475aSchristos    if MDNS_TIMEOUT is not None:
392f337475aSchristos        dbg('Avahi request timeout: %s' % MDNS_TIMEOUT)
393f337475aSchristos
394f337475aSchristos    MDNS_GETONE = str2bool(os.environ.get('MDNS_GETONE', str(True)))
395f337475aSchristos    dbg('Terminate Avahi requests on first record: %s' % MDNS_GETONE)
396f337475aSchristos
397f337475aSchristos    dbus_thread = threading.Thread(target=dbus_main)
398f337475aSchristos    dbus_thread.daemon = True
399f337475aSchristos    dbus_thread.start()
400f337475aSchristos
401f337475aSchristos
402f337475aSchristosdef deinit(*args, **kwargs):
403f337475aSchristos    dbus_loop.quit()
404f337475aSchristos    dbus_thread.join()
405f337475aSchristos    return True
406f337475aSchristos
407f337475aSchristos
408f337475aSchristosdef inform_super(id, qstate, superqstate, qdata):
409f337475aSchristos    return True
410f337475aSchristos
411f337475aSchristos
412f337475aSchristosdef get_rcode(msg):
413f337475aSchristos    if not msg:
414f337475aSchristos        return RCODE_SERVFAIL
415f337475aSchristos
416f337475aSchristos    return msg.rep.flags & 0xf
417f337475aSchristos
418f337475aSchristos
419f337475aSchristosdef rr2text(rec, ttl):
420f337475aSchristos    name, class_, type_, rdata = rec
421f337475aSchristos    wire = array.array('B', rdata).tostring()
422f337475aSchristos    return '%s. %d %s %s %s' % (
423f337475aSchristos        name,
424f337475aSchristos        ttl,
425f337475aSchristos        dns.rdataclass.to_text(class_),
426f337475aSchristos        dns.rdatatype.to_text(type_),
427f337475aSchristos        dns.rdata.from_wire(class_, type_, wire, 0, len(wire), None))
428f337475aSchristos
429f337475aSchristos
430f337475aSchristosdef operate(id, event, qstate, qdata):
431f337475aSchristos    qi = qstate.qinfo
432f337475aSchristos    name = qi.qname_str
433f337475aSchristos    type_ = qi.qtype
434f337475aSchristos    type_str = dns.rdatatype.to_text(type_)
435f337475aSchristos    class_ = qi.qclass
436f337475aSchristos    class_str = dns.rdataclass.to_text(class_)
437f337475aSchristos    rc = get_rcode(qstate.return_msg)
438f337475aSchristos
439f337475aSchristos    if event == MODULE_EVENT_NEW or event == MODULE_EVENT_PASS:
440f337475aSchristos        qstate.ext_state[id] = MODULE_WAIT_MODULE
441f337475aSchristos        return True
442f337475aSchristos
443f337475aSchristos    if event != MODULE_EVENT_MODDONE:
444f337475aSchristos        log_err("avahi-resolver: Unexpected event %d" % event)
445f337475aSchristos        qstate.ext_state[id] = MODULE_ERROR
446f337475aSchristos        return True
447f337475aSchristos
448f337475aSchristos    qstate.ext_state[id] = MODULE_FINISHED
449f337475aSchristos
450f337475aSchristos    # Only resolve via Avahi if we got NXDOMAIn from the upstream DNS
451f337475aSchristos    # server, or if we could not reach the upstream DNS server. If we
452f337475aSchristos    # got some records for the name from the upstream DNS server
453f337475aSchristos    # already, do not resolve the record in Avahi.
454f337475aSchristos    if rc != RCODE_NXDOMAIN and rc != RCODE_SERVFAIL:
455f337475aSchristos        return True
456f337475aSchristos
457f337475aSchristos    dbg("Got request for '%s %s %s'" % (name, class_str, type_str))
458f337475aSchristos
459f337475aSchristos    # Avahi only supports the IN class
460f337475aSchristos    if class_ != RR_CLASS_IN:
461f337475aSchristos        dbg('Rejected, Avahi only supports the IN class')
462f337475aSchristos        return True
463f337475aSchristos
464f337475aSchristos    # Avahi does not support meta queries (e.g., ANY)
465f337475aSchristos    if dns.rdatatype.is_metatype(type_):
466f337475aSchristos        dbg('Rejected, Avahi does not support the type %s' % type_str)
467f337475aSchristos        return True
468f337475aSchristos
469f337475aSchristos    # If we have a type blacklist and the requested type is on the
470f337475aSchristos    # list, reject it.
471f337475aSchristos    if MDNS_REJECT_TYPES and type_ in MDNS_REJECT_TYPES:
472f337475aSchristos        dbg('Rejected, type %s is on the blacklist' % type_str)
473f337475aSchristos        return True
474f337475aSchristos
475f337475aSchristos    # If we have a type whitelist and if the requested type is not on
476f337475aSchristos    # the list, reject it.
477f337475aSchristos    if MDNS_ACCEPT_TYPES and type_ not in MDNS_ACCEPT_TYPES:
478f337475aSchristos        dbg('Rejected, type %s is not on the whitelist' % type_str)
479f337475aSchristos        return True
480f337475aSchristos
481f337475aSchristos    # If we have a name blacklist and if the requested name matches
482f337475aSchristos    # the blacklist, reject it.
483f337475aSchristos    if MDNS_REJECT_NAMES is not None:
484f337475aSchristos        if MDNS_REJECT_NAMES.search(name):
485f337475aSchristos            dbg('Rejected, name %s is on the blacklist' % name)
486f337475aSchristos            return True
487f337475aSchristos
488f337475aSchristos    # If we have a name whitelist and if the requested name does not
489f337475aSchristos    # match the whitelist, reject it.
490f337475aSchristos    if MDNS_ACCEPT_NAMES is not None:
491f337475aSchristos        if not MDNS_ACCEPT_NAMES.search(name):
492f337475aSchristos            dbg('Rejected, name %s is not on the whitelist' % name)
493f337475aSchristos            return True
494f337475aSchristos
495f337475aSchristos    dbg("Resolving '%s %s %s' via Avahi" % (name, class_str, type_str))
496f337475aSchristos
497f337475aSchristos    recs = resolve(name, type_, getone=MDNS_GETONE, timeout=MDNS_TIMEOUT)
498f337475aSchristos
499f337475aSchristos    if not recs:
500f337475aSchristos        dbg('Result: Not found (NXDOMAIN)')
501f337475aSchristos        qstate.return_rcode = RCODE_NXDOMAIN
502f337475aSchristos        return True
503f337475aSchristos
504f337475aSchristos    m = DNSMessage(name, type_, class_, PKT_QR | PKT_RD | PKT_RA)
505f337475aSchristos    for r in recs:
506f337475aSchristos        s = rr2text(r, MDNS_TTL)
507f337475aSchristos        dbg('Result: %s' % s)
508f337475aSchristos        m.answer.append(s)
509f337475aSchristos
510f337475aSchristos    if not m.set_return_msg(qstate):
511f337475aSchristos        raise Exception("Error in set_return_msg")
512f337475aSchristos
513f337475aSchristos    if not storeQueryInCache(qstate, qstate.return_msg.qinfo, qstate.return_msg.rep, 0):
514f337475aSchristos        raise Exception("Error in storeQueryInCache")
515f337475aSchristos
516f337475aSchristos    qstate.return_msg.rep.security = 2
517f337475aSchristos    qstate.return_rcode = RCODE_NOERROR
518f337475aSchristos    return True
519f337475aSchristos
520f337475aSchristos
521f337475aSchristos#
522f337475aSchristos# It does not appear to be sufficient to check __name__ to determine
523f337475aSchristos# whether we are being run in interactive mode. As a workaround, try
524f337475aSchristos# to import module unboundmodule and if that fails, assume we're being
525f337475aSchristos# run in interactive mode.
526f337475aSchristos#
527f337475aSchristostry:
528f337475aSchristos    import unboundmodule
529f337475aSchristos    embedded = True
530f337475aSchristosexcept ImportError:
531f337475aSchristos    embedded = False
532f337475aSchristos
533f337475aSchristosif __name__ == '__main__' and not embedded:
534f337475aSchristos    import sys
535f337475aSchristos
536f337475aSchristos    def log_info(msg):
537f337475aSchristos        print(msg)
538f337475aSchristos
539f337475aSchristos    def log_err(msg):
540f337475aSchristos        print('ERROR: %s' % msg, file=sys.stderr)
541f337475aSchristos
542f337475aSchristos    if len(sys.argv) != 3:
543f337475aSchristos        print('Usage: %s <name> <rr_type>' % sys.argv[0])
544f337475aSchristos        sys.exit(2)
545f337475aSchristos
546f337475aSchristos    name = sys.argv[1]
547f337475aSchristos    type_str = sys.argv[2]
548f337475aSchristos
549f337475aSchristos    try:
550f337475aSchristos        type_ = dns.rdatatype.from_text(type_str)
551f337475aSchristos    except dns.rdatatype.UnknownRdatatype:
552f337475aSchristos        log_err('Unsupported DNS record type "%s"' % type_str)
553f337475aSchristos        sys.exit(2)
554f337475aSchristos
555f337475aSchristos    if dns.rdatatype.is_metatype(type_):
556f337475aSchristos        log_err('Meta record type "%s" cannot be resolved via Avahi' % type_str)
557f337475aSchristos        sys.exit(2)
558f337475aSchristos
559f337475aSchristos    init()
560f337475aSchristos    try:
561f337475aSchristos        recs = resolve(name, type_, getone=MDNS_GETONE, timeout=MDNS_TIMEOUT)
562f337475aSchristos        if not len(recs):
563f337475aSchristos            print('%s not found (NXDOMAIN)' % name)
564f337475aSchristos            sys.exit(1)
565f337475aSchristos
566f337475aSchristos        for r in recs:
567f337475aSchristos            print(rr2text(r, MDNS_TTL))
568f337475aSchristos    finally:
569f337475aSchristos        deinit()
570