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