feat: serve multiple domains with per-domain keys, ports and databases

This commit is contained in:
randogoth 2026-09-30 12:57:44 +03:00
parent 931933c423
commit dfb57b265c
8 changed files with 891 additions and 120 deletions

View file

@ -34,10 +34,50 @@ Add bunshin as a flake input and import the module:
}
```
`services.bunshin` also takes `host`, `port`, `dataDir`, `maxEnvelope`, `quota`, `requestsQuota`, `retentionDays`, `requestsRetentionDays`, `maxTokens`, `rateConnections`, `rateSends` and `rateTokens`; see `flake.nix` for defaults. The module renders a `systemd` unit that runs `bunshin serve` under `DynamicUser`; it does not generate a key.
`services.bunshin` also takes `host`, `port`, `dataDir`, `maxEnvelope`, `quota`, `requestsQuota`, `retentionDays`, `requestsRetentionDays`, `maxTokens`, `rateConnections`, `rateSends`, `rateTokens` and `domains`; see `flake.nix` for defaults. The module renders a `systemd` unit that runs `bunshin serve` under `DynamicUser`; it does not generate a key.
For the RNS carrier, build `packages.rns`, set `services.bunshin.package` to it, and enable `services.bunshin.rns` with its `keyFile`; see [RNS.md](RNS.md) for the protocol and the remaining options.
## Multiple domains
One process can serve several logical servers, each with its own static key, port and mailbox database. bunshin routes by port — the wire protocol has no domain field, and each domain's clients simply pin `user@host → host:port, public key`. How a `user@domain` address resolves to that `host:port` (published addresses, DNS SRV records, a proxy) is outside the server.
The agreed discovery convention is a DNS SRV record: `_smolmail._tcp.<domain>. SRV 0 1 <port> <host>`, lowest priority wins, and no record falls back to the default port 1961. SRV is discovery only — a lying record lands the client on a server with the wrong static key and the handshake aborts, so the pin stays out of band. Client-side only: `fumi` implements the lookup (rev `1468f3c`), pinning by `host:port` with the default port inheriting the bare-host pin and other ports trust-on-first-use; bunshin serves ports and pins keys, and needs nothing for it.
`serve --config <file>` takes a TOML file: one `[defaults]` table inherited by every `[domains.<name>]` table (names are 1-63 bytes of `[a-z0-9._-]`). Per domain, `key` and `port` are required; `db` defaults to `mail-<name>.db`, and any of the other settings can be overridden per domain:
```toml
[defaults]
host = "127.0.0.1"
quota = 67108864
[domains.example_org]
key = "/var/lib/bunshin/example_org.key"
port = 11961
[domains.example_net]
key = "/var/lib/bunshin/example_net.key"
port = 11962
invite_token_file = "/run/secrets/example_net-invite"
```
Every knob settable on the command line is settable in either table; unknown settings are rejected, not silently defaulted. `--config` cannot be combined with the single-domain flags or the `--rns-*` flags — the RNS carrier is single-domain, so it stays on the legacy flags. All keys are read and all ports bound before any domain serves, so a missing key or a taken port fails the process at startup.
On NixOS, set `services.bunshin.domains` instead; the module renders the TOML, gives each domain a database under `dataDir`, opens each domain's port with `openFirewall`, and loads per-domain invite token files via `LoadCredential`. The existing flat options keep serving the single-domain case unchanged.
```nix
services.bunshin = {
enable = true;
domains = {
example_org.keyFile = "/var/lib/bunshin/example_org.key";
example_org.port = 11961;
example_net.keyFile = "/var/lib/bunshin/example_net.key";
example_net.port = 11962;
example_net.inviteTokenFile = "/run/keys/example_net-invite";
};
};
```
Before the first deploy, generate the server's static key once (from a dev shell or `nix run`) and place it at the configured `keyFile`:
```