write explanatory text as we build DNS zones so we can help the user manage DNS off of the box

This commit is contained in:
Joshua Tauberer 2014-07-17 12:36:45 +00:00
parent 91cf45c843
commit 7803ac9ca4
1 changed files with 36 additions and 22 deletions

View File

@ -129,36 +129,41 @@ def build_zone(domain, subdomains, additional_records, env, with_ns=True):
records = [] records = []
# For top-level zones, define ourselves as the authoritative name server. # For top-level zones, define ourselves as the authoritative name server.
# 'False' in the tuple indicates these records would not be used if the zone
# is managed outside of the box.
if with_ns: if with_ns:
records.append((None, "NS", "ns1.%s." % env["PRIMARY_HOSTNAME"])) records.append((None, "NS", "ns1.%s." % env["PRIMARY_HOSTNAME"], False))
records.append((None, "NS", "ns2.%s." % env["PRIMARY_HOSTNAME"])) records.append((None, "NS", "ns2.%s." % env["PRIMARY_HOSTNAME"], False))
# The MX record says where email for the domain should be delivered: Here! # The MX record says where email for the domain should be delivered: Here!
records.append((None, "MX", "10 %s." % env["PRIMARY_HOSTNAME"])) records.append((None, "MX", "10 %s." % env["PRIMARY_HOSTNAME"], "Required. Specifies the hostname of the machine that handles @%s mail." % domain))
# SPF record: Permit the box ('mx', see above) to send mail on behalf of # SPF record: Permit the box ('mx', see above) to send mail on behalf of
# the domain, and no one else. # the domain, and no one else.
records.append((None, "TXT", '"v=spf1 mx -all"')) records.append((None, "TXT", '"v=spf1 mx -all"', "Recomended. Specifies that only the box is permitted to send @%s mail." % domain))
# If we need to define DNS for any subdomains of this domain, include it # If we need to define DNS for any subdomains of this domain, include it
# in the zone. # in the zone.
for subdomain in subdomains: for subdomain in subdomains:
subdomain_qname = subdomain[0:-len("." + domain)] subdomain_qname = subdomain[0:-len("." + domain)]
for child_qname, child_rtype, child_value in build_zone(subdomain, [], {}, env, with_ns=False): subzone = build_zone(subdomain, [], {}, env, with_ns=False)
for child_qname, child_rtype, child_value, child_explanation in subzone:
if child_qname == None: if child_qname == None:
child_qname = subdomain_qname child_qname = subdomain_qname
else: else:
child_qname += "." + subdomain_qname child_qname += "." + subdomain_qname
records.append((child_qname, child_rtype, child_value)) records.append((child_qname, child_rtype, child_value, child_explanation))
# In PRIMARY_HOSTNAME... # In PRIMARY_HOSTNAME...
if domain == env["PRIMARY_HOSTNAME"]: if domain == env["PRIMARY_HOSTNAME"]:
# Define ns1 and ns2. # Define ns1 and ns2.
records.append(("ns1", "A", env["PUBLIC_IP"])) # 'False' in the tuple indicates these records would not be used if the zone
records.append(("ns2", "A", env["PUBLIC_IP"])) # is managed outside of the box.
records.append(("ns1", "A", env["PUBLIC_IP"], False))
records.append(("ns2", "A", env["PUBLIC_IP"], False))
# Add a DANE TLSA record for SMTP. # Add a DANE TLSA record for SMTP.
records.append(("_25._tcp", "TLSA", build_tlsa_record(env))) records.append(("_25._tcp", "TLSA", build_tlsa_record(env), "Recommended when DNSSEC is enabled. Advertises to mail servers connecting to the box that mandatory encryption should be used."))
def has_rec(qname, rtype): def has_rec(qname, rtype):
for rec in records: for rec in records:
@ -175,17 +180,26 @@ def build_zone(domain, subdomains, additional_records, env, with_ns=True):
qname = qname[0:len(qname)-len("." + domain)] qname = qname[0:len(qname)-len("." + domain)]
if has_rec(qname, value): continue if has_rec(qname, value): continue
if isinstance(value, str): if isinstance(value, str):
records.append((qname, "A", value)) values = [("A", value)]
elif isinstance(value, dict): elif isinstance(value, dict):
for rtype, value2 in value.items(): values = value.items()
if rtype == "TXT": value2 = "\"" + value2 + "\"" else:
records.append((qname, rtype, value2)) raise ValueError()
for rtype, value2 in values:
if rtype == "TXT": value2 = "\"" + value2 + "\""
records.append((qname, rtype, value2, "(Set by user.)"))
# Add defaults if not overridden by the user's custom settings. # Add defaults if not overridden by the user's custom settings.
if not has_rec(None, "A"): records.append((None, "A", env["PUBLIC_IP"])) defaults = [
if env.get('PUBLIC_IPV6') and not has_rec(None, "AAAA"): records.append((None, "AAAA", env["PUBLIC_IPV6"])) (None, "A", env["PUBLIC_IP"], "Optional. Sets the IP address that %s resolves to, e.g. for web hosting." % domain),
if not has_rec("www", "A"): records.append(("www", "A", env["PUBLIC_IP"])) ("www", "A", env["PUBLIC_IP"], "Optional. Sets the IP address that www.%s resolves to, e.g. for web hosting." % domain),
if env.get('PUBLIC_IPV6') and not has_rec("www", "AAAA"): records.append(("www", "AAAA", env["PUBLIC_IPV6"])) (None, "AAAA", env.get('PUBLIC_IPV6'), "Optional. Sets the IPv6 address that %s resolves to, e.g. for web hosting." % domain),
("www", "AAAA", env.get('PUBLIC_IPV6'), "Optional. Sets the IPv6 address that www.%s resolves to, e.g. for web hosting." % domain),
]
for qname, rtype, value, explanation in defaults:
if value is None or value.strip() == "": continue # skip IPV6 if not set
if not has_rec(qname, rtype):
records.append((qname, rtype, value, explanation))
# If OpenDKIM is in use.. # If OpenDKIM is in use..
opendkim_record_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.txt') opendkim_record_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.txt')
@ -193,12 +207,12 @@ def build_zone(domain, subdomains, additional_records, env, with_ns=True):
# Append the DKIM TXT record to the zone as generated by OpenDKIM, after string formatting above. # Append the DKIM TXT record to the zone as generated by OpenDKIM, after string formatting above.
with open(opendkim_record_file) as orf: with open(opendkim_record_file) as orf:
m = re.match(r"(\S+)\s+IN\s+TXT\s+(\(.*\))\s*;", orf.read(), re.S) m = re.match(r"(\S+)\s+IN\s+TXT\s+(\(.*\))\s*;", orf.read(), re.S)
records.append((m.group(1), "TXT", m.group(2))) records.append((m.group(1), "TXT", m.group(2), "Recommended. Specifies that only the box is permitted to send mail at this domain."))
# Append a DMARC record. # Append a DMARC record.
records.append(("_dmarc", "TXT", '"v=DMARC1; p=quarantine"')) records.append(("_dmarc", "TXT", '"v=DMARC1; p=quarantine"', "Optional. Specifies that mail that does not originate from the box but claims to be from @%s is suspect and should be quarantined by the recipient's mail system." % domain))
# Sort the records. The None records *must* go first. Otherwise it doesn't matter. # Sort the records. The None records *must* go first in the nsd zone file. Otherwise it doesn't matter.
records.sort(key = lambda rec : list(reversed(rec[0].split(".")) if rec[0] is not None else "")) records.sort(key = lambda rec : list(reversed(rec[0].split(".")) if rec[0] is not None else ""))
return records return records
@ -255,7 +269,7 @@ $TTL 86400 ; default time to live
zone = zone.format(domain=domain, primary_domain=env["PRIMARY_HOSTNAME"]) zone = zone.format(domain=domain, primary_domain=env["PRIMARY_HOSTNAME"])
# Add records. # Add records.
for subdomain, querytype, value in records: for subdomain, querytype, value, explanation in records:
if subdomain: if subdomain:
zone += subdomain zone += subdomain
zone += "\tIN\t" + querytype + "\t" zone += "\tIN\t" + querytype + "\t"
@ -490,7 +504,7 @@ def justtestingdotemail(domain, records):
if not domain.endswith(".justtesting.email"): if not domain.endswith(".justtesting.email"):
return return
for subdomain, querytype, value in records: for subdomain, querytype, value, explanation in records:
if querytype in ("NS",): continue if querytype in ("NS",): continue
if subdomain in ("www", "ns1", "ns2"): continue # don't do unnecessary things if subdomain in ("www", "ns1", "ns2"): continue # don't do unnecessary things