mirror of
https://github.com/chatmail/relay.git
synced 2026-05-10 16:04:37 +00:00
Deploy iroh relay
This commit is contained in:
@@ -17,4 +17,5 @@ $TTL 300
|
||||
;; DNS records.
|
||||
@ IN A 37.27.95.249
|
||||
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.
|
||||
|
||||
@@ -17,5 +17,6 @@ $TTL 300
|
||||
;; DNS records.
|
||||
@ IN A 37.27.24.139
|
||||
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.
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
- add guide to migrate chatmail to a new server
|
||||
([#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.
|
||||
([#437](https://github.com/deltachat/chatmail/pull/437))
|
||||
|
||||
|
||||
@@ -33,7 +33,12 @@ class Config:
|
||||
self.mtail_address = params.get("mtail_address")
|
||||
self.disable_ipv6 = params.get("disable_ipv6", "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_mail = params.get("privacy_mail")
|
||||
self.privacy_pdo = params.get("privacy_pdo")
|
||||
|
||||
@@ -55,6 +55,13 @@ postfix_reinject_port = 10025
|
||||
# if set to "True" IPv6 is disabled
|
||||
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,
|
||||
# e.g. 127.0.0.1 or some private network
|
||||
# address like 192.168.10.1.
|
||||
|
||||
@@ -10,7 +10,7 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
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.systemd import SystemdEnabled
|
||||
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:
|
||||
"""Deploy a chat-mail instance.
|
||||
|
||||
@@ -508,6 +557,7 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
||||
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
|
||||
files.put(
|
||||
@@ -556,9 +606,11 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
deploy_iroh_relay(config)
|
||||
|
||||
# Deploy acmetool to have TLS certificates.
|
||||
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(
|
||||
|
||||
@@ -69,8 +69,9 @@ def run_cmd(args, out):
|
||||
"""Deploy chatmail services on the remote server."""
|
||||
|
||||
sshexec = args.get_sshexec()
|
||||
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
|
||||
if not dns.check_initial_remote_data(remote_data, print=out.red):
|
||||
require_iroh = args.config.enable_iroh_relay
|
||||
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
|
||||
|
||||
env = os.environ.copy()
|
||||
@@ -109,7 +110,8 @@ def dns_cmd_options(parser):
|
||||
def dns_cmd(args, out):
|
||||
"""Check DNS entries and optionally generate dns zone file."""
|
||||
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:
|
||||
return 1
|
||||
|
||||
|
||||
@@ -6,19 +6,22 @@ from jinja2 import Template
|
||||
from . import remote
|
||||
|
||||
|
||||
def get_initial_remote_data(sshexec, mail_domain):
|
||||
def get_initial_remote_data(sshexec, mail_domain, iroh_enabled):
|
||||
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"]
|
||||
if not remote_data["A"] and not remote_data["AAAA"]:
|
||||
print(f"Missing A and/or AAAA DNS records for {mail_domain}!")
|
||||
elif remote_data["MTA_STS"] != f"{mail_domain}.":
|
||||
print("Missing MTA-STS CNAME record:")
|
||||
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}.":
|
||||
print("Missing www CNAME record:")
|
||||
print(f"www.{mail_domain}. CNAME {mail_domain}.")
|
||||
|
||||
12
cmdeploy/src/cmdeploy/iroh-relay.service
Normal file
12
cmdeploy/src/cmdeploy/iroh-relay.service
Normal 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
|
||||
5
cmdeploy/src/cmdeploy/iroh-relay.toml
Normal file
5
cmdeploy/src/cmdeploy/iroh-relay.toml
Normal 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"
|
||||
@@ -108,4 +108,16 @@ http {
|
||||
return 301 $scheme://{{ config.domain_name }}$request_uri;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import re
|
||||
from .rshell import CalledProcessError, shell
|
||||
|
||||
|
||||
def perform_initial_checks(mail_domain):
|
||||
def perform_initial_checks(mail_domain, iroh_enabled):
|
||||
"""Collecting initial DNS settings."""
|
||||
assert mail_domain
|
||||
if not shell("dig", fail_ok=True):
|
||||
@@ -23,13 +23,14 @@ def perform_initial_checks(mail_domain):
|
||||
A = query_dns("A", mail_domain)
|
||||
AAAA = query_dns("AAAA", 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}")
|
||||
|
||||
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["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
|
||||
|
||||
# parse out sts-id if exists, example: "v=STSv1; id=2090123"
|
||||
|
||||
@@ -18,13 +18,13 @@ class TestSSHExecutor:
|
||||
|
||||
def test_perform_initial(self, sshexec, maildomain):
|
||||
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"]
|
||||
|
||||
def test_logged(self, sshexec, maildomain, capsys):
|
||||
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()
|
||||
assert err.startswith("Collecting")
|
||||
@@ -33,7 +33,7 @@ class TestSSHExecutor:
|
||||
|
||||
sshexec.verbose = True
|
||||
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()
|
||||
lines = err.split("\n")
|
||||
@@ -44,7 +44,7 @@ class TestSSHExecutor:
|
||||
try:
|
||||
sshexec.logged(
|
||||
remote.rdns.perform_initial_checks,
|
||||
kwargs=dict(mail_domain=None),
|
||||
kwargs=dict(mail_domain=None, iroh_enabled=True),
|
||||
)
|
||||
except sshexec.FuncError as e:
|
||||
assert "rdns.py" in str(e)
|
||||
|
||||
@@ -26,6 +26,7 @@ def mockdns(mockdns_base):
|
||||
"AAAA": {"some.domain": "fde5:cd7a:9e1c:3240:5a99:936f:cdac:53ae"},
|
||||
"CNAME": {
|
||||
"mta-sts.some.domain": "some.domain.",
|
||||
"iroh.some.domain": "some.domain.",
|
||||
"www.some.domain": "some.domain.",
|
||||
},
|
||||
}
|
||||
@@ -35,30 +36,31 @@ def mockdns(mockdns_base):
|
||||
|
||||
class TestPerformInitialChecks:
|
||||
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["AAAA"] == mockdns["AAAA"]["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"]
|
||||
|
||||
@pytest.mark.parametrize("drop", ["A", "AAAA"])
|
||||
def test_perform_initial_checks_with_one_of_A_AAAA(self, 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]
|
||||
|
||||
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 not l
|
||||
|
||||
def test_perform_initial_checks_no_mta_sts(self, mockdns):
|
||||
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"]
|
||||
|
||||
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 len(l) == 2
|
||||
|
||||
|
||||
Reference in New Issue
Block a user