mirror of
https://github.com/chatmail/relay.git
synced 2026-05-10 16:04:37 +00:00
simplify remote zone-file checking and insist for "dns" subcommand that all records are present
This commit is contained in:
@@ -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 }}.
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user