Пример #1
0
def main():
    killer = Killer()

    args = _parse_argv()

    logger = get_logger("DNSWatch", log_level=args.loglevel, log_file=args.logfile)
    logger.info("Starting dnswatch v.{}.".format(__version__))

    misc = Misc(logger)

    try: 
        exit_code = 2

        if not get_lock("dnswatch", timeout=5):
            misc.die("Lock exists")

        c = Config()
        action = None
        while True:
            config = c.read(args.config)
            dw = DNSWatch(config)

            if action == 'reload':
                dw.reload_config()
            else:
                dw.initial_config()
            
            try: 
                action = dw.watch(pause=config["watch"]["pause"])
            except: 
                action = dw.watch()

            if action == "kill":
                # Do DNS cleanup and exit loop
                dw.cleanup()
                break
            elif action == "softkill":
                # Exit loop without DNS cleanup
                break
            elif action == "reload":
                # Do nothing
                pass
            else:
                misc.die("Unknown action requested: {}".format(action))

        logger.info("Finished successfully.")
        exit_code = 0
    except SystemExit:
        exit_code = sys.exc_info()[1]
    except:
        logger.error("Exiting with errors.")
        if args.trace:
            print(sys.exc_info())
            trace = sys.exc_info()[2]
            traceback.print_tb(trace)
            exit_code = 1
    finally:
        sys.exit(exit_code)
Пример #2
0
class DHClient:

    def __init__(self):
        self.logger = logging.getLogger("DNSWatch.DHClient")
        self.misc = Misc(self.logger)
        self.args = self._collect_args()
        self.config_files = ["/etc/dhcp/dhclient.conf"]
        self.config_updated = False

    def _collect_args(self):
        self.logger.debug("Looking for dhclient process arguments.")

        default_cmdline = ['dhclient',
                '-1',
                '-v',
                '-pf',
                '/run/dhclient.eth0.pid',
                '-lf',
                '/var/lib/dhcp/dhclient.eth0.leases',
                'eth0']
        for proc in psutil.process_iter():
            if re.match("^dhclient\d*$", proc.name):
                self.logger.debug("dhclient cmdline: '{}'.".format(
                                    " ".join(proc.cmdline)))
                return proc.cmdline

        self.logger.warning(
            "dhclient process not found. Falling back to default: '{}'.".format(
                " ".join(default_cmdline)))
        self._request_lease(default_cmdline)
        return default_cmdline

    def set_nameserver(self, ns):
        self.logger.debug("Setting nameserver: {}.".format(ns))
        if not self._set_option("supersede", "domain-name-servers", ", ".join(ns)):
            self.misc.die("Failed to set nameserver for dhclient")

    def get_nameserver(self):
        self.logger.debug("Getting nameserver from dhclient config.")
        return self._get_option("domain-name-servers", otype="supersede")[2]

    def set_search(self, domain):
        self.logger.debug("Setting search domain: {}.".format(domain))
        if not self._set_option("prepend", "domain-name", '"{} "'.format(" ".join(domain))):
            self.misc.die("Failed to set search domain for dhclient")

    def renew_lease(self):
        self._release_lease(self.args)
        self._request_lease(self.args)
        self.config_updated = False

    def _release_lease(self, args_list):
        self.logger.debug("Releasing DHCP lease.")
        args = list(args_list)
        args.append("-r")
        FNULL = open(os.devnull, 'w')
        # If close_fds is true, all file descriptors except 0, 1 and 2 will be
        # closed before the child process is executed. (Unix only).
        check_call(args, stdout=FNULL, stderr=STDOUT, close_fds=True)

    def _request_lease(self, args_list):
        self.logger.debug("Requesting DHCP lease.")
        FNULL = open(os.devnull, 'w')
        # If close_fds is true, all file descriptors except 0, 1 and 2 will be
        # closed before the child process is executed. (Unix only).
        #
        # Here it's a must because in other case dhclient will inherit lock
        # file/socket. Which will prevent future starts.
        check_call(args_list, stdout=FNULL, stderr=STDOUT, close_fds=True)

    def _set_option(self, otype, option, value):
        if not otype in ["append", "prepend", "supersede"]:
            self.misc.die("Unknown dhclient option type: {}".format(otype))

        new_line = "{} {} {};".format(otype, option, value)
        new_config = list()
        option_exist = False
        write_config = False

        config_file = self._get_config_file()
        config = self._read_config(config_file)
        for line in config:
            if re.match("^{}\s+{}\s+.*;$".format(otype, option), line):
                option_exist = True
                self.logger.debug("Option '{}' exist, checking value.".format(option))
                if re.match("^{}\s+{}\s+{};$".format(otype, option, value), line):
                    self.logger.debug("Value '{}' is the same, skipping.".format(value))
                    new_config.append(line)
                else:
                    self.logger.debug("Values differ, updating to '{}'.".format(value))
                    write_config = True
                    new_config.append(new_line)
                    continue
            new_config.append(line)

        if not option_exist:
            write_config = True
            new_config.append(new_line)

        if write_config:
            if self._write_config(new_config, config_file):
                self.config_updated = True
                return True
            else:
                return False
        else:
            return True

    def _get_option(self, option, otype=".+"):
        config_file = self._get_config_file()
        config = self._read_config(config_file)
        return self._read_option(option, config, otype)

    def _read_option(self, option, config, otype):
        result = None
        for line in config:
            rm = re.match("^({})\s+{}\s+(.+);$".format(otype, option), line)
            if rm:
                otype = rm.group(1)
                value = [ e.strip() for e in rm.group(2).split(",")]
                result = [otype, option, value]
        return result

    def _get_config_file(self):
        for config_file in self.config_files:
            if os.path.isfile(config_file):
                return config_file

    def _read_config(self, config_file):
        config = list()
        full_line = ""
        with open(config_file, "r") as cf:
            for line in cf.readlines():
                line = line.strip()
                if not re.match("^(.*#.*|)$", line):
                    if len(full_line) > 0:
                        line = full_line + line
                    if line[-1] != ";":
                        full_line = line
                    else:
                        config.append(line)
                        full_line = ""
        return set(config)

    def _write_config(self, config, config_file):
        self._backup_config(config_file)
        self.logger.debug("Writing new config.")
        with open(config_file, "w") as cf:
            for line in config:
                cf.write("{}\n".format(line))
        return True

    def _backup_config(self, config_file):
        backup_file = "{}.bak-{}".format(config_file, time.time())
        self.logger.debug("Doing backup of {} to {}.".format(config_file, backup_file))
        copyfile(config_file, backup_file)
        return True
Пример #3
0
class BindProvider:

    def __init__(self, config):
        self.logger = logging.getLogger("DNSWatch.BindProvider")
        self.misc = Misc(self.logger)
        self.dhcl = DHClient()
        self.dnso = DNSOps(config["dnsupdate"])

        self.zone = config["dnsupdate"]["zone"]
        self.fqdn = config["host"]["fqdn"]
        self.private_ip = config["host"]["private_ip"]
        self.public_ip = config["host"]["public_ip"]
        self.alias_dict = config["dnsupdate"]["alias"]
        self.aliases = None

    def initial_config(self):
        """To do on start"""
        self._initial_config_wo_resolvers()

        if len(self.slaves["private"]) > 0:
            self._setup_resolver(
                self.slaves["private"], [self.zone])
        else:
            self.misc.die("No private DNS slaves found: {}.".format(self.slaves))

    def reload_config(self):
        """To do on reload"""
        self._initial_config_wo_resolvers()

    def _initial_config_wo_resolvers(self):
        self.dnso.setup_key()

        self.masters = self.dnso.get_masters()
        self.aliases = Provider()._look_for_alias(self.fqdn, self.zone, self.alias_dict)

        if not self._update_records(self.masters['private'], self.private_ip, ptr=True):
            self.misc.die("DNS update of PRIVATE view failed on all masters: {}".format(self.masters['private']))
        if not self._update_records(self.masters['public'],self.public_ip, ptr=False):
            self.misc.die("DNS update of PUBLIC view failed on all masters: {}".format(self.masters['public']))

        self.slaves = self.dnso.get_slaves(self.masters)

    def watch(self):
        """Some periodic actions"""
    	# Check if masters changed
        new_masters = self.dnso.get_masters()
        if (self._list_changed(self.masters["private"], new_masters["private"])
            or self._list_changed(self.masters["public"], new_masters["public"])):
            self.logger.warning("Masters list changed.")
            self.initial_config()
        else:
            # Check if slaves list was changed
            new_slaves = self.dnso.get_slaves(self.masters)
            if len(new_slaves["private"]) > 0:
                old_slaves = self.dhcl.get_nameserver()
                if self._list_changed(old_slaves, new_slaves["private"]):
                    self.logger.warning("Slaves list changed.")
                    self.slaves = dict(new_slaves)
                    self._setup_resolver(self.slaves["private"], [self.zone])
            else:
                self.logger.error("No private DNS slaves found: {}.".format(new_slaves))

    def cleanup(self):
        """To do on shutdown"""
        # Delete aliases if any
        if self.aliases:
            for alias in self.aliases:
                self.dnso.delete_alias(self.masters["private"][0], alias, self.fqdn)
                self.dnso.delete_alias(self.masters["public"][0], alias, self.fqdn)

        # Delete hosts' records
        self.dnso.delete_host(
            self.masters["private"][0], self.fqdn, self.private_ip, ptr=True)
        self.dnso.delete_host(
            self.masters["public"][0], self.fqdn, self.public_ip)

    def _update_records(self, masters, ip, ptr):
        """Try update on any master"""
        for master in masters:
            self.logger.debug("Trying update at master: {}.".format(master))
            try:
                self.dnso.update_host(master, self.fqdn, ip, ptr=ptr)

                # Add aliases if any
                if self.aliases:
                    for alias in self.aliases:
                        self.dnso.update_alias(master, alias, self.fqdn)
                return True
            except:
                continue
        return False

    def _setup_resolver(self, servers, domain):
        self.logger.info(
            "Configuring local resolver with: NS={}; domain={}.".format(
                servers, domain))
        self.dhcl.set_nameserver(servers)
        self.dhcl.set_search(domain)
        if self.dhcl.config_updated:
            self.dhcl.renew_lease()

    def _list_changed(self, first, second):
        self.logger.debug("Comparing lists: {} vs {}.".format(first, second))
        if len(first) != len(second):
            return True
        else:
            for i, element in enumerate(first):
                if element != second[i]:
                    return True
            return False
Пример #4
0
class Route53:

    def __init__(self, config, sync=True):
        self.logger = logging.getLogger("DNSWatch.Route53")
        self.misc = Misc(self.logger)
        self.config = config
        self.sync = sync
        self.unchecked_requests = list()
        self.client = boto3.client(
                        "route53",
                        aws_access_key_id=config["update_key"]["name"],
                        aws_secret_access_key=config["update_key"]["key"])

    def update_host(self, host, ip, ptr=False):
        result = True
        return result

    def get_zones(self):
        self.logger.debug("Getting hosted DNS zones.")
        zones = dict()
        response = self.client.list_hosted_zones()

        if not response["IsTruncated"]:
            zones_info = response["HostedZones"]
        else:
            self.misc.die(("Truncated aswers are not supported yet"))

        for zone in zones_info:
            zone_id = self._extract_id(zone["Id"])
            zones[zone_id] = {
                "Name": zone["Name"],
                "Private": zone["Config"]["PrivateZone"]
            }
        return zones

    def add_host(self, zone_id, hostname, ip, ptr=False):
        self._operate_record("create", zone_id, hostname, "A", ip)

    def delete_host(self, zone_id, hostname, ip, ptr=False):
        self._operate_record("delete", zone_id, hostname, "A", ip)

    def update_host(self, zone_id, hostname, ip, ptr=False):
        self._operate_record("upsert", zone_id, hostname, "A", ip)

    def add_ptr(self, zone_id, ptr_name, hostname):
        self._operate_record("create", zone_id, ptr_name, "PTR", self._ensure_fqdn(hostname))

    def delete_ptr(self, zone_id, ptr_name, hostname):
        self._operate_record("delete", zone_id, ptr_name, "PTR", self._ensure_fqdn(hostname))

    def update_ptr(self, zone_id, ptr_name, hostname):
        self._operate_record("upsert", zone_id, ptr_name, "PTR", self._ensure_fqdn(hostname))

    def add_alias(self, zone_id, cname, hostname):
        self._operate_record("create", zone_id, cname, "CNAME", self._ensure_fqdn(hostname))

    def delete_alias(self, zone_id, cname, hostname):
        self._operate_record("delete", zone_id, cname, "CNAME", self._ensure_fqdn(hostname))

    def update_alias(self, zone_id, cname, hostname):
        self._operate_record("upsert", zone_id, cname, "CNAME", self._ensure_fqdn(hostname))

    def _operate_record(self, action, zone_id, rdname, rdtype, data):
        action = action.upper()
        if not action in ["CREATE", "DELETE", "UPSERT"]:
            self.misc.die("{} with DNS record isn't supported".format(action))

        self.logger.debug("Requesting {} of '{}':'{}' record at {} with data '{}'.".format(
            action, rdtype, rdname, zone_id, data))

        response = self.client.change_resource_record_sets(
            HostedZoneId=zone_id,
            ChangeBatch={
                "Comment": "made by dnswatch",
                "Changes": [
                    {
                        "Action": action,
                        "ResourceRecordSet": {
                            "Name": rdname,
                            "Type": rdtype,
                            "TTL": self.config["ttl"],
                            "ResourceRecords": [
                                { "Value": data },
                            ]
                        },
                    },
                ],
            }
        )

        request_id = self._extract_id(response["ChangeInfo"]["Id"])
        self.logger.debug("Request sent: %s." % request_id)

        if self.sync:
            self._wait_request(request_id)
        else:
            self.unchecked_requests.append(request_id)

    def check_request_status(self, request_id=None):
        if request_id:
            self._wait_request(request_id)
            self.unchecked_requests(request_id)
        else:
            for request_id in self.unchecked_requests:
                self._wait_request(request_id)
                self.unchecked_requests.remove(request_id)

    def _ensure_fqdn(self, name):
        """Make a proper FQDN from name"""
        if name[-1:] != ".":
            return "%s." % name
        else:
            return name

    def _extract_id(self, dirty_id):
        """Delete /prefix from Id returned by Amazon API"""
        if dirty_id[:1] == "/":
            return dirty_id.split("/")[-1]
        else:
            return dirty_id

    def _wait_request(self, request_id):
            self.logger.debug("Checking request: %s." % request_id)
            waiter = self.client.get_waiter('resource_record_sets_changed')
            try:
                waiter.wait(Id=request_id)
                self.logger.debug("Request completed: %s." % request_id)
                return True
            except:
                self.logger.error("Request failed: %s." % request_id)
                return False
Пример #5
0
class DNSOps:

    def __init__(self, config):
        self.logger = logging.getLogger("DNSWatch.DNSOps")
        self.misc = Misc(self.logger)
        self.config = config
        self.keyring = None
        self.key_algorithm = None

    def setup_key(self):
        update_key = self.config["update_key"]
        name = update_key["name"]
        key = update_key["key"]
        algorithm = update_key["algorithm"]

        self.logger.debug(
            "Creating keyring for domain '{}' with key '{}...'.".format(
                name, key[:10]))
        self.keyring = dns.tsigkeyring.from_text({name: key}) 

        self.logger.debug("Setting key algorithm to '{}'.".format(algorithm))
        self.key_algorithm = getattr(dns.tsig, algorithm)

    def get_masters(self):
        zone = self.config["zone"]
        self.logger.debug("Getting DNS masters for zone {}.".format(zone))

        masters = dict()
        for mtype in ["private", "public"]:
            try:
                record = "dns-master-{}.{}".format(mtype, zone)
                self.logger.debug("Looking for TXT record {}.".format(record))
                answer = self._query(record, "TXT")
            except dns.resolver.NXDOMAIN:
                upper_zone = zone.split(".", 1)[1]
                record = "dns-master-{}.{}".format(mtype, upper_zone)
                self.logger.debug(
                    "Failed. Checking upper zone {}.".format(upper_zone))
                answer = self._query(record, "TXT")

            self.logger.debug("Got {} masters: {}.".format(mtype, answer))
            masters[mtype] = answer

        self.logger.debug("Masters: {}.".format(masters))
        return masters

    def get_slaves(self, masters):
        zone = self.config["zone"]
        self.logger.debug("Getting DNS slaves for zone {}.".format(zone))

        slaves = dict()
        for stype in masters.keys():
            record = "dns-slave.{}".format(zone)
            self.logger.debug("Looking for TXT record {} at {}.".format(
                record, masters[stype]))
            answer = self._query(
                "dns-slave.{}".format(zone), "TXT", masters[stype])
            slaves[stype] = answer

        self.logger.debug("Slaves: {}.".format(slaves))
        return slaves

    def add_host(self, dnsserver, host, ip, ptr=False):
        self._operate_record("add", dnsserver, host, "A", ip)
        if ptr:
            self.add_ptr(dnsserver, host, ip)

    def delete_host(self, dnsserver, host, ip, ptr=False):
        self._operate_record("delete", dnsserver, host, "A", ip)
        if ptr:
            self.delete_ptr(dnsserver, host, ip)

    def update_host(self, dnsserver, host, ip, ptr=False):
        self._operate_record("replace", dnsserver, host, "A", ip)
        if ptr:
            self.update_ptr(dnsserver, host, ip)

    def add_ptr(self, dnsserver, host, ip):
        ptr_record = dns.reversename.from_address(ip)
        self._operate_record("add", dnsserver, ptr_record, "PTR", self._ensure_fqdn(host))

    def delete_ptr(self, dnsserver, host, ip):
        ptr_record = dns.reversename.from_address(ip)
        self._operate_record("delete", dnsserver, ptr_record, "PTR", self._ensure_fqdn(host))

    def update_ptr(self, dnsserver, host, ip):
        ptr_record = dns.reversename.from_address(ip)
        self._operate_record("replace", dnsserver, ptr_record, "PTR", self._ensure_fqdn(host))

    def add_alias(self, dnsserver, cname, hostname):
        self._operate_record("add", dnsserver, cname, "CNAME", self._ensure_fqdn(hostname))

    def delete_alias(self, dnsserver, cname, hostname):
        self._operate_record("delete", dnsserver, cname, "CNAME", self._ensure_fqdn(hostname))

    def update_alias(self, dnsserver, cname, hostname):
        self._operate_record("replace", dnsserver, cname, "CNAME", self._ensure_fqdn(hostname))

    def _operate_record(self, action, dnsserver, rdname, rdtype, data):
        if not action in ["add", "delete", "replace"]:
            self.misc.die("{} with DNS record isn't supported".format(action))
        if not self.keyring:
            self.misc.die("Keyring for DNS action not found")
        if not self.key_algorithm:
            self.misc.die("Key algorithm for DNS action not specified")
        self.logger.debug("Doing {} of '{}':'{}' record at {} with data '{}'.".format(
            action, rdtype, rdname, dnsserver, data))

        # Adjusting variables
        if rdtype == "PTR":
            rdname, origin = str(rdname).split(".", 1)
        else:
            origin = dns.name.from_text(self.config["zone"])
            rdname = dns.name.from_text(rdname) - origin
        data = data.encode("utf-8")

        # Collecting arguments for DNS update
        args = list()
        if action in ["add", "replace"]:
            args.append(self.config["ttl"])
        args.append(rdtype)        
        if action in ["add", "replace"]:
            args.append(data)

        # Doing DNS update
        update = dns.update.Update(
            origin,
            keyring=self.keyring, 
            keyalgorithm=self.key_algorithm)  
        eval('update.{}(rdname, *args)'.format(action))

        result = dns.query.tcp(update, dnsserver, timeout=self.config["timeout"])

        rcode = self._compile_rcode(result)
        if rcode[0] != 0:
            self.misc.die("DNS update failed: rcode={}; message='{}'".format(rcode[0], rcode[1]))
        else:
            self.logger.debug("DNS update done: rcode={}; message='{}'.".format(rcode[0], rcode[1]))

    def _compile_rcode(self, message):
        text = str()
        code = message.rcode()
        for line in message.to_text().splitlines():
            match = re.match("^rcode\s(.+)$", line)
            if match:
                text = match.group(1)
        return [ code, text ]

    def _query(self, name, rtype="A", nameservers=None):
        result = list()
        resolver = dns.resolver.Resolver()
        if nameservers:
            resolver.nameservers = nameservers

        answers = list()
        try:
            answers = resolver.query(name, rtype)
        except dns.exception.Timeout:
            self.logger.error(
                "Timeout reached while getting {} record {} from {}.".format(
                    rtype, name, nameservers))

        for answer in answers:
            if rtype == "TXT":
                for line in answer.strings:
                    line = line.replace('"', "")
                    result.extend(line.split(","))
            else:
                result.append(answer.address)
        return result

    def _ensure_fqdn(self, name):
        """Make a proper FQDN from name"""
        if name[-1:] != ".":
            return "%s." % name
        else:
            return name