This repository has no description
4.9 kB
130 lines
1let
2 domain = "relay.lassul.us";
3 # Plain HTTP/WS on loopback only; nginx terminates TLS in front of it.
4 httpPort = 3340;
5 # QUIC address discovery. Public UDP, and the one part nginx cannot proxy,
6 # so the relay reads the ACME cert itself. 7842 is iroh's
7 # DEFAULT_RELAY_QUIC_PORT -- clients derive it from the relay url and expect
8 # it there, so do not move it without telling every client.
9 quicPort = 7842;
10 certGroup = "irohrelaycert";
11
12 configFile = (
13 pkgs:
14 pkgs.writeText "iroh-relay.toml" ''
15 enable_relay = true
16 http_bind_addr = "127.0.0.1:${toString httpPort}"
17
18 # The whole reason to run our own relay: QAD tells an endpoint its own NAT
19 # mapping, which is what makes hole punching work. Without it this is just
20 # a data relay and peers never upgrade to direct. Requires TLS.
21 enable_quic_addr_discovery = true
22
23 enable_metrics = true
24 metrics_bind_addr = "127.0.0.1:9096"
25
26 [tls]
27 hostname = "${domain}"
28 quic_bind_addr = "[::]:${toString quicPort}"
29
30 # `Reloading` re-reads the cert from disk periodically, so ACME renewals are
31 # picked up without a restart. `LetsEncrypt` is not usable here: it wants to
32 # own port 80 for the challenge, which nginx already has.
33 cert_mode = "Reloading"
34 manual_cert_path = "/var/lib/acme/${domain}/fullchain.pem"
35 manual_key_path = "/var/lib/acme/${domain}/key.pem"
36
37 # Serve the relay's HTTP/WS side as plain HTTP so nginx can reverse-proxy
38 # it; the cert above is then used only by the QUIC listener. "dangerous"
39 # overstates it here: the plaintext hop is loopback, nginx does real TLS on
40 # 443, and relayed payloads are already end-to-end encrypted between iroh
41 # endpoints -- the relay only ever sees ciphertext.
42 dangerous_http_only = true
43 ''
44 );
45in
46{
47 pkgs,
48 ...
49}:
50{
51 # Self-hosted iroh relay.
52 #
53 # A relay supplies the two things a relay-free iroh endpoint cannot get for
54 # itself: QUIC address discovery (learning its own NAT mapping) and a
55 # signalling path to coordinate hole punching. Measured from a double-NATed
56 # host, relayed configs hole-punched to direct in ~80ms with 0 failures over
57 # 30 runs, while the relay-free config failed 100% of the time.
58 #
59 # Running our own also fixes the throughput floor: n0's public relays
60 # rate-limit to ~0.9 Mbps, which is fine for keystrokes and miserable for
61 # anything larger when hole punching fails.
62 #
63 # Clients opt in with `RelayMode::Custom`, e.g.
64 # iroh-bench --config dht-selfrelay --relay-url https://relay.lassul.us
65 # dumbpipe listen-tcp --relay-url https://relay.lassul.us (patched build)
66
67 # nginx owns 80/443 and the ACME challenge, so it fetches the cert and the
68 # relay only reads it. A dedicated group keeps that read as narrow as
69 # possible; nginx must be a member to serve the vhost. Same pattern as
70 # mailserver.nix's `lasscert`.
71 security.acme.certs.${domain}.group = certGroup;
72 users.groups.${certGroup}.members = [ "nginx" ];
73
74 services.nginx.virtualHosts.${domain} = {
75 enableACME = true;
76 forceSSL = true;
77 # One location covers every route the relay serves: `/relay` (the websocket
78 # clients ride), `/ping` and `/generate_204` (net_report latency probes,
79 # which is how a client picks its nearest relay out of a map).
80 locations."/" = {
81 proxyPass = "http://127.0.0.1:${toString httpPort}";
82 proxyWebsockets = true;
83 # Relay connections are long-lived and idle whenever a pair has upgraded
84 # to a direct path; nginx's default 60s read timeout would tear them down
85 # and force clients back through a reconnect.
86 extraConfig = ''
87 proxy_read_timeout 1d;
88 proxy_send_timeout 1d;
89 '';
90 };
91 };
92
93 systemd.services.iroh-relay = {
94 description = "iroh relay (${domain})";
95 wantedBy = [ "multi-user.target" ];
96 after = [
97 "network-online.target"
98 "nginx.service"
99 ];
100 wants = [ "network-online.target" ];
101
102 serviceConfig = {
103 ExecStart = "${pkgs.iroh-relay}/bin/iroh-relay --config-path ${configFile pkgs}";
104 Restart = "always";
105 RestartSec = 5;
106
107 DynamicUser = true;
108 # Read access to /var/lib/acme/${domain}. Works with DynamicUser: systemd
109 # adds the transient user to the group at start.
110 SupplementaryGroups = [ certGroup ];
111
112 # ProtectSystem=strict only remounts read-only, so reading the cert is
113 # still fine.
114 ProtectSystem = "strict";
115 ProtectHome = true;
116 PrivateTmp = true;
117 NoNewPrivileges = true;
118 ProtectKernelTunables = true;
119 ProtectControlGroups = true;
120 RestrictAddressFamilies = [
121 "AF_INET"
122 "AF_INET6"
123 ];
124 };
125 };
126
127 # Only the QAD listener needs a hole punched; the relay's HTTP side is
128 # reached through nginx on 443, which is already open.
129 networking.firewall.allowedUDPPorts = [ quicPort ];
130}