eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1<!doctype html>
2<html lang="en">
3<head>
4<meta charset="utf-8" />
5<meta name="viewport" content="width=device-width, initial-scale=1" />
6<title>The router — Running the fleet</title>
7<link rel="stylesheet" href="guide.css" />
8<script src="guide.js" defer></script>
9</head>
10<body data-page="router.html">
11<div class="frame">
12<nav class="side"></nav>
13<main class="content">
14<div class="wrap">
15
16<header class="top">
17 <p class="eyebrow"><span class="robot">&#129302;</span> Running the fleet &middot; Part 02 of 10</p>
18 <h1>The router: the front door of every box</h1>
19 <p class="thesis">Exactly one process on each box listens on the public internet. It terminates TLS, looks each request&rsquo;s hostname up in a small database, and forwards the request to the right local process over a unix socket. <strong>Everything else on the box hides behind it.</strong></p>
20</header>
21
22<div class="sysmap" data-highlight="router"></div>
23<p class="sysmap-caption">You are here: the router is the single public-facing process on a box. The hibernation activator (<a href="hibernation.html">part 06</a>) and the mesh (<a href="mesh.html">part 07</a>) both live inside this same process, switched on by configuration.</p>
24
25<h2 id="what-it-is">What the router is and why it exists</h2>
26<p>A box hosts many chat space instances, plus a few supporting services. Each of those is its own process, and none of them listens on a public network port — every one of them listens on a <strong>unix socket</strong>, which is a special file (in this system, under <code>/run/lchat/</code>) that processes on the same machine can connect to like a network address. Unix sockets are invisible to the internet, which is the point: the only way traffic reaches an instance is through the router.</p>
27<p>The router is started by systemd as the service <code>lchat-router.service</code>, and it is the same single <code>lchat</code> binary as everything else, started with the <code>router</code> subcommand. Its full start command, exactly as the systemd unit runs it, is:</p>
28<figure class="cmd">
29<figcaption>What systemd runs (from /etc/systemd/system/lchat-router.service)</figcaption>
30<pre><code>/opt/lchat/lchat router --registry /data/registry.db --acme-cache /data/acme</code></pre>
31</figure>
32<p>The two arguments name the two files the router depends on: the <a href="#the-registry">registry</a> (its routing table) and the <a href="#tls">certificate cache</a> (its TLS material). The router has three jobs, taken in turn below:</p>
33<ol>
34 <li><strong>TLS:</strong> obtain and present the right certificate for every hostname it serves.</li>
35 <li><strong>Routing:</strong> map each request&rsquo;s hostname to the right unix socket and forward the request.</li>
36 <li><strong>Hosting the activator and the mesh:</strong> when configured, wake sleeping instances (<a href="hibernation.html">part 06</a>) and cooperate with peer boxes (<a href="mesh.html">part 07</a>). This page covers the first two; the other two have their own pages.</li>
37</ol>
38
39<div class="plain">
40 <span class="label">In plain words</span>
41 <p>Think of the box as an office building. The router is the reception desk: it is the only door to the street, it checks which company (hostname) a visitor is asking for, and it walks the visitor to the right office (unix socket). The offices have no street doors of their own.</p>
42</div>
43
44<h2 id="the-registry">The registry: the routing table on disk</h2>
45<p>The router&rsquo;s routing table lives in a small SQLite database, by default <code>/data/registry.db</code>, called <strong>the registry</strong> throughout this guide. It has one table, <code>routes</code>, with one row per hostname the box serves. The columns you will care about as an operator:</p>
46<div class="tablewrap">
47<table>
48 <tr><th>Column</th><th>Meaning</th></tr>
49 <tr><td><code>host</code></td><td>The full domain name, for example <code>chat-a.example.com</code>. This is the primary key.</td></tr>
50 <tr><td><code>kind</code></td><td><code>instance</code> for a messenger space the box lifecycle-manages, or <code>service</code> for a sibling service (the home server, the push relay, the cloud provisioner) that deploys on its own cadence.</td></tr>
51 <tr><td><code>socket</code></td><td>The unix socket the router forwards this host&rsquo;s traffic to, for example <code>/run/lchat/chat-a.example.com.sock</code>.</td></tr>
52 <tr><td><code>status</code></td><td><code>creating</code>, <code>ready</code>, or <code>failed</code>. Only <code>ready</code> rows are served; a <code>failed</code> row keeps its error message in <code>last_error</code>.</td></tr>
53 <tr><td><code>version</code> / <code>prev_version</code></td><td>Which build of the software this instance currently runs, and which it ran before — the basis for <a href="deploys.html#rollback">rollback</a>.</td></tr>
54 <tr><td><code>hibernate</code></td><td>Whether this host is managed by the <a href="hibernation.html">activator</a> (put to sleep when idle) instead of by systemd.</td></tr>
55 <tr><td><code>next_due</code></td><td>For a hibernating host: the time of its earliest scheduled delivery (for example a reminder), so the activator can wake it on time with no request. See <a href="hibernation.html#due-time-wakes">part 06</a>.</td></tr>
56 <tr><td><code>db_path</code>, <code>app_store_url</code>, <code>home_origin</code>, <code>push_relay_url</code>, <code>push_relay_token</code>, <code>home_region</code>, <code>provider</code></td><td>Per-instance configuration recorded at creation time. <code>home_region</code> and <code>provider</code> are written but not yet acted on — they are seams for future geographic and co-operative placement. <span class="status partial">partially built</span></td></tr>
57</table>
58</div>
59<p>You rarely edit the registry directly — the command-line tools (<code>lchat add-instance</code>, <code>lchat remove</code>, and so on) do it for you, and <code>lchat instances</code> prints it in a readable form. But it is useful to know that the whole routing state of a box is one ordinary SQLite file you can inspect:</p>
60<figure class="cmd">
61<figcaption>Run on the box — inspect the routing table directly (read-only)</figcaption>
62<pre><code>sqlite3 "file:/data/registry.db?mode=ro" "SELECT host, kind, status, version, hibernate FROM routes"
63<span class="out">chat-a.example.com|instance|ready|1b4bd808|0
64chat-b.example.com|instance|ready|1b4bd808|1
65home.example.com|service|ready||0</span></code></pre>
66</figure>
67
68<h3 id="reloading">How the router learns about changes</h3>
69<p>The router keeps the routing table in memory (swapped atomically, so a reload never drops an in-flight connection) and re-reads the registry in two ways:</p>
70<ul>
71 <li><strong>On a reload signal.</strong> <code>systemctl reload lchat-router</code> sends the process a HUP signal, and the unit&rsquo;s <code>ExecReload</code> line is exactly that. The provisioning commands (<code>add-instance</code>, <code>remove</code>, <code>hibernate</code>) trigger this themselves, so you rarely type it.</li>
72 <li><strong>On a two-second poll.</strong> The router also fingerprints the registry file every two seconds and reloads if it changed. This exists because a change made by another process (for example the control daemon) must be picked up even if nobody sends a signal.</li>
73</ul>
74
75<h2 id="tls">TLS certificates: automatic, two ways</h2>
76<p>The router obtains its certificates from <a href="reference.html#gloss-le">Let&rsquo;s Encrypt</a> automatically. There are two mechanisms, and they coexist:</p>
77<h3 id="http01">Per-host certificates, issued on first contact</h3>
78<p>By default the router uses the ACME <strong>HTTP-01</strong> method: when a browser first connects for a hostname the router has never served, the router asks Let&rsquo;s Encrypt for a certificate for exactly that hostname, answers the validation request Let&rsquo;s Encrypt makes to port 80, and receives a certificate within a few seconds. Certificates (and their private keys) are cached on disk in <code>/data/acme/</code> — one file per host — so this happens once per host, not once per connection.</p>
79<p>Crucially, the router will only request a certificate for a hostname that exists in the registry with a serveable status. This rule (the &ldquo;host policy&rdquo;) prevents strangers from pointing arbitrary DNS names at your box and burning through Let&rsquo;s Encrypt&rsquo;s rate limits on your behalf.</p>
80<h3 id="dns01">A wildcard certificate for a whole domain</h3>
81<p>Per-host issuance rides Let&rsquo;s Encrypt&rsquo;s rate limit of roughly fifty new certificates per registered domain per week — a hard ceiling on how fast new spaces can be created. So the router can additionally hold a <strong>wildcard</strong> certificate (<code>*.example.com</code>), obtained with the ACME <strong>DNS-01</strong> method, which covers every direct subdomain at once. Enable it by setting two variables in the router&rsquo;s environment file, <code>/data/router.env</code>, then restarting the router:</p>
82<figure class="cmd">
83<figcaption>Contents of /data/router.env (edit on the box, then: systemctl restart lchat-router)</figcaption>
84<pre><code>LCHAT_WILDCARD_DOMAINS=example.com
85LCHAT_DNSIMPLE_TOKEN=&lt;a DNSimple account API token that can edit the example.com zone&gt;</code></pre>
86</figure>
87<p>With this set, the router re-checks the wildcard certificate at startup and every twelve hours, renewing when it is missing or within thirty days of expiry. Issuance works by publishing a temporary DNS record (<code>_acme-challenge.example.com</code>) through the DNSimple API, which is why the token is needed. The certificate persists in the same <code>/data/acme/</code> cache as <code>wildcard-example.com.pem</code>. Hostnames on other domains, and the apex domain itself, keep using the per-host path — and if wildcard issuance ever fails, per-host issuance quietly keeps covering those hosts too, so a DNS-provider outage degrades gracefully instead of taking the box down.</p>
88<div class="warn">
89 <span class="label">Treat the certificate cache as secret material</span>
90 <p>The files in <code>/data/acme/</code> contain private keys. They are mode 0600 for a reason. Include the directory in backups (losing it only forces re-issuance, but re-issuance is rate-limited), and never copy it anywhere world-readable.</p>
91</div>
92<div class="roadmap">
93 <span class="label">Designed, not built: a shared certificate cache in S3</span>
94 <p>Today each box keeps its own certificate cache on its own disk, which is fine while each hostname is served by exactly one box. The day a <em>pool</em> of boxes must be able to answer for the same hostname, the cache needs to live somewhere they all can see — object storage. The configuration value for this, <code>LCHAT_CERT_CACHE=s3</code>, is already reserved in the code and deliberately <em>refuses to start</em>, so nobody can half-enable an unfinished mode. Until it exists, certificate issuance ties a hostname to one box. See also <a href="mesh.html#mesh-limits">the mesh&rsquo;s honest limits</a>.</p>
95</div>
96
97<h2 id="ports">What the router does on each port</h2>
98<div class="tablewrap">
99<table>
100 <tr><th>Port</th><th>Behaviour</th></tr>
101 <tr><td><strong>443 (HTTPS)</strong></td><td>Terminates TLS with the certificate for the requested hostname, then routes by hostname (below). WebSocket connections — which the chat clients hold open continuously — pass straight through.</td></tr>
102 <tr><td><strong>80 (HTTP)</strong></td><td>Answers Let&rsquo;s Encrypt validation requests (<code>/.well-known/acme-challenge/&hellip;</code>), and permanently redirects everything else to HTTPS.</td></tr>
103</table>
104</div>
105<p>These are the only two public <em>TCP</em> ports on a box. The firewall guidance in <a href="operations.html#hardening">part 08</a> allows exactly 80, 443, and the WireGuard port (51820, UDP) inbound, and nothing else — SSH is not publicly reachable at all; operators reach it only through the WireGuard tunnel, and WireGuard itself does not answer unauthenticated packets, so to a port scanner the box shows nothing but the web. <span class="status built">running today</span></p>
106
107<h2 id="routing">How a request is routed, step by step</h2>
108<ol>
109 <li>A request arrives for, say, <code>https://chat-a.example.com/api/version</code>. The router strips any port from the hostname and looks <code>chat-a.example.com</code> up in its in-memory copy of the registry.</li>
110 <li><strong>If the host is unknown</strong> (no <code>ready</code> row), the router answers 404 — or, if <code>LCHAT_FALLBACK_URL</code> is set in <code>/data/router.env</code>, redirects to that URL instead. The fallback pairs well with wildcard DNS: a mistyped or retired subdomain lands on your marketing site rather than an error page.</li>
111 <li><strong>If the host is a normal (systemd-managed) route</strong>, the router opens a connection to the row&rsquo;s unix socket and forwards the request, preserving the original hostname so the instance knows who it is.</li>
112 <li><strong>If the host is flagged for hibernation</strong>, the request is handed to the activator inside the router, which may need to wake the instance first — the whole story is <a href="hibernation.html">part 06</a>.</li>
113 <li><strong>If the box is in a mesh</strong> and the placement rule says another box owns this host, the request is forwarded to that box instead — <a href="mesh.html">part 07</a>.</li>
114</ol>
115
116<h3 id="restart-tolerance">Why deploys do not drop requests</h3>
117<p>When an instance restarts (during a <a href="deploys.html">deploy</a>), there is a brief moment when the old process has exited and the new one has not yet bound its socket. During that gap, the router&rsquo;s connection attempts fail — and the router responds by <strong>retrying the connection for up to thirteen seconds</strong> before giving up. Thirteen seconds comfortably exceeds an instance&rsquo;s shutdown drain (ten seconds) plus startup, so in practice a request arriving mid-deploy just waits a beat and then succeeds. This retry window is a load-bearing detail: it is what makes &ldquo;restart the instance&rdquo; a zero-downtime operation without any blue-green complexity.</p>
118<p>The router also quietly strips two internal headers from every request before forwarding it (<code>X-Lchat-Sidecar</code> and <code>X-Lchat-Mesh-Forwarded</code>), so no outside client can forge the markers that <a href="hibernation.html#sidecar-co-spawn">bot sidecars</a> and <a href="mesh.html#one-hop">mesh peers</a> use.</p>
119
120<h2 id="router-config">Configuring the router</h2>
121<p>The router reads optional configuration from <code>/data/router.env</code> (a plain <code>KEY=value</code> file; the unit declares it with a leading <code>-</code>, meaning the file may be absent). Everything it understands:</p>
122<div class="tablewrap">
123<table>
124 <tr><th>Variable</th><th>Default</th><th>Meaning</th></tr>
125 <tr><td><code>LCHAT_REGISTRY</code></td><td><code>/data/registry.db</code></td><td>Path to the registry database (also a <code>--registry</code> flag).</td></tr>
126 <tr><td><code>LCHAT_ACME_CACHE</code></td><td><code>/data/acme</code></td><td>Certificate cache directory (also <code>--acme-cache</code>).</td></tr>
127 <tr><td><code>LCHAT_HTTPS_ADDR</code> / <code>LCHAT_HTTP_ADDR</code></td><td><code>:443</code> / <code>:80</code></td><td>Listen addresses.</td></tr>
128 <tr><td><code>LCHAT_FALLBACK_URL</code></td><td>unset (404)</td><td>Where unknown hostnames are redirected.</td></tr>
129 <tr><td><code>LCHAT_WILDCARD_DOMAINS</code>, <code>LCHAT_DNSIMPLE_TOKEN</code></td><td>unset</td><td>Wildcard certificate issuance, described <a href="#dns01">above</a>.</td></tr>
130 <tr><td><code>LCHAT_HIBERNATE_BUCKET</code>, <code>LCHAT_HIBERNATE_REGION</code>, <code>LCHAT_HIBERNATE_PREFIX</code>, <code>LCHAT_HIBERNATE_IDLE</code></td><td>unset</td><td>The hibernation activator&rsquo;s master switch and tuning — explained fully in <a href="hibernation.html#activator-config">part 06</a>. Unset means the activator does not exist.</td></tr>
131 <tr><td><code>LCHAT_MESH_SELF</code>, <code>LCHAT_MESH_DOMAINS</code></td><td>unset</td><td>The mesh&rsquo;s master switch — explained fully in <a href="mesh.html#mesh-config">part 07</a>. Unset means zero mesh code runs.</td></tr>
132</table>
133</div>
134
135<h2 id="checking">Checking on the router</h2>
136<figure class="cmd">
137<figcaption>Run on the box — the three routine checks</figcaption>
138<pre><code># Is it running, and since when?
139systemctl status lchat-router
140
141# What has it logged recently? (-f follows live)
142journalctl -u lchat-router -n 100
143
144# Does it serve a known host end to end? (checks TLS, routing, and the instance)
145curl -s https://chat-a.example.com/api/version
146<span class="out">{"version":"a41c9f2","build":"1b4bd808"}</span></code></pre>
147</figure>
148<p>Because the activator runs inside the router, a hibernating fleet&rsquo;s wake/sleep log lines also appear in <code>journalctl -u lchat-router</code> — that journal is the single most informative log on the box. <a href="operations.html#reading-logs">Part 08</a> catalogues the log lines worth knowing.</p>
149
150<div class="why">
151 <span class="label">Why it is this way</span>
152 <p>The router is deliberately tiny and stateless: it holds no per-request state, its routing table is rebuilt from a file on disk, and systemd restarts it within a second if it crashes. It is a single point of failure for the box, but a much smaller and more stable one than any alternative — and every stateful, complicated thing (the instances) lives outside it in separate processes it merely connects to.</p>
153</div>
154
155</div>
156</main>
157</div>
158</body>
159</html>
160