Commit 7803ac9c authored by Joshua Tauberer's avatar Joshua Tauberer

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

parent 91cf45c8
...@@ -129,36 +129,41 @@ def build_zone(domain, subdomains, additional_records, env, with_ns=True): ...@@ -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): ...@@ -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()
else:
raise ValueError()
for rtype, value2 in values:
if rtype == "TXT": value2 = "\"" + value2 + "\"" if rtype == "TXT": value2 = "\"" + value2 + "\""
records.append((qname, rtype, 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): ...@@ -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 ...@@ -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): ...@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment