#!/usr/bin/python2.7 """ Configure UCS-domain and forward DNS servers. """ # Copyright 2016-2017 Univention GmbH # # http://www.univention.de/ # # All rights reserved. # # The source code of this program is made available # under the terms of the GNU Affero General Public License version 3 # (GNU AGPL V3) as published by the Free Software Foundation. # # Binary versions of this program provided by Univention to you as # well as other copyrighted, protected or trademarked materials like # Logos, graphics, fonts, specific documentations and configurations, # cryptographic keys etc. are subject to a license agreement between # you and Univention and not subject to the GNU AGPL V3. # # In the case you use this program under the terms of the GNU AGPL V3, # the program is provided in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public # License with the Debian GNU/Linux or Univention distribution in file # /usr/share/common-licenses/AGPL-3; if not, see # . from os import environ from sys import modules, stderr from collections import OrderedDict from subprocess import check_output, check_call from optparse import OptionParser, SUPPRESS_HELP from logging import getLogger, basicConfig, DEBUG, INFO, WARNING, ERROR from univention.config_registry import ConfigRegistry from univention.config_registry.frontend import ucr_update from univention.config_registry.interfaces import Interfaces from DNS import DnsRequest, SocketError, TimeoutError from ipaddr import IPAddress, Bytes UCR_VARS_FWD = ['dns/forwarder%d' % (i,) for i in range(1, 4)] UCR_VARS_DNS = ['nameserver%d' % (i,) for i in range(1, 4)] LOCAL = '127.0.0.1' # or ::1 for IPv6 def main(): options = parse_args() setup_logging(options) log = getLogger(__name__) if options.run_tests: run_tests() ucr = ConfigRegistry() ucr.load() nameservers, forwarders = OrderedDict(), OrderedDict() add_self(nameservers, ucr) get_forwarders(forwarders, ucr) get_nameservers(nameservers, forwarders, ucr) action_required = validate_servers(nameservers, forwarders, ucr['domainname'], options) if not action_required: log.info("No action required.") return add_nameservers(nameservers, ucr['domainname'], options) add_master(nameservers, ucr['ldap/master'], options) update_ucr(ucr, nameservers, forwarders, options) def parse_args(): usage = '%prog [options]' description = modules[__name__].__doc__ parser = OptionParser(usage=usage, description=description) parser.add_option( '--verbose', '-v', action='count', default=2, help='Increase verbosity') parser.add_option( '--no-act', '-n', action='store_true', help='Enable dry-run mode') parser.add_option( '--ipv6', '-6', action='store_const', const=('A', 'AAAA'), default=('A',), dest='rrtyp', help='Also add IPv6 addresses') parser.add_option( '--no-master', '-M', action='store_true', help='Do not add domaincontroller_master as name-server') parser.add_option( '--no-nameservers', '-N', action='store_true', help='Do not add other name-servers') parser.add_option( '--no-validation', '-V', action='store_true', help='Do not validate DNS servers') parser.add_option( '--run-tests', action='store_true', help=SUPPRESS_HELP) options, args = parser.parse_args() if args: parser.error('No argument expected') return options def setup_logging(options): FORMAT = '%(asctime)-15s %(levelname)-7s %(name)-17s %(message)s' LEVELS = [ERROR, WARNING, INFO, DEBUG] try: level = LEVELS[options.verbose] except IndexError: level = LEVELS[-1] basicConfig(format=FORMAT, level=level, stream=stderr) def add_self(nameservers, ucr): global myself log = getLogger(__name__).getChild('ucr/self') iface = Interfaces(ucr) mynet = iface.get_default_ip_address() myself = mynet.ip log.info('Default IP address configured in UCR: %s', myself) log.debug('Checking DNS server is responding on %s', myself) try: r = DnsRequest(ucr['domainname'], qtype='SOA', server=['%s' % (myself,)], aa=1, rd=0).req() log.debug('header=%r', r.header) except (SocketError, TimeoutError) as exc: log.error('Query failed (%s), skip putting myself as nameserver1', exc.args[0]) else: nameservers[myself] = None def get_forwarders(forwarders, ucr): log = getLogger(__name__).getChild('ucr/fwd') for var in UCR_VARS_FWD: fwd = ucr.get(var, '').strip() if not fwd: continue fwd = IPAddress(fwd) if is_self(fwd): log.info("Found local interface address %s configured as %s, dropping that because it's cyclic", fwd, var) continue else: log.info('Found server %s configured as %s', fwd, var) forwarders[fwd] = None def get_nameservers(nameservers, forwarders, ucr): log = getLogger(__name__).getChild('ucr/ns') for var in UCR_VARS_DNS: dns = ucr.get(var, '').strip() if not dns: continue dns = IPAddress(dns) if dns in forwarders: log.info('Dropping %s as %s, already configured as forwarder', dns, var) continue if is_self(dns): log.info('Found local interface address %s configured as %s', dns, var) if nameservers and nameservers.keys()[0] == myself: continue else: log.info('Found server %s configured as %s', dns, var) nameservers[dns] = None def validate_servers(nameservers, forwarders, domain, options): log = getLogger(__name__).getChild('val') if options.no_validation: log.info('Skip validation of DNS servers') return action_required = False rec = '_domaincontroller_master._tcp.%s.' % (domain.rstrip('.'),) for server in nameservers: log.debug('Querying %s for SRV %s', server, rec) try: r = DnsRequest(rec, qtype='SRV', server=['%s' % (server,)], aa=1, rd=0).req() log.debug('header=%r', r.header) except (SocketError, TimeoutError) as exc: log.error('Connection check to %s (%s) failed, maybe down?!', server, exc.args[0]) log.info('Leaving it configured as nameserver anyway') continue if r.header['status'] == 'NOERROR' and r.header['aa']: log.info('Validated UCS domain server: %s', server) else: log.warn('UCS master SRV record is unknown at %s, converting into forwarder', server) action_required = True del nameservers[server] forwarders[server] = None return action_required def add_nameservers(nameservers, domain, options): log = getLogger(__name__).getChild('ns') if options.no_nameservers: log.info('Skip adding NS') return log.debug('Querying %s for additional NS records in %s', LOCAL, domain) try: r = DnsRequest(domain, qtype='NS', server=[LOCAL], aa=1, rd=0).req() log.debug('header=%r', r.header) except (SocketError, TimeoutError) as exc: log.error('Querying %s failed (%s), skip adding NS', LOCAL, exc.args[0]) return if r.header['status'] == 'NOERROR' and r.header['aa']: names = set(rr['data'] for rr in r.answers) log.debug('servers=%r', names) for rr in r.additional: log.debug('rr=%r', rr) name = rr['name'] if rr['typename'] in options.rrtyp and name in names: ip = get_ip(rr) if is_self(ip): log.info('Skipping local interface address %s found for NS record %s', ip, name) continue log.info('Adding server found in NS: %s=%s', name, ip) nameservers[ip] = None names.remove(name) else: log.error('DNS lookup of NS records in %s against %s failed', domain, LOCAL) def add_master(nameservers, master, options): log = getLogger(__name__).getChild('ldap') if options.no_master: log.info('Skip adding master') return log.debug('Querying %s for address of master %s', LOCAL, master) try: r = DnsRequest(master, qtype='ANY', server=[LOCAL], aa=1, rd=0).req() log.debug('header=%r', r.header) except (SocketError, TimeoutError) as exc: log.error('Querying %s failed (%s), skip adding master', LOCAL, exc.args[0]) return if r.header['status'] == 'NOERROR' and r.header['aa']: for rr in r.answers: log.debug('rr=%r', rr) if rr['typename'] in options.rrtyp: ip = get_ip(rr) if is_self(ip): log.info('Skipping local interface address %s found for ldap/master %s', ip, master) continue log.info('Adding master %s', ip) nameservers[ip] = None break else: log.error('DNS lookup of %s against %s failed', master, LOCAL) def show_ucr_changes(names, ucr, new_ucr_settings): log = getLogger(__name__).getChild('ucr') for var in names: old = ucr.get(var, None) new = new_ucr_settings.get(var, None) if new == old: continue if not new: log.warn("unset '%s' old:%s", var, old) elif not old: log.warn("set %s=%s old:[Previously undefined]", var, new) else: log.warn("set %s=%s old:%s", var, new, old) def update_ucr(ucr, nameservers, forwarders, options): log = getLogger(__name__).getChild('ucr') new_ucr_settings = {} def update(names, values, typ): log.debug('%s=%r', typ, values) values = map(str, values) diff = len(names) - len(values) if diff > 0: values += [None] * diff elif diff < 0: log.warn('Skipping extra %s: %r', typ, values[len(names):]) new_ucr_settings.update(dict(zip(names, values))) update(UCR_VARS_FWD, forwarders, 'forwarders') update(UCR_VARS_DNS, nameservers, 'nameservers') for names in (UCR_VARS_FWD, UCR_VARS_DNS): show_ucr_changes(names, ucr, new_ucr_settings) # log.info('Updating %r', new_ucr_settings) if not options.no_act: ucr_update(ucr, new_ucr_settings) log.info('Reloading BIND') check_call(('rndc', 'reconfig')) def get_ip(rr): r""" >>> get_ip({'typename': 'A', 'data': '127.0.0.1'}) IPv4Address('127.0.0.1') >>> get_ip({'typename': 'AAAA', 'data': '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01'}) IPv6Address('::1') """ typ, data = rr['typename'], rr['data'] if typ == 'A': return IPAddress(data) elif typ == 'AAAA': # Work-around bug in python-pydns, which does not unpack IPv6 addresses assert len(data) == 16 _cb = Bytes if issubclass(Bytes, str) else lambda bytestr: bytes(bytestr, 'charmap') return IPAddress(_cb(data)) else: raise TypeError(typ) def is_self(addr): """ >>> is_self('127.0.0.1') True >>> is_self('::1') True >>> is_self('8.8.8.8') False """ log = getLogger(__name__).getChild('ip') env = dict(environ) env['LC_ALL'] = 'C' cmd = ['ip', 'route', 'get', '%s' % addr] log.debug('calling %r', cmd) out = check_output(cmd, env=env) return out.startswith('local ') def run_tests(): import doctest doctest.testmod() if __name__ == '__main__': main()