130 lines
8.6 KiB
Markdown
130 lines
8.6 KiB
Markdown
# 分身 bunshin
|
|
|
|
[](LICENSE) [](https://ai-declaration.md) 
|
|
|
|
A Rust implementation of the [Smol Mail](https://code.randogoth.com/randogoth/smolmail) server: a minimalist, end-to-end encrypted mail protocol over a Noise-secured TCP connection, with an optional Reticulum (RNS) mesh carrier behind the `rns` build feature. `bunshin` implements the server side only — receiving, storing and serving sealed mail — not the client.
|
|
|
|
The server never sees plaintext, sender identities or any private key. It learns only which mailbox an envelope is for, its size, and when it arrived.
|
|
|
|
The flake's main purpose is turnkey deployment on a NixOS host: import `nixosModules.default`, point it at a key, and `nixos-rebuild switch`.
|
|
|
|
## Deploying on NixOS
|
|
|
|
Add bunshin as a flake input and import the module:
|
|
|
|
```nix
|
|
{
|
|
inputs.bunshin.url = "https://code.randogoth.com/randogoth/bunshin";
|
|
|
|
outputs = { self, nixpkgs, bunshin, ... }: {
|
|
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
|
|
modules = [
|
|
bunshin.nixosModules.default
|
|
{
|
|
services.bunshin = {
|
|
enable = true;
|
|
keyFile = "/var/lib/bunshin/server.key"; # provisioned out of band, see below
|
|
openFirewall = true;
|
|
inviteTokenFile = "/run/secrets/bunshin-invite"; # or inviteToken directly
|
|
};
|
|
}
|
|
];
|
|
};
|
|
};
|
|
}
|
|
```
|
|
|
|
`services.bunshin` also takes `host`, `port`, `dataDir`, `maxEnvelope`, `quota`, `requestsQuota`, `retentionDays`, `requestsRetentionDays`, `maxTokens`, `rateConnections`, `rateSends`, `rateTokens`, `maxConnections` 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`:
|
|
|
|
```
|
|
nix run "https://code.randogoth.com/randogoth/bunshin" -- keygen --key server.key
|
|
```
|
|
|
|
`keygen` writes the server's static X25519 key (used for the Noise handshake, distinct from any user's Ed25519 identity) and prints its public key in base32. Publish that public key through a trusted channel — clients pin it, and a mismatch aborts the handshake.
|
|
|
|
## RNS carrier
|
|
|
|
The `rns` feature (built by `packages.rns`, wired to the module through `services.bunshin.rns`) serves the same mailbox over the Reticulum Network Stack alongside TCP: `bunshin serve --rns` runs both carriers in one process against one database. The mesh address is the `smolmail.server` destination hash — there is no key to pin, the address is the pin.
|
|
|
|
```
|
|
bunshin rns-keygen --key server.rns.key
|
|
bunshin serve --rns --key server.key --rns-key server.rns.key --db mail.db --rns-udp 0.0.0.0:4242
|
|
```
|
|
|
|
`rns-keygen` writes the 64-byte Reticulum identity and prints the destination hash; publish `smol+rns://<user>@<hash>` as the server's address. RNS-specific limits (`--rns-max-envelope`, `--rns-fetch-budget`, `--rns-max-links`, `--rns-rate-link-requests`, `--rns-rate-link-bytes`) default to mesh-friendly 32 KiB caps; `--rns-link-idle` (default 300 s) reaps links whose client went silent, since nothing else times a link out; the UDP interface is the one supported transport interface so far. See [RNS.md](RNS.md).
|
|
|
|
## Building
|
|
|
|
Requires a Rust toolchain (stable, edition 2021) and a C compiler — `rusqlite`'s `bundled` feature compiles SQLite from source rather than linking a system copy, so no separate SQLite install is needed. The `rns` feature additionally needs cmake, ninja and a microReticulum checkout pointed at by `MICRORETICULUM_SOURCE_DIR` — the flake's dev shell provides all of it.
|
|
|
|
```
|
|
cargo build --release
|
|
cargo test
|
|
```
|
|
|
|
The binary lands at `target/release/bunshin`. With Nix, skip the toolchain setup entirely: `nix build` produces the same binary at `./result/bin/bunshin`.
|
|
|
|
Running directly, outside the NixOS module:
|
|
|
|
```
|
|
bunshin keygen --key server.key
|
|
bunshin serve --key server.key --db mail.db --host 0.0.0.0 --port 1961
|
|
```
|
|
|
|
`serve` accepts `--max-envelope`, `--quota`, `--requests-quota`, `--retention-days`, `--requests-retention-days`, `--max-tokens`, `--invite-token`, `--rate-connections`, `--rate-sends`, `--rate-tokens` and `--max-connections` to control size limits, the mailbox's two quota tiers, their retention, the accept-token cap, registration gating, abuse control and the concurrent-connection cap. Rate limits are sliding-window: hits age out 60 s after they happen, so a burst straddling a window boundary cannot exceed the configured rate. Connections past `--max-connections` (default 100, 0 = unlimited) are refused, mirroring the RNS carrier's link cap. Run `bunshin serve --help` for defaults.
|
|
|
|
`--verbose` (or `services.bunshin.verbose` in the NixOS module) raises logging to debug level, which adds server-side metrics on both carriers: per-operation timing, request and response sizes, handshake duration, per-session summaries, and RNS link events. The default info level stays quiet on success.
|
|
|
|
## Status
|
|
|
|
Implements SPEC.md version 1.1 in full: `AUTH`, `RESOLVE`, `SEND`, `FETCH`, `DELETE` and `REGISTER`, including accept tokens and the main/requests tier split, fetch cursors, 32-byte message ids, `REGISTER` proof of possession, and dual-signed key rotation chains.
|
|
|
|
With the `rns` feature, implements the version 1.2 RNS transport: the same six operations over Reticulum Links to an announced `smolmail.server` destination, with link-bound AUTH/REGISTER signatures, per-link request and byte limits, and a concurrent-link cap. One server process can serve both carriers over one store.
|
|
|
|
## License
|
|
|
|
Licensed under the Apache License, Version 2.0 ([LICENSE](LICENSE)). Third-party dependency licenses are listed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|