simplify remote zone-file checking and insist for "dns" subcommand that all records are present

This commit is contained in:
holger krekel
2024-07-10 18:57:50 +02:00
parent 9b5b4c3787
commit ffe313528e
4 changed files with 48 additions and 33 deletions

View File

@@ -1,8 +1,8 @@
{% if ipv4 %}
{{ chatmail_domain }}. A {{ ' '.join(ipv4) }}
{{ chatmail_domain }}. A {{ ipv4 }}
{% endif %}
{% if ipv6 %}
{{ chatmail_domain }}. AAAA {{ ' '.join(ipv6) }}
{{ chatmail_domain }}. AAAA {{ ipv6 }}
{% endif %}
{{ chatmail_domain }}. MX 10 {{ chatmail_domain }}.
_submission._tcp.{{ chatmail_domain }}. SRV 0 1 587 {{ chatmail_domain }}.

View File

@@ -16,7 +16,7 @@ from chatmaild.config import read_config, write_initial_config
from termcolor import colored
from . import remote_funcs
from .dns import show_dns
from .dns import NoIPRecords, show_dns
from .sshexec import SSHExec
#
@@ -67,7 +67,7 @@ def run_cmd(args, out):
out.green("Deploy completed, call `cmdeploy test` next.")
elif not remote_data["acme_account_url"]:
out.red("Deploy completed but letsencrypt not configured")
out.red("Run 'cmdeploy dns' or 'cmdeploy run' again")
out.red("Run 'cmdeploy run' again")
retcode = 0
else:
out.red("Deploy failed")
@@ -85,6 +85,10 @@ def dns_cmd_options(parser):
def dns_cmd(args, out):
"""Check DNS entries and optionally generate dns zone file."""
retcode, remote_data = show_dns(args, out)
for name in ["acme_account_url", "dkim_entry"]:
if not remote_data[name]:
# dns run insists on all records present
return 1
return retcode
@@ -305,6 +309,9 @@ def main(args=None):
except KeyboardInterrupt:
out.red("KeyboardInterrupt")
sys.exit(130)
except NoIPRecords as e:
out.red(str(e))
sys.exit(1)
if __name__ == "__main__":

View File

@@ -7,7 +7,11 @@ from jinja2 import Template
from . import remote_funcs
def show_dns(args, out, fullcheck=True) -> int:
class NoIPRecords(Exception):
"""Indicates that no DNS A or AAAA record is present."""
def show_dns(args, out) -> int:
"""Check existing DNS records, optionally write them to zone file
and return (exitcode, remote_data) tuple."""
template = importlib.resources.files(__package__).joinpath("chatmail.zone.j2")
@@ -23,9 +27,11 @@ def show_dns(args, out, fullcheck=True) -> int:
remote_data = sshexec(remote_funcs.perform_initial_checks, mail_domain=mail_domain)
if not remote_data["ipv4"] and not remote_data["ipv6"]:
print()
print(f"no A and also no AAAA record set for domain: {mail_domain}")
raise SystemExit(1)
raise NoIPRecords(f"No A or AAAA DNS records set for {mail_domain}!")
sts_id = remote_data.get("sts_id")
if not sts_id:
sts_id = datetime.datetime.now().strftime("%Y%m%d%H%M")
content = template.read_text()
zonefile = Template(content).render(
@@ -33,14 +39,12 @@ def show_dns(args, out, fullcheck=True) -> int:
dkim_entry=remote_data.get("dkim_entry"),
ipv4=remote_data["ipv4"],
ipv6=remote_data["ipv6"],
sts_id=datetime.datetime.now().strftime("%Y%m%d%H%M"),
sts_id=sts_id,
chatmail_domain=args.config.mail_domain,
)
zonefile = "\n".join([x.strip() for x in zonefile.split("\n") if x.strip()])
to_print = sshexec(
remote_funcs.check_zonefile, zonefile=zonefile, fullcheck=fullcheck
)
to_print = sshexec(remote_funcs.check_zonefile, zonefile=zonefile)
if not args.verbose:
print()

View File

@@ -36,16 +36,30 @@ def perform_initial_checks(mail_domain):
shell("apt-get install -y dnsutils")
shell(f"unbound-control flush_zone {mail_domain}", fail_ok=True)
res["dkim_entry"] = get_dkim_entry(mail_domain, dkim_selector="opendkim")
res["ipv4"] = query_dns("A", mail_domain)
res["ipv6"] = query_dns("AAAA", mail_domain)
# parse out sts-id if exists
val = query_dns("TXT", f"_mta-sts.{mail_domain}")
if val:
# "v=STSv1; id={{ sts_id }}"
parts = val.split("id=")
if len(parts) == 2:
val = parts[1].rstrip('"')
res["sts_id"] = val
return res
def get_dkim_entry(mail_domain, dkim_selector):
dkim_pubkey = shell(
f"openssl rsa -in /etc/dkimkeys/{dkim_selector}.private "
"-pubout 2>/dev/null | awk '/-/{next}{printf(\"%s\",$0)}'"
)
try:
dkim_pubkey = shell(
f"openssl rsa -in /etc/dkimkeys/{dkim_selector}.private "
"-pubout 2>/dev/null | awk '/-/{next}{printf(\"%s\",$0)}'"
)
except CalledProcessError:
return
dkim_value_raw = f"v=DKIM1;k=rsa;p={dkim_pubkey};s=email;t=s"
dkim_value = '" "'.join(re.findall(".{1,255}", dkim_value_raw))
return f'{dkim_selector}._domainkey.{mail_domain}. TXT "{dkim_value}"'
@@ -54,10 +68,11 @@ def get_dkim_entry(mail_domain, dkim_selector):
def query_dns(typ, domain):
res = shell(f"dig -r -q {domain} -t {typ} +short")
print(res)
return set(filter(None, res.split("\n")))
if res:
return res.split("\n")[0]
def check_zonefile(zonefile, fullcheck):
def check_zonefile(zonefile):
diff = []
for zf_line in zonefile.splitlines():
@@ -66,21 +81,10 @@ def check_zonefile(zonefile, fullcheck):
zf_domain, zf_typ, zf_value = zf_line.split(maxsplit=2)
zf_domain = zf_domain.rstrip(".")
zf_value = zf_value.strip()
query_values = query_dns(zf_typ, zf_domain)
if zf_value in query_values:
continue
if zf_typ == "CAA" and zf_value.endswith('accounturi="'):
# this is an initial run where acmetool did not work yet
continue
if query_values and zf_typ == "TXT" and zf_domain.startswith("_mta-sts."):
(query_value,) = query_values
if query_value.split("id=")[0] == zf_value.split("id=")[0]:
continue
assert zf_typ in ("A", "AAAA", "CNAME", "CAA", "SRV", "MX", "TXT"), zf_line
diff.append(zf_line)
query_value = query_dns(zf_typ, zf_domain)
if zf_value != query_value:
assert zf_typ in ("A", "AAAA", "CNAME", "CAA", "SRV", "MX", "TXT"), zf_line
diff.append(zf_line)
return diff