Deploy iroh relay

This commit is contained in:
link2xt
2024-10-22 17:40:25 +00:00
committed by missytake
parent b92d9c889b
commit 5048bde6d0
14 changed files with 127 additions and 21 deletions

View File

@@ -17,4 +17,5 @@ $TTL 300
;; DNS records. ;; DNS records.
@ IN A 37.27.95.249 @ IN A 37.27.95.249
mta-sts.staging-ipv4.testrun.org. CNAME staging-ipv4.testrun.org. mta-sts.staging-ipv4.testrun.org. CNAME staging-ipv4.testrun.org.
iroh.staging-ipv4.testrun.org. CNAME staging-ipv4.testrun.org.
www.staging-ipv4.testrun.org. CNAME staging-ipv4.testrun.org. www.staging-ipv4.testrun.org. CNAME staging-ipv4.testrun.org.

View File

@@ -17,5 +17,6 @@ $TTL 300
;; DNS records. ;; DNS records.
@ IN A 37.27.24.139 @ IN A 37.27.24.139
mta-sts.staging2.testrun.org. CNAME staging2.testrun.org. mta-sts.staging2.testrun.org. CNAME staging2.testrun.org.
iroh.staging2.testrun.org. CNAME staging2.testrun.org.
www.staging2.testrun.org. CNAME staging2.testrun.org. www.staging2.testrun.org. CNAME staging2.testrun.org.

View File

@@ -5,6 +5,9 @@
- add guide to migrate chatmail to a new server - add guide to migrate chatmail to a new server
([#429](https://github.com/deltachat/chatmail/pull/429)) ([#429](https://github.com/deltachat/chatmail/pull/429))
- deploy `iroh-relay` (requires new "iroh.{mail_domain}" DNS entry)
([#434](https://github.com/deltachat/chatmail/pull/434))
- increase `request_queue_size` for UNIX sockets to 1000. - increase `request_queue_size` for UNIX sockets to 1000.
([#437](https://github.com/deltachat/chatmail/pull/437)) ([#437](https://github.com/deltachat/chatmail/pull/437))

View File

@@ -33,7 +33,12 @@ class Config:
self.mtail_address = params.get("mtail_address") self.mtail_address = params.get("mtail_address")
self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true" self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true"
self.imap_rawlog = params.get("imap_rawlog", "false").lower() == "true" self.imap_rawlog = params.get("imap_rawlog", "false").lower() == "true"
self.iroh_relay = params.get("iroh_relay") if "iroh_relay" not in params:
self.iroh_relay = "https://iroh." + params["mail_domain"]
self.enable_iroh_relay = True
else:
self.iroh_relay = params["iroh_relay"].strip()
self.enable_iroh_relay = False
self.privacy_postal = params.get("privacy_postal") self.privacy_postal = params.get("privacy_postal")
self.privacy_mail = params.get("privacy_mail") self.privacy_mail = params.get("privacy_mail")
self.privacy_pdo = params.get("privacy_pdo") self.privacy_pdo = params.get("privacy_pdo")

View File

@@ -55,6 +55,13 @@ postfix_reinject_port = 10025
# if set to "True" IPv6 is disabled # if set to "True" IPv6 is disabled
disable_ipv6 = False disable_ipv6 = False
# Defaults to https://iroh.{{mail_domain}} and running `iroh-relay` on the chatmail
# service.
# If you set it to anything else, the service will be disabled
# and users will be directed to use the given iroh relay URL.
# Set it to empty string if you want users to use their default iroh relay.
# iroh_relay =
# Address on which `mtail` listens, # Address on which `mtail` listens,
# e.g. 127.0.0.1 or some private network # e.g. 127.0.0.1 or some private network
# address like 192.168.10.1. # address like 192.168.10.1.

View File

@@ -10,7 +10,7 @@ import sys
from pathlib import Path from pathlib import Path
from chatmaild.config import Config, read_config from chatmaild.config import Config, read_config
from pyinfra import host from pyinfra import host, facts
from pyinfra.facts.files import File from pyinfra.facts.files import File
from pyinfra.facts.systemd import SystemdEnabled from pyinfra.facts.systemd import SystemdEnabled
from pyinfra.operations import apt, files, pip, server, systemd from pyinfra.operations import apt, files, pip, server, systemd
@@ -479,6 +479,55 @@ def deploy_mtail(config):
) )
def deploy_iroh_relay(config) -> None:
(url, sha256sum) = {
"x86_64": ("https://github.com/n0-computer/iroh/releases/download/v0.27.0/iroh-relay-v0.27.0-x86_64-unknown-linux-musl.tar.gz", "8af7f6d29d17476ce5c3053c3161db5793cb2ac49057d0bcaf689436cdccbeab"),
"aarch64": ("https://github.com/n0-computer/iroh/releases/download/v0.27.0/iroh-relay-v0.27.0-aarch64-unknown-linux-musl.tar.gz", "18039f0d39df78922a5055a0d4a5a8fa98a2a0e19b1eaa4c3fe6db73b8698697")
}[host.get_fact(facts.server.Arch)]
server.shell(
name="Download iroh-relay",
commands=[
f"(echo '{sha256sum} /usr/local/bin/iroh-relay' | sha256sum -c) || curl -L {url} | gunzip | tar -x -f - ./iroh-relay -O >/usr/local/bin/iroh-relay",
"chmod 755 /usr/local/bin/iroh-relay",
],
)
need_restart = False
systemd_unit = files.put(
name="Upload iroh-relay systemd unit",
src=importlib.resources.files(__package__).joinpath(
"iroh-relay.service"
),
dest="/etc/systemd/system/iroh-relay.service",
user="root",
group="root",
mode="644",
)
need_restart |= systemd_unit.changed
iroh_config = files.put(
name=f"Upload iroh-relay config",
src=importlib.resources.files(__package__).joinpath(
"iroh-relay.toml"
),
dest=f"/etc/iroh-relay.toml",
user="iroh",
group="iroh",
mode="600",
)
need_restart |= iroh_config.changed
systemd.service(
name="Start and enable iroh-relay",
service="iroh-relay.service",
running=True,
enabled=config.enable_iroh_relay,
restarted=need_restart,
)
def deploy_chatmail(config_path: Path, disable_mail: bool) -> None: def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
"""Deploy a chat-mail instance. """Deploy a chat-mail instance.
@@ -508,6 +557,7 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
system=True, system=True,
) )
server.user(name="Create echobot user", user="echobot", system=True) server.user(name="Create echobot user", user="echobot", system=True)
server.user(name="Create iroh user", user="iroh", system=True)
# Add our OBS repository for dovecot_no_delay # Add our OBS repository for dovecot_no_delay
files.put( files.put(
@@ -556,9 +606,11 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
enabled=True, enabled=True,
) )
deploy_iroh_relay(config)
# Deploy acmetool to have TLS certificates. # Deploy acmetool to have TLS certificates.
deploy_acmetool( deploy_acmetool(
domains=[mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"], domains=[mail_domain, f"mta-sts.{mail_domain}", f"iroh.{mail_domain}", f"www.{mail_domain}"],
) )
apt.packages( apt.packages(

View File

@@ -69,8 +69,9 @@ def run_cmd(args, out):
"""Deploy chatmail services on the remote server.""" """Deploy chatmail services on the remote server."""
sshexec = args.get_sshexec() sshexec = args.get_sshexec()
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain) require_iroh = args.config.enable_iroh_relay
if not dns.check_initial_remote_data(remote_data, print=out.red): remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain, require_iroh)
if not dns.check_initial_remote_data(remote_data, require_iroh, print=out.red):
return 1 return 1
env = os.environ.copy() env = os.environ.copy()
@@ -109,7 +110,8 @@ def dns_cmd_options(parser):
def dns_cmd(args, out): def dns_cmd(args, out):
"""Check DNS entries and optionally generate dns zone file.""" """Check DNS entries and optionally generate dns zone file."""
sshexec = args.get_sshexec() sshexec = args.get_sshexec()
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain) require_iroh = args.config.enable_iroh_relay
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain, require_iroh)
if not remote_data: if not remote_data:
return 1 return 1

View File

@@ -6,19 +6,22 @@ from jinja2 import Template
from . import remote from . import remote
def get_initial_remote_data(sshexec, mail_domain): def get_initial_remote_data(sshexec, mail_domain, iroh_enabled):
return sshexec.logged( return sshexec.logged(
call=remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=mail_domain) call=remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=mail_domain, iroh_enabled=iroh_enabled)
) )
def check_initial_remote_data(remote_data, print=print): def check_initial_remote_data(remote_data, require_iroh, *, print=print):
mail_domain = remote_data["mail_domain"] mail_domain = remote_data["mail_domain"]
if not remote_data["A"] and not remote_data["AAAA"]: if not remote_data["A"] and not remote_data["AAAA"]:
print(f"Missing A and/or AAAA DNS records for {mail_domain}!") print(f"Missing A and/or AAAA DNS records for {mail_domain}!")
elif remote_data["MTA_STS"] != f"{mail_domain}.": elif remote_data["MTA_STS"] != f"{mail_domain}.":
print("Missing MTA-STS CNAME record:") print("Missing MTA-STS CNAME record:")
print(f"mta-sts.{mail_domain}. CNAME {mail_domain}.") print(f"mta-sts.{mail_domain}. CNAME {mail_domain}.")
elif require_iroh and remote_data["IROH"] != f"{mail_domain}.":
print("Missing iroh CNAME record:")
print(f"iroh.{mail_domain}. CNAME {mail_domain}.")
elif remote_data["WWW"] != f"{mail_domain}.": elif remote_data["WWW"] != f"{mail_domain}.":
print("Missing www CNAME record:") print("Missing www CNAME record:")
print(f"www.{mail_domain}. CNAME {mail_domain}.") print(f"www.{mail_domain}. CNAME {mail_domain}.")

View File

@@ -0,0 +1,12 @@
[Unit]
Description=Iroh relay
[Service]
ExecStart=/usr/local/bin/iroh-relay --config-path /etc/iroh-relay.toml
Restart=on-failure
RestartSec=5s
User=iroh
Group=iroh
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,5 @@
enable_relay = true
http_bind_addr = "[::]:3340"
enable_stun = true
enable_metrics = false
metrics_bind_addr = "127.0.0.1:9092"

View File

@@ -108,4 +108,16 @@ http {
return 301 $scheme://{{ config.domain_name }}$request_uri; return 301 $scheme://{{ config.domain_name }}$request_uri;
access_log syslog:server=unix:/dev/log,facility=local7; access_log syslog:server=unix:/dev/log,facility=local7;
} }
# Pass iroh. to iroh-relay service.
server {
listen 8443 ssl;
{% if not disable_ipv6 %}
listen [::]:8443 ssl;
{% endif %}
server_name iroh.{{ config.domain_name }};
location / {
proxy_pass http://127.0.0.1:3340;
}
}
} }

View File

@@ -15,7 +15,7 @@ import re
from .rshell import CalledProcessError, shell from .rshell import CalledProcessError, shell
def perform_initial_checks(mail_domain): def perform_initial_checks(mail_domain, iroh_enabled):
"""Collecting initial DNS settings.""" """Collecting initial DNS settings."""
assert mail_domain assert mail_domain
if not shell("dig", fail_ok=True): if not shell("dig", fail_ok=True):
@@ -23,13 +23,14 @@ def perform_initial_checks(mail_domain):
A = query_dns("A", mail_domain) A = query_dns("A", mail_domain)
AAAA = query_dns("AAAA", mail_domain) AAAA = query_dns("AAAA", mail_domain)
MTA_STS = query_dns("CNAME", f"mta-sts.{mail_domain}") MTA_STS = query_dns("CNAME", f"mta-sts.{mail_domain}")
IROH = query_dns("CNAME", f"iroh.{mail_domain}")
WWW = query_dns("CNAME", f"www.{mail_domain}") WWW = query_dns("CNAME", f"www.{mail_domain}")
res = dict(mail_domain=mail_domain, A=A, AAAA=AAAA, MTA_STS=MTA_STS, WWW=WWW) res = dict(mail_domain=mail_domain, A=A, AAAA=AAAA, MTA_STS=MTA_STS, IROH=IROH, WWW=WWW)
res["acme_account_url"] = shell("acmetool account-url", fail_ok=True) res["acme_account_url"] = shell("acmetool account-url", fail_ok=True)
res["dkim_entry"] = get_dkim_entry(mail_domain, dkim_selector="opendkim") res["dkim_entry"] = get_dkim_entry(mail_domain, dkim_selector="opendkim")
if not MTA_STS or not WWW or (not A and not AAAA): if not MTA_STS or (not IROH and not iroh_enabled) or not WWW or (not A and not AAAA):
return res return res
# parse out sts-id if exists, example: "v=STSv1; id=2090123" # parse out sts-id if exists, example: "v=STSv1; id=2090123"

View File

@@ -18,13 +18,13 @@ class TestSSHExecutor:
def test_perform_initial(self, sshexec, maildomain): def test_perform_initial(self, sshexec, maildomain):
res = sshexec( res = sshexec(
remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=maildomain) remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=maildomain, iroh_enabled=True)
) )
assert res["A"] or res["AAAA"] assert res["A"] or res["AAAA"]
def test_logged(self, sshexec, maildomain, capsys): def test_logged(self, sshexec, maildomain, capsys):
sshexec.logged( sshexec.logged(
remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=maildomain) remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=maildomain, iroh_enabled=True)
) )
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert err.startswith("Collecting") assert err.startswith("Collecting")
@@ -33,7 +33,7 @@ class TestSSHExecutor:
sshexec.verbose = True sshexec.verbose = True
sshexec.logged( sshexec.logged(
remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=maildomain) remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=maildomain, iroh_enabled=True)
) )
out, err = capsys.readouterr() out, err = capsys.readouterr()
lines = err.split("\n") lines = err.split("\n")
@@ -44,7 +44,7 @@ class TestSSHExecutor:
try: try:
sshexec.logged( sshexec.logged(
remote.rdns.perform_initial_checks, remote.rdns.perform_initial_checks,
kwargs=dict(mail_domain=None), kwargs=dict(mail_domain=None, iroh_enabled=True),
) )
except sshexec.FuncError as e: except sshexec.FuncError as e:
assert "rdns.py" in str(e) assert "rdns.py" in str(e)

View File

@@ -26,6 +26,7 @@ def mockdns(mockdns_base):
"AAAA": {"some.domain": "fde5:cd7a:9e1c:3240:5a99:936f:cdac:53ae"}, "AAAA": {"some.domain": "fde5:cd7a:9e1c:3240:5a99:936f:cdac:53ae"},
"CNAME": { "CNAME": {
"mta-sts.some.domain": "some.domain.", "mta-sts.some.domain": "some.domain.",
"iroh.some.domain": "some.domain.",
"www.some.domain": "some.domain.", "www.some.domain": "some.domain.",
}, },
} }
@@ -35,30 +36,31 @@ def mockdns(mockdns_base):
class TestPerformInitialChecks: class TestPerformInitialChecks:
def test_perform_initial_checks_ok1(self, mockdns): def test_perform_initial_checks_ok1(self, mockdns):
remote_data = remote.rdns.perform_initial_checks("some.domain") remote_data = remote.rdns.perform_initial_checks("some.domain", iroh_enabled=True)
assert remote_data["A"] == mockdns["A"]["some.domain"] assert remote_data["A"] == mockdns["A"]["some.domain"]
assert remote_data["AAAA"] == mockdns["AAAA"]["some.domain"] assert remote_data["AAAA"] == mockdns["AAAA"]["some.domain"]
assert remote_data["MTA_STS"] == mockdns["CNAME"]["mta-sts.some.domain"] assert remote_data["MTA_STS"] == mockdns["CNAME"]["mta-sts.some.domain"]
assert remote_data["IROH"] == mockdns["CNAME"]["iroh.some.domain"]
assert remote_data["WWW"] == mockdns["CNAME"]["www.some.domain"] assert remote_data["WWW"] == mockdns["CNAME"]["www.some.domain"]
@pytest.mark.parametrize("drop", ["A", "AAAA"]) @pytest.mark.parametrize("drop", ["A", "AAAA"])
def test_perform_initial_checks_with_one_of_A_AAAA(self, mockdns, drop): def test_perform_initial_checks_with_one_of_A_AAAA(self, mockdns, drop):
del mockdns[drop] del mockdns[drop]
remote_data = remote.rdns.perform_initial_checks("some.domain") remote_data = remote.rdns.perform_initial_checks("some.domain", iroh_enabled=True)
assert not remote_data[drop] assert not remote_data[drop]
l = [] l = []
res = check_initial_remote_data(remote_data, print=l.append) res = check_initial_remote_data(remote_data, require_iroh=True, print=l.append)
assert res assert res
assert not l assert not l
def test_perform_initial_checks_no_mta_sts(self, mockdns): def test_perform_initial_checks_no_mta_sts(self, mockdns):
del mockdns["CNAME"]["mta-sts.some.domain"] del mockdns["CNAME"]["mta-sts.some.domain"]
remote_data = remote.rdns.perform_initial_checks("some.domain") remote_data = remote.rdns.perform_initial_checks("some.domain", iroh_enabled=True)
assert not remote_data["MTA_STS"] assert not remote_data["MTA_STS"]
l = [] l = []
res = check_initial_remote_data(remote_data, print=l.append) res = check_initial_remote_data(remote_data, require_iroh=True, print=l.append)
assert not res assert not res
assert len(l) == 2 assert len(l) == 2