From 09133c8f595bea3714620b0504b95c7281862583 Mon Sep 17 00:00:00 2001 From: Brian Bustin Date: Wed, 1 Jul 2015 15:02:40 -0400 Subject: [PATCH 1/2] Initial backend changes to make it possible to have one or more secondary name servers --- management/daemon.py | 2 +- management/dns_update.py | 78 ++++++++++++++++------------ management/status_checks.py | 7 +-- management/templates/custom-dns.html | 2 +- 4 files changed, 50 insertions(+), 39 deletions(-) diff --git a/management/daemon.py b/management/daemon.py index 88dd9a42..2d14b4e6 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -229,7 +229,7 @@ def dns_get_secondary_nameserver(): def dns_set_secondary_nameserver(): from dns_update import set_secondary_dns try: - return set_secondary_dns(request.form.get('hostname'), env) + return set_secondary_dns(request.form.get('hostname').split(","), env) except ValueError as e: return (str(e), 400) diff --git a/management/dns_update.py b/management/dns_update.py index 5a3ca1dc..5321b4f7 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -144,8 +144,13 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en records.append((None, "NS", "ns1.%s." % env["PRIMARY_HOSTNAME"], False)) # Define ns2.PRIMARY_HOSTNAME or whatever the user overrides. - secondary_ns = get_secondary_dns(additional_records) or ("ns2." + env["PRIMARY_HOSTNAME"]) - records.append((None, "NS", secondary_ns+'.', False)) + # User may provide one or more additional nameservers + secondary_dns_records = get_secondary_dns(additional_records) + if len(secondary_dns_records) > 0: + for secondary_ns in secondary_dns_records: + records.append((None, "NS", secondary_ns+'.', False)) + else: + records.append((None, "NS", "ns2." + env["PRIMARY_HOSTNAME"] + '.', False)) # In PRIMARY_HOSTNAME... @@ -462,17 +467,23 @@ zone: zonefile: %s """ % (domain, zonefile) - # If a custom secondary nameserver has been set, allow zone transfers - # and notifies to that nameserver. - if get_secondary_dns(additional_records): - # Get the IP address of the nameserver by resolving it. - hostname = get_secondary_dns(additional_records) - resolver = dns.resolver.get_default_resolver() - response = dns.resolver.query(hostname+'.', "A") - ipaddr = str(response[0]) - nsdconf += """\tnotify: %s NOKEY - provide-xfr: %s NOKEY -""" % (ipaddr, ipaddr) + # If custom secondary nameservers have been set, allow zone transfers + # and notifies to them. + if get_secondary_dns(additional_records, ['_secondary_nameserver']): + for hostname in get_secondary_dns(additional_records): + # Get the IP address of the nameserver by resolving it. + resolver = dns.resolver.get_default_resolver() + response = dns.resolver.query(hostname+'.', "A") + ipaddr = str(response[0]) + nsdconf += "\n\tnotify: %s NOKEY\n\tprovide-xfr: %s NOKEY" % (ipaddr, ipaddr) + # Some providers use different servers for zone transfers and notifies + # such as DNS Made Easy. This allows us to set these IP addresses as well manually + # in custom.yaml + if get_secondary_dns(additional_records, ["_secondary_notify_xfr"]): + for ipaddr in get_secondary_dns(additional_records, ["_secondary_notify_xfr"]): + nsdconf += "\n\tnotify: %s NOKEY\n\tprovide-xfr: %s NOKEY" % (ipaddr, ipaddr) + + # Check if the file is changing. If it isn't changing, # return False to flag that no change was made. @@ -785,33 +796,36 @@ def set_custom_dns_record(qname, rtype, value, action, env): if made_change: # serialize & save write_custom_dns_config(newconfig, env) - return made_change ######################################################################## -def get_secondary_dns(custom_dns): +def get_secondary_dns(custom_dns, dns_type=['_secondary_nameserver']): + valid_types = set(['_secondary_nameserver', '_secondary_notify_xfr']) + if not valid_types.issuperset(set(dns_type)): + raise ValueError("Valid types are one or more of the following: %s" % ", ".join(valid_types)) + + values = [] for qname, rtype, value in custom_dns: - if qname == "_secondary_nameserver": - return value - return None + if qname in dns_type: + if isinstance(value, str): + values.append(value) + return values def set_secondary_dns(hostname, env): - - if hostname in (None, ""): - # Clear. - set_custom_dns_record("_secondary_nameserver", "A", None, "set", env) - else: - # Validate. - hostname = hostname.strip().lower() + hostnames = [item.strip().lower() for item in hostname] + if len(hostnames) > 0: resolver = dns.resolver.get_default_resolver() - try: - response = dns.resolver.query(hostname, "A") - except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): - raise ValueError("Could not resolve the IP address of %s." % hostname) - - # Set. - set_custom_dns_record("_secondary_nameserver", "A", hostname, "set", env) + for item in hostnames: + try: + response = dns.resolver.query(item, "A") + except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): + raise ValueError("Could not resolve the IP address of %s." % item) + # Set. + set_custom_dns_record("_secondary_nameserver", "A", {"A":hostnames}, "set", env) + else: + # Clear. + set_custom_dns_record("_secondary_nameserver", "A", None, "set", env) # Apply. return do_dns_update(env) diff --git a/management/status_checks.py b/management/status_checks.py index f7020c6f..b804378a 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -370,12 +370,9 @@ def check_dns_zone(domain, env, output, dns_zonefiles): # the TLD, and so we're not actually checking the TLD. For that we'd need # to do a DNS trace. ip = query_dns(domain, "A") - secondary_ns = get_secondary_dns(get_custom_dns_config(env)) or "ns2." + env['PRIMARY_HOSTNAME'] + secondary_ns = get_secondary_dns(get_custom_dns_config(env)) or ["ns2." + env['PRIMARY_HOSTNAME']] existing_ns = query_dns(domain, "NS") - correct_ns = "; ".join(sorted([ - "ns1." + env['PRIMARY_HOSTNAME'], - secondary_ns, - ])) + correct_ns = "; ".join(sorted(["ns1." + env['PRIMARY_HOSTNAME']] + secondary_ns)) if existing_ns.lower() == correct_ns.lower(): output.print_ok("Nameservers are set correctly at registrar. [%s]" % correct_ns) elif ip == env['PUBLIC_IP']: diff --git a/management/templates/custom-dns.html b/management/templates/custom-dns.html index 711bc384..ddc1153f 100644 --- a/management/templates/custom-dns.html +++ b/management/templates/custom-dns.html @@ -67,7 +67,7 @@

Using a Secondary Nameserver

-

If your TLD requires you to have two separate nameservers, you can either set up a secondary (aka “slave”) nameserver or, alternatively, set up external DNS and ignore the DNS server on this box. If you choose to use a seconday/slave nameserver, you must find a seconday/slave nameserver service provider. Your domain name registrar or virtual cloud provider may provide this service for you. Once you set up the seconday/slave nameserver service, enter the hostname of their secondary nameserver:

+

If your TLD requires you to have two separate nameservers, you can either set up a secondary (aka “slave”) nameserver or, alternatively, set up external DNS and ignore the DNS server on this box. If you choose to use a seconday/slave nameserver, you must find a seconday/slave nameserver service provider. Your domain name registrar or virtual cloud provider may provide this service for you. Once you set up the seconday/slave nameserver service, enter the hostname of their secondary nameserver (multiple secondaries can be added separated with commas i.e. ns2.hostingcompany.com,ns3.hostingcompany.com):

From 5dd5fc4a1c1bf1cec793287c3bcc598a2777709e Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Fri, 10 Jul 2015 15:42:33 +0000 Subject: [PATCH 2/2] clean up multiple secondary nameservers and zone xfr ip addresses --- management/daemon.py | 4 +- management/dns_update.py | 86 ++++++++++++++++------------ management/status_checks.py | 2 +- management/templates/custom-dns.html | 15 +++-- 4 files changed, 62 insertions(+), 45 deletions(-) diff --git a/management/daemon.py b/management/daemon.py index 2d14b4e6..af15b1c3 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -222,14 +222,14 @@ def dns_update(): @authorized_personnel_only def dns_get_secondary_nameserver(): from dns_update import get_custom_dns_config, get_secondary_dns - return json_response({ "hostname": get_secondary_dns(get_custom_dns_config(env)) }) + return json_response({ "hostnames": get_secondary_dns(get_custom_dns_config(env), mode=None) }) @app.route('/dns/secondary-nameserver', methods=['POST']) @authorized_personnel_only def dns_set_secondary_nameserver(): from dns_update import set_secondary_dns try: - return set_secondary_dns(request.form.get('hostname').split(","), env) + return set_secondary_dns([ns.strip() for ns in re.split(r"[, ]+", request.form.get('hostnames') or "") if ns.strip() != ""], env) except ValueError as e: return (str(e), 400) diff --git a/management/dns_update.py b/management/dns_update.py index 5321b4f7..120c77ee 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -145,12 +145,10 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en # Define ns2.PRIMARY_HOSTNAME or whatever the user overrides. # User may provide one or more additional nameservers - secondary_dns_records = get_secondary_dns(additional_records) - if len(secondary_dns_records) > 0: - for secondary_ns in secondary_dns_records: - records.append((None, "NS", secondary_ns+'.', False)) - else: - records.append((None, "NS", "ns2." + env["PRIMARY_HOSTNAME"] + '.', False)) + secondary_ns_list = get_secondary_dns(additional_records, mode="NS") \ + or ["ns2." + env["PRIMARY_HOSTNAME"]] + for secondary_ns in secondary_ns_list: + records.append((None, "NS", secondary_ns+'.', False)) # In PRIMARY_HOSTNAME... @@ -469,21 +467,8 @@ zone: # If custom secondary nameservers have been set, allow zone transfers # and notifies to them. - if get_secondary_dns(additional_records, ['_secondary_nameserver']): - for hostname in get_secondary_dns(additional_records): - # Get the IP address of the nameserver by resolving it. - resolver = dns.resolver.get_default_resolver() - response = dns.resolver.query(hostname+'.', "A") - ipaddr = str(response[0]) - nsdconf += "\n\tnotify: %s NOKEY\n\tprovide-xfr: %s NOKEY" % (ipaddr, ipaddr) - # Some providers use different servers for zone transfers and notifies - # such as DNS Made Easy. This allows us to set these IP addresses as well manually - # in custom.yaml - if get_secondary_dns(additional_records, ["_secondary_notify_xfr"]): - for ipaddr in get_secondary_dns(additional_records, ["_secondary_notify_xfr"]): - nsdconf += "\n\tnotify: %s NOKEY\n\tprovide-xfr: %s NOKEY" % (ipaddr, ipaddr) - - + for ipaddr in get_secondary_dns(additional_records, mode="xfr"): + nsdconf += "\n\tnotify: %s NOKEY\n\tprovide-xfr: %s NOKEY\n" % (ipaddr, ipaddr) # Check if the file is changing. If it isn't changing, # return False to flag that no change was made. @@ -800,32 +785,59 @@ def set_custom_dns_record(qname, rtype, value, action, env): ######################################################################## -def get_secondary_dns(custom_dns, dns_type=['_secondary_nameserver']): - valid_types = set(['_secondary_nameserver', '_secondary_notify_xfr']) - if not valid_types.issuperset(set(dns_type)): - raise ValueError("Valid types are one or more of the following: %s" % ", ".join(valid_types)) +def get_secondary_dns(custom_dns, mode=None): + resolver = dns.resolver.get_default_resolver() values = [] for qname, rtype, value in custom_dns: - if qname in dns_type: - if isinstance(value, str): - values.append(value) + if qname != '_secondary_nameserver': continue + for hostname in value.split(" "): + hostname = hostname.strip() + if mode == None: + # Just return the setting. + values.append(hostname) + + # This is a hostname. Before including in zone xfr lines, + # resolve to an IP address. Otherwise just return the hostname. + if not hostname.startswith("xfr:"): + if mode == "xfr": + response = dns.resolver.query(hostname+'.', "A") + hostname = str(response[0]) + values.append(hostname) + + # This is a zone-xfer-only IP address. Do not return if + # we're querying for NS record hostnames. Only return if + # we're querying for zone xfer IP addresses - return the + # IP address. + elif mode == "xfr": + values.append(hostname[4:]) + return values -def set_secondary_dns(hostname, env): - hostnames = [item.strip().lower() for item in hostname] +def set_secondary_dns(hostnames, env): if len(hostnames) > 0: + # Validate that all hostnames are valid and that all zone-xfer IP addresses are valid. resolver = dns.resolver.get_default_resolver() for item in hostnames: - try: - response = dns.resolver.query(item, "A") - except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): - raise ValueError("Could not resolve the IP address of %s." % item) - # Set. - set_custom_dns_record("_secondary_nameserver", "A", {"A":hostnames}, "set", env) + if not item.startswith("xfr:"): + # Resolve hostname. + try: + response = dns.resolver.query(item, "A") + except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): + raise ValueError("Could not resolve the IP address of %s." % item) + else: + # Validate IP address. + try: + v = ipaddress.ip_address(item[4:]) # raises a ValueError if there's a problem + if not isinstance(v, ipaddress.IPv4Address): raise ValueError("That's an IPv6 address.") + except ValueError: + raise ValueError("'%s' is not an IPv4 address." % item[4:]) + + # Set. + set_custom_dns_record("_secondary_nameserver", "A", " ".join(hostnames), "set", env) else: # Clear. - set_custom_dns_record("_secondary_nameserver", "A", None, "set", env) + set_custom_dns_record("_secondary_nameserver", "A", None, "set", env) # Apply. return do_dns_update(env) diff --git a/management/status_checks.py b/management/status_checks.py index b804378a..8a31a2f2 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -370,7 +370,7 @@ def check_dns_zone(domain, env, output, dns_zonefiles): # the TLD, and so we're not actually checking the TLD. For that we'd need # to do a DNS trace. ip = query_dns(domain, "A") - secondary_ns = get_secondary_dns(get_custom_dns_config(env)) or ["ns2." + env['PRIMARY_HOSTNAME']] + secondary_ns = get_secondary_dns(get_custom_dns_config(env), mode="NS") or ["ns2." + env['PRIMARY_HOSTNAME']] existing_ns = query_dns(domain, "NS") correct_ns = "; ".join(sorted(["ns1." + env['PRIMARY_HOSTNAME']] + secondary_ns)) if existing_ns.lower() == correct_ns.lower(): diff --git a/management/templates/custom-dns.html b/management/templates/custom-dns.html index ddc1153f..99fcd6b6 100644 --- a/management/templates/custom-dns.html +++ b/management/templates/custom-dns.html @@ -67,7 +67,8 @@

Using a Secondary Nameserver

-

If your TLD requires you to have two separate nameservers, you can either set up a secondary (aka “slave”) nameserver or, alternatively, set up external DNS and ignore the DNS server on this box. If you choose to use a seconday/slave nameserver, you must find a seconday/slave nameserver service provider. Your domain name registrar or virtual cloud provider may provide this service for you. Once you set up the seconday/slave nameserver service, enter the hostname of their secondary nameserver (multiple secondaries can be added separated with commas i.e. ns2.hostingcompany.com,ns3.hostingcompany.com):

+

If your TLD requires you to have two separate nameservers, you can either set up external DNS and ignore the DNS server on this box entirely, or use the DNS server on this box but add a secondary (aka “slave”) nameserver.

+

If you choose to use a seconday nameserver, you must find a seconday nameserver service provider. Your domain name registrar or virtual cloud provider may provide this service for you. Once you set up the seconday nameserver service, enter the hostname (not the IP address) of their secondary nameserver in the box below.

@@ -83,7 +84,11 @@
@@ -152,8 +157,8 @@ function show_custom_dns() { "GET", { }, function(data) { - $('#secondarydnsHostname').val(data.hostname ? data.hostname : ''); - $('#secondarydns-clear-instructions').toggle(data.hostname != null); + $('#secondarydnsHostname').val(data.hostnames.join(' ')); + $('#secondarydns-clear-instructions').toggle(data.hostnames.length > 0); }); api( @@ -210,7 +215,7 @@ function do_set_secondary_dns() { "/dns/secondary-nameserver", "POST", { - hostname: $('#secondarydnsHostname').val() + hostnames: $('#secondarydnsHostname').val() }, function(data) { if (data == "") return; // nothing updated