ascension.py (30698B)
1 #!/usr/bin/env python3 2 # This file is part of Ascension. 3 # Copyright (C) 2019 GNUnet e.V. 4 # 5 # Ascension is free software: you can redistribute it and/or modify it 6 # under the terms of the GNU Affero General Public License as published 7 # by the Free Software Foundation, either version 3 of the License, 8 # or (at your option) any later version. 9 # 10 # Ascension is distributed in the hope that it will be useful, but 11 # WITHOUT ANY WARRANTY; without even the implied warranty of 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 # Affero General Public License for more details. 14 # 15 # You should have received a copy of the GNU Affero General Public License 16 # along with this program. If not, see <http://www.gnu.org/licenses/>. 17 # 18 # SPDX-License-Identifier: AGPL3.0-or-later 19 # 20 # Author rexxnor 21 """ 22 Usage: 23 ascension <domain> [-d] [-p] [-s] [--minimum-ttl=<ttl>] [--dry-run] 24 ascension <domain> <port> [-d] [-p] [-s] [--minimum-ttl=<ttl>] 25 ascension <domain> -n <transferns> [-d] [-p] [-s] [--minimum-ttl=<ttl>] [--dry-run] 26 ascension <domain> -n <transferns> <port> [-d] [-p] [-s] [--minimum-ttl=<ttl>] [--dry-run] 27 ascension -p | --public 28 ascension -s | --debug 29 ascension -s | --standalone 30 ascension -h | --help 31 ascension -v | --version 32 33 Options: 34 <domain> Domain to migrate 35 <port> Port for zone transfer 36 <transferns> DNS Server that does the zone transfer 37 --minimum-ttl=<ttl> Minimum TTL for records to migrate [default: 3600] 38 --dry-run Only try if a zone transfer is allowed 39 -p --public Make records public on the DHT 40 -s --standalone Run ascension once 41 -d --debug Enable debugging 42 -h --help Show this screen. 43 -v --version Show version. 44 """ 45 46 # imports 47 import logging 48 import queue 49 import re 50 import socket 51 import sys 52 import time 53 import subprocess as sp 54 import threading 55 import dns.query 56 import dns.resolver 57 import dns.zone 58 import docopt 59 60 # GLOBALS 61 GNUNET_ZONE_CREATION_COMMAND = 'gnunet-identity' 62 GNUNET_NAMESTORE_COMMAND = 'gnunet-namestore' 63 GNUNET_GNS_COMMAND = 'gnunet-gns' 64 GNUNET_ARM_COMMAND = 'gnunet-arm' 65 # This is the list of record types Ascension (and GNS) currently 66 # explicitly supports. Record types we encounter that are not 67 # in this list and not in the OBSOLETE_RECORD_TYPES list will 68 # create a warning (information loss during migration). 69 SUPPORTED_RECORD_TYPES = [ 70 "A", "AAAA", "NS", "MX", "SRV", "TXT", "CNAME", 71 ] 72 # Record types that exist in DNS but that won't ever exist in GNS 73 # as they are not needed anymore (so we should not create a warning 74 # if we drop one of these). 75 OBSOLETE_RECORD_TYPES = [ 76 "PTR", 77 "SIG", "KEY", 78 "RRSIG", "NSEC", "DNSKEY", "NSEC3", "NSEC3PARAM", "CDNSKEY", 79 "TKEY", "TSIG", 80 "TA", "DLV", 81 ] 82 83 class Ascender(): 84 """ 85 Class that provides migration for any given domain 86 """ 87 def __init__(self, 88 domain: str, 89 transferns: str, 90 port: str, 91 flags: str, 92 minimum: str) -> None: 93 self.domain = domain 94 if domain[-1] == '.': 95 self.domain = self.domain[:-1] 96 self.port = int(port) 97 self.transferns = transferns 98 self.soa = None 99 self.tld = self.domain.split(".")[::-1][0] 100 self.zone = None 101 self.zonegenerator = None 102 self.flags = flags 103 self.minimum = int(minimum) 104 self.subzonedict = dict() 105 106 def bootstrap_zone(self) -> None: 107 """ 108 Creates the zone in gnunet 109 """ 110 try: 111 ret = sp.run([GNUNET_ZONE_CREATION_COMMAND, 112 '-C', self.domain]) 113 logging.info("executed command: %s", " ".join(ret.args)) 114 except sp.CalledProcessError: 115 logging.info("Zone %s already exists!", self.domain) 116 117 def get_dns_zone_serial(self, 118 domain: str, 119 resolver=None) -> int: 120 """ 121 Gets the current serial for a given zone 122 :param domain: Domain to query for in DNS 123 :param resolver: Nameserver to query in DNS, defaults to None 124 :returns: Serial of the zones SOA record 125 """ 126 # Makes domains better resolvable 127 domain = domain + "." 128 # SOA is different if taken directly from SOA record 129 # compared to AXFR/IXFR - changed to respect this 130 try: 131 soa_answer = dns.resolver.query(domain, 'SOA') 132 master_answer = dns.resolver.query(soa_answer[0].mname, 'A') 133 except dns.resolver.NoAnswer: 134 logging.warning("The domain '%s' is not publicly resolvable.", 135 domain) 136 except dns.resolver.NXDOMAIN: 137 logging.warning("The domain '%s' is not publicly resolvable.", 138 domain) 139 except Exception: 140 logging.warning("The domain '%s' is not publicly resolvable.", 141 domain) 142 143 try: 144 if resolver: 145 zone = dns.zone.from_xfr(dns.query.xfr( 146 resolver, domain, port=self.port)) 147 else: 148 zone = dns.zone.from_xfr(dns.query.xfr( 149 master_answer[0].address, domain, 150 port=self.port)) 151 except dns.resolver.NoAnswer: 152 logging.critical("Nameserver for '%s' did not answer.", domain) 153 return None 154 except dns.exception.FormError: 155 logging.critical("Domain '%s' does not allow xfr requests.", 156 domain) 157 return None 158 except Exception: 159 logging.error("Unexpected error while transfering domain '%s'", 160 domain) 161 return None 162 163 for soa_record in zone.iterate_rdatas(rdtype=dns.rdatatype.SOA): 164 if not self.transferns: 165 mname = soa_record[2].mname 166 if self.domain not in mname: 167 self.transferns = str(soa_record[2].mname) + "." + domain 168 else: 169 self.transferns = str(soa_record[2].mname) 170 return int(soa_record[2].serial) 171 172 def add_records_to_gns(self) -> None: 173 """ 174 Extracts records from zone and adds them to GNS 175 :raises AttributeError: When getting incomplete data 176 """ 177 logging.info("Starting to add records into GNS...") 178 179 # Defining FIFO Queue 180 taskqueue = queue.Queue(maxsize=5) 181 182 # Defining worker 183 def worker(): 184 while True: 185 # define recordline 186 recordline = list() 187 label = "" 188 domain = None 189 190 labelrecords = taskqueue.get() 191 # break if taskqueue is empty 192 if labelrecords is None: 193 break 194 195 # execute thing to run on item 196 label, listofrdatasets = labelrecords 197 subzones = label.split('.') 198 domain = self.domain 199 200 if len(subzones) > 1: 201 label = subzones[0] 202 subdomains = ".".join(subzones[1:]) 203 subzone = "%s.%s" % (subdomains, domain) 204 fqdn = "%s.%s.%s" % (label, subdomains, domain) 205 if fqdn in self.subzonedict.keys(): 206 label = "@" 207 domain = fqdn 208 elif subzone in self.subzonedict.keys(): 209 domain = subzone 210 211 for rdataset in listofrdatasets: 212 for record in rdataset: 213 rdtype = dns.rdatatype.to_text(record.rdtype) 214 if rdtype not in SUPPORTED_RECORD_TYPES: 215 continue 216 217 try: 218 if rdataset.ttl <= self.minimum: 219 ttl = self.minimum 220 else: 221 ttl = rdataset.ttl 222 except AttributeError: 223 ttl = self.minimum 224 225 value = str(record) 226 227 # ignore NS for itself here 228 if label == '@' and rdtype == 'NS': 229 logging.info("ignoring NS record for itself") 230 231 # modify value to fit gns syntax 232 rdtype, value, label = \ 233 self.transform_to_gns_format(record, 234 rdtype, 235 domain, 236 label) 237 # skip record if value is none 238 if value is None: 239 continue 240 241 if isinstance(value, list): 242 for element in value: 243 # build recordline 244 recordline.append("-R") 245 recordline.append('%d %s %s %s' % 246 (int(ttl), 247 rdtype, 248 self.flags, 249 element)) 250 else: 251 # build recordline 252 recordline.append("-R") 253 254 # TODO possible regression here; maybe use a separate 255 # list to pass those arguments to prevent quoting 256 # issues in the future. 257 recordline.append('%d %s %s %s' % 258 (int(ttl), 259 rdtype, 260 self.flags, 261 value)) 262 263 # add recordline to gns and filter out empty lines 264 if len(recordline) > 1: 265 self.add_recordline_to_gns(recordline, 266 domain, 267 label) 268 269 taskqueue.task_done() 270 # End of worker 271 272 273 # Check if a delegated zone is available in GNS as per NS record 274 nsrecords = self.zone.iterate_rdatasets(dns.rdatatype.NS) 275 276 # This is broken if your NS is for ns.foo.YOURZONE as you add 277 # the PKEY to YOURZONE instead of to the foo.YOURZONE subzone. 278 # alice NS IN ns.alice 279 # bob NS IN ns.bob 280 # carol NS IN ns.alice 281 # => carol GNS2DNS GNS ns.alice@$IP 282 # dave.foo NS IN gns--pkey--$KEY.bob 283 # => dave.foo PKEY GNS $KEY 284 # foo.bar A IN 1.2.3.4 285 # => bar PKEY GNS $NEWKEY + mapping: bar => $NEWKEY 286 # => foo[.bar] A GNS 1.2.3.4 287 #gnspkey = list(filter(lambda record: for rec in record[2]: if str(rec).startswith('gns--pkey--'): return true; return false, nsrecords)) 288 illegalchars = ["I", "L", "O", "U", "i", "l", "o", "u"] 289 for nsrecord in nsrecords: 290 name = str(nsrecord[0]) 291 values = nsrecord[1] 292 ttl = values.ttl 293 294 gnspkeys = list(filter(lambda record: 295 str(record).startswith('gns--pkey--'), 296 values)) 297 298 num_gnspkeys = len(gnspkeys) 299 if not num_gnspkeys: 300 # skip empty values 301 continue 302 if num_gnspkeys > 1: 303 logging.critical("Detected ambiguous PKEY records for label \ 304 %s (not generating PKEY record)", name) 305 continue 306 307 gnspkey = str(gnspkeys[0]) 308 # FIXME: drop all NS records under this name later! 309 # => new map, if entry present during NS processing, skip! 310 if not any(illegal in gnspkey for illegal in illegalchars): 311 self.add_pkey_record_to_zone(gnspkey[11:], 312 self.domain, 313 name, 314 ttl) 315 316 # Unify all records under same label into a record set 317 customrdataset = dict() 318 for name, rdset in self.zone.iterate_rdatasets(): 319 # build lookup table for later GNS2DNS records 320 name = str(name) # Name could be str or DNS.name.Name 321 if customrdataset.get(name) is None: 322 work = list() 323 work.append(rdset) 324 customrdataset[name] = work 325 else: 326 customrdataset[name].append(rdset) 327 328 for label, value in customrdataset.items(): 329 if value is None: 330 continue 331 332 subzones = label.split('.') 333 label = subzones[0] 334 subdomain = ".".join(subzones[1:]) 335 zonename = "%s.%s" % (subdomain, self.domain) 336 337 try: 338 if value.ttl <= self.minimum: 339 ttl = self.minimum 340 else: 341 ttl = value.ttl 342 except AttributeError: 343 ttl = self.minimum 344 345 if len(subzones) > 1: 346 if self.subzonedict.get(zonename): 347 continue 348 else: 349 self.subzonedict[zonename] = (False, ttl) 350 351 self.create_zone_hierarchy() 352 353 # Create one thread 354 thread = threading.Thread(target=worker) 355 thread.start() 356 357 # add records 358 for label, value in customrdataset.items(): 359 if value is None: 360 continue 361 taskqueue.put((label, value)) 362 363 # Block until all tasks are done 364 taskqueue.join() 365 366 # Stop workers and threads 367 taskqueue.put(None) 368 thread.join(timeout=10) 369 if thread.is_alive(): 370 logging.critical("thread join timed out, still running") 371 372 # Add soa record to GNS once completed (updates the previous one) 373 self.add_soa_record_to_gns(self.soa) 374 375 logging.info("All records have been added!") 376 377 @staticmethod 378 def add_recordline_to_gns(recordline: list, 379 zonename: str, 380 label: str) -> None: 381 """ 382 Replaces records in zone or adds them if not 383 :param recordline: records to replace as list in form 384 ['-R', 'TTL TYPE FLAGS VALUE'] 385 :param zonename: zonename of zone to add records to 386 :param label: label under which to add the records 387 """ 388 logging.info("trying to add %d records with name %s", 389 len(recordline)/2, label) 390 391 ret = sp.run([GNUNET_NAMESTORE_COMMAND, 392 '-z', zonename, 393 '-n', str(label), 394 ] + recordline) 395 396 if ret.returncode != 0: 397 logging.warning("failed adding record with name %s", 398 ' '.join(ret.args)) 399 else: 400 logging.info("successfully added record with command %s", 401 ' '.join(ret.args)) 402 403 def resolve_glue(self, 404 authorityname: str) -> list: 405 """ 406 Resolves IP Adresses within zone 407 :param authorityname: 408 """ 409 try: 410 rdsets = self.zone[authorityname].rdatasets 411 except KeyError: 412 return [] 413 value = [] 414 for rdataset in rdsets: 415 if rdataset.rdtype in [dns.rdatatype.A, dns.rdatatype.AAAA]: 416 for rdata in rdataset: 417 value.append("%s.%s@%s" % (authorityname, 418 self.domain, 419 str(rdata))) 420 return value 421 422 def transform_to_gns_format(self, 423 record: dns.rdata.Rdata, 424 rdtype: dns.rdata.Rdata, 425 zonename: str, 426 label: str) -> tuple: 427 """ 428 Transforms value of record to GNS compatible format 429 :param record: record to transform 430 :param rdtype: record value to transform 431 :param zonename: name of the zone to add to 432 :param label: label under which the record is stored 433 :returns: a tuple consisting of the new rdtype, the label and value 434 """ 435 value = str(record) 436 if label is None: 437 label = '@' 438 if rdtype == 'SOA': 439 zonetuple = str(value).split(' ') 440 authns, owner, serial, refresh, retry, expiry, irefresh = zonetuple 441 if authns[-1] == '.': 442 authns = authns[:-1] 443 if owner[-1] == '.': 444 owner = owner[:-1] 445 # hacky and might cause bugs 446 authns += self.tld 447 owner += self.tld 448 value = "rname=%s.%s mname=%s.%s %d,%d,%d,%d,%d" % ( 449 authns, zonename, owner, zonename, 450 int(serial), int(refresh), int(retry), 451 int(expiry), int(irefresh) 452 ) 453 elif rdtype in ['CNAME']: 454 if value[-1] == ".": 455 value = value[:-1] 456 else: 457 value = "%s.%s" % (value, self.domain) 458 elif rdtype == 'NS': 459 nameserver = str(record.target) 460 if nameserver[-1] == ".": 461 nameserver = nameserver[:-1] 462 if value[-1] == ".": 463 # FQDN provided 464 if value.endswith(".%s." % zonename): 465 # in bailiwick 466 value = self.resolve_glue(record.target) 467 else: 468 # out of bailiwick 469 if label.startswith("@"): 470 value = '%s@%s' % (zonename, nameserver) 471 else: 472 value = '%s.%s@%s' % (str(label), 473 zonename, 474 nameserver) 475 else: 476 # Name is relative to zone, must be in bailiwick 477 value = self.resolve_glue(record.target) 478 if not value: 479 if label.startswith("@"): 480 value = '%s@%s.%s' % (self.domain, 481 record.target, 482 self.domain) 483 else: 484 value = '%s.%s@%s.%s' % (str(label), self.domain, 485 record.target, self.domain) 486 487 logging.info("transformed %s record to GNS2DNS format", rdtype) 488 rdtype = 'GNS2DNS' 489 elif rdtype == 'MX': 490 priority, mailserver = str(value).split(' ') 491 if mailserver[-1] == ".": 492 mailserver = mailserver[:-1] 493 mailserver = '%s.%s' % (mailserver, zonename) 494 value = '%s,%s' % (priority, mailserver) 495 logging.info("transformed %s record to GNS format", rdtype) 496 elif rdtype == 'SRV': 497 # this is the number for a SRV record 498 rdtype = 'BOX' 499 srv = 33 500 501 # tearing the record apart 502 try: 503 srvrecord = str(label).split('.') 504 proto = srvrecord[1] 505 except IndexError: 506 logging.warning("could not parse SRV label %s", label) 507 return (rdtype, None, None) 508 priority, weight, destport, target = value.split(' ') 509 510 try: 511 protostring = proto.strip('_') 512 protonum = socket.getprotobyname(protostring) 513 except OSError: 514 logging.warning("invalid protocol: %s", protostring) 515 return (rdtype, None, None) 516 517 if target[:-1] == ".": 518 value = '%s %s %s %s %s %s %s' % ( 519 destport, protonum, srv, priority, weight, destport, 520 "%s" % target 521 ) 522 else: 523 value = '%s %s %s %s %s %s %s' % ( 524 destport, protonum, srv, priority, weight, destport, 525 "%s.%s" % (target, zonename) 526 ) 527 528 label = target 529 else: 530 logging.info("Did not transform record of type: %s", rdtype) 531 return (rdtype, value, label) 532 533 def get_gns_zone_serial(self) -> int: 534 """ 535 Fetches the zones serial from GNS 536 :returns: serial of the SOA record in GNS 537 """ 538 try: 539 serial = sp.check_output([GNUNET_GNS_COMMAND, 540 '-t', 'SOA', 541 '-u', '%s' % self.domain,]) 542 serial = serial.decode() 543 except sp.CalledProcessError: 544 serial = "" 545 soa_serial = 0 546 soapattern = re.compile(r'.+\s(\d+),\d+,\d+,\d+,\d+', re.M) 547 if re.findall(soapattern, serial): 548 soa_serial = re.findall(soapattern, serial)[0] 549 else: 550 soa_serial = 0 551 return int(soa_serial) 552 553 @staticmethod 554 def get_zone_soa(zone) -> dns.rdatatype.SOA: 555 """ 556 Fetches soa record from zone a given zone 557 :param zone: A dnspython zone 558 :returns: SOA record of given zone 559 """ 560 soa = None 561 for soarecord in zone.iterate_rdatas(rdtype=dns.rdatatype.SOA): 562 if str(soarecord[0]) == '@': 563 soa = soarecord 564 return soa 565 566 def add_soa_record_to_gns(self, record) -> None: 567 """ 568 Adds a SOA record to GNS 569 :param record: The record to add 570 """ 571 label, ttl, rdata = record 572 zonetuple = str(rdata).split(' ') 573 authns, owner, serial, refresh, retry, expiry, irefresh = zonetuple 574 if authns[-1] == '.': 575 authns = authns[:-1] 576 else: 577 authns = "%s.%s" % (authns, self.domain) 578 if owner[-1] == '.': 579 owner = owner[:-1] 580 else: 581 owner = "%s.%s" % (owner, self.domain) 582 583 value = "rname=%s mname=%s %s,%s,%s,%s,%s" % (authns, 584 owner, 585 serial, 586 refresh, 587 retry, 588 expiry, 589 irefresh) 590 recordval = '%s %s %s %s' % (ttl, "SOA", self.flags, str(value)) 591 recordline = ['-R', recordval] 592 self.add_recordline_to_gns(recordline, self.domain, str(label)) 593 594 @staticmethod 595 def create_zone_and_get_pkey(zonestring: str) -> str: 596 """ 597 Creates the zone in zonestring and returns pkey 598 :param zonestring: The label name of the zone 599 :returns: gnunet pkey of the zone 600 """ 601 try: 602 ret = sp.run([GNUNET_ZONE_CREATION_COMMAND, 603 '-C', zonestring, 604 '-V'], 605 stdout=sp.PIPE, 606 stderr=sp.DEVNULL, 607 check=True) 608 logging.info("executed command: %s", " ".join(ret.args)) 609 pkey_zone = ret.stdout.decode().strip() 610 except sp.CalledProcessError: 611 ret = sp.run([GNUNET_ZONE_CREATION_COMMAND, 612 '-dq', 613 '-e', zonestring], 614 stdout=sp.PIPE) 615 logging.info("executed command: %s", " ".join(ret.args)) 616 pkey_zone = ret.stdout.decode().strip() 617 return pkey_zone 618 619 @staticmethod 620 def add_pkey_record_to_zone(pkey: str, 621 domain: str, 622 label: str, 623 ttl: str) -> None: 624 """ 625 Adds the pkey of the subzone to the parent zone 626 :param pkey: the public key of the child zone 627 :param domain: the name of the parent zone 628 :param label: the label under which to add the pkey 629 :param ttl: the time to live the record should have 630 """ 631 #lookup = sp.run(GNUNET_NAMESTORE_COMMAND, 632 # '-z', domain, 633 # '-n', label, 634 # '-t', 'PKEY') 635 #if not 'Got result:' in lookup.stdout: 636 debug = " ".join([GNUNET_NAMESTORE_COMMAND, 637 '-z', domain, 638 '-a', '-n', label, 639 '-t', 'PKEY', 640 '-V', pkey, 641 '-e', "%ss" % ttl]) 642 ret = sp.run([GNUNET_NAMESTORE_COMMAND, 643 '-z', domain, 644 '-a', '-n', label, 645 '-t', 'PKEY', 646 '-V', pkey, 647 '-e', "%ss" % ttl]) 648 logging.info("executed command: %s", debug) 649 if ret.returncode != 0: 650 logging.warning("failed to add PKEY record %s to %s", 651 label, domain) 652 #logging.warning("PKEY record %s already exists in %s", label, domain) 653 654 def create_zone_hierarchy(self) -> None: 655 """ 656 Creates the zone hierarchy in GNS for label 657 """ 658 domain = self.domain 659 660 # Build Dictionary from GNS identities 661 ids = sp.run([GNUNET_ZONE_CREATION_COMMAND, '-d'], stdout=sp.PIPE) 662 domainlist = ''.join(col for col in ids.stdout.decode()).split('\n') 663 altdomainlist = [e for e in domainlist if domain + " " in e] 664 for zone in altdomainlist: 665 zonename, _, pkey = zone.split(" ") 666 self.subzonedict[zonename] = (pkey, self.minimum) 667 668 zonelist = self.subzonedict.items() 669 sortedlist = sorted(zonelist, key=lambda s: len(str(s).split('.'))) 670 for zone, pkeyttltuple in sortedlist: 671 pkey, ttl = pkeyttltuple 672 if not pkey: 673 domain = ".".join(zone.split('.')[1::]) 674 label = zone.split('.')[0] 675 pkey = self.create_zone_and_get_pkey(zone) 676 logging.info("adding zone %s with %s pkey into %s", zone, pkey, domain) 677 self.add_pkey_record_to_zone(pkey, domain, label, pkeyttltuple[1]) 678 self.subzonedict[zone] = (pkey, ttl) 679 680 def main(): 681 """ 682 Initializes object and handles arguments 683 """ 684 # argument parsing from docstring definition 685 args = docopt.docopt(__doc__, version='Ascension 0.6.1') 686 687 # argument parsing 688 debug = args['--debug'] 689 domain = args.get('<domain>', None) 690 transferns = args['<transferns>'] if args['<transferns>'] else None 691 port = args['<port>'] if args['<port>'] else "53" 692 flags = "p" if args.get('--public') else "n" 693 standalone = bool(args.get('--standalone')) 694 dryrun = bool(args.get('--dry-run')) 695 minimum = args['--minimum-ttl'] 696 697 # Change logging severity to debug 698 if debug: 699 logging.basicConfig(level=logging.DEBUG) 700 701 # Initialize class instance 702 ascender = Ascender(domain, transferns, port, flags, minimum) 703 704 # Do dry run before GNUnet check 705 if dryrun: 706 dns_zone_serial = ascender.get_dns_zone_serial(ascender.domain, 707 ascender.transferns) 708 if dns_zone_serial is None: 709 return 1 710 else: 711 return 0 712 713 # Checks if GNUnet services are running 714 try: 715 sp.check_output([GNUNET_ARM_COMMAND, '-I'], timeout=1) 716 except sp.TimeoutExpired: 717 logging.critical('GNUnet services are not running!') 718 sys.exit(1) 719 720 # Set to defaults to use before we get a SOA for the first time 721 retry = 300 722 refresh = 300 723 724 # Main loop for actual daemon 725 while True: 726 gns_zone_serial = ascender.get_gns_zone_serial() 727 if gns_zone_serial: 728 ascender.zonegenerator = dns.query.xfr(ascender.transferns, 729 ascender.domain, 730 rdtype=dns.rdatatype.IXFR, 731 serial=gns_zone_serial, 732 port=ascender.port) 733 else: 734 ascender.zonegenerator = dns.query.xfr(ascender.transferns, 735 ascender.domain, 736 port=ascender.port) 737 dns_zone_serial = ascender.get_dns_zone_serial(ascender.domain, 738 ascender.transferns) 739 740 if not dns_zone_serial: 741 logging.error("Could not get DNS zone serial") 742 if standalone: 743 return 1 744 time.sleep(retry) 745 continue 746 if not gns_zone_serial: 747 logging.info("GNS zone does not exist yet, performing full transfer.") 748 print("GNS zone does not exist yet, performing full transfer.") 749 ascender.bootstrap_zone() 750 elif gns_zone_serial == dns_zone_serial: 751 logging.info("GNS zone is up to date.") 752 print("GNS zone is up to date.") 753 if standalone: 754 return 0 755 time.sleep(refresh) 756 continue 757 elif gns_zone_serial > dns_zone_serial: 758 logging.critical("SOA serial in GNS is bigger than SOA serial in DNS?") 759 logging.critical("GNS zone: %s, DNS zone: %s", gns_zone_serial, dns_zone_serial) 760 if standalone: 761 return 1 762 time.sleep(retry) 763 continue 764 else: 765 logging.info("GNS zone is out of date, performing incremental transfer.") 766 print("GNS zone is out of date, performing incremental transfer.") 767 768 try: 769 ascender.zone = dns.zone.from_xfr(ascender.zonegenerator, 770 check_origin=False) 771 ascender.soa = ascender.get_zone_soa(ascender.zone) 772 refresh = int(str(ascender.soa[2]).split(" ")[3]) 773 retry = int(str(ascender.soa[2]).split(" ")[4]) 774 except dns.zone.BadZone: 775 logging.critical("Malformed DNS Zone '%s'", ascender.domain) 776 if standalone: 777 return 2 778 time.sleep(retry) 779 continue 780 781 ascender.add_records_to_gns() 782 logging.info("Finished migration of the zone %s", ascender.domain) 783 784 if __name__ == '__main__': 785 main()