diff --git a/management/daemon.py b/management/daemon.py index 88dd9a42..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'), 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 5a3ca1dc..120c77ee 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -144,8 +144,11 @@ 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_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... @@ -462,17 +465,10 @@ 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. + 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. @@ -785,33 +781,63 @@ 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, mode=None): + resolver = dns.resolver.get_default_resolver() + + values = [] for qname, rtype, value in custom_dns: - if qname == "_secondary_nameserver": - return value - return None + if qname != '_secondary_nameserver': continue + for hostname in value.split(" "): + hostname = hostname.strip() + if mode == None: + # Just return the setting. + values.append(hostname) -def set_secondary_dns(hostname, env): + # 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) - if hostname in (None, ""): - # Clear. - set_custom_dns_record("_secondary_nameserver", "A", None, "set", env) - else: - # Validate. - hostname = hostname.strip().lower() + # 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(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() - 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) + for item in hostnames: + 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", hostname, "set", env) + set_custom_dns_record("_secondary_nameserver", "A", " ".join(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..8a31a2f2 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), mode="NS") 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..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:

+

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