The protocol link is a different kind of module from the service pseudo-clients, so give it its own home: modules/inspircd -> modules/protocol/inspircd. A future ircd link is a new crate alongside it. Workspace member, dependency path, the crate's own api path, and the docs are updated to match. No code change.
115 lines
5.1 KiB
Markdown
115 lines
5.1 KiB
Markdown
# fedserv
|
|
|
|
A federated IRC services daemon in Rust. Protocol-agnostic core, InspIRCd link first.
|
|
|
|
Clean-room, inspired by the ideas behind modern event-log/gossip services, not
|
|
derived from any project's source.
|
|
|
|
## Design
|
|
|
|
The core lives in `src/`. The pluggable parts are separate crates in a Cargo
|
|
workspace, each depending only on the `fedserv-api` SDK crate.
|
|
|
|
- **`api/`** (`fedserv-api`) the module SDK: the `Service`, `Protocol`, `Store`
|
|
and `NetView` traits a module implements or is handed, plus the normalized
|
|
`NetEvent`/`NetAction` vocabulary and the read views. It has no storage or
|
|
runtime dependencies, so a third-party module builds against it alone.
|
|
- **`src/engine/`** the services engine: live network `state`, the account store,
|
|
and the log. The store is event-sourced: every change is an `Event` appended to
|
|
a per-node log, and state is a fold over that log. A service touches it only
|
|
through the `Store`/`NetView` traits, so the log, gossip and credential material
|
|
stay out of a module's reach.
|
|
- **`src/gossip.rs`** node-to-node replication over the logs.
|
|
- **`modules/`** the loadable modules, each its own crate depending only on the
|
|
`fedserv-api` SDK:
|
|
- **`modules/protocol/inspircd/`** (`fedserv-inspircd`) the ircd link layer. A
|
|
`Protocol` impl maps raw server-to-server lines to and from the normalized
|
|
model, so the engine never touches a raw line and a new ircd is one new crate
|
|
under `modules/protocol/`.
|
|
- **`modules/nickserv/`, `modules/chanserv/`, `modules/botserv/`,
|
|
`modules/memoserv/`, `modules/statserv/`, `modules/hostserv/`, `modules/operserv/`**
|
|
and diceserv, infoserv, reportserv, groupserv, chanfix, helpserv — the
|
|
pseudo-clients, each a crate implementing `Service`. OperServ holds the
|
|
network-operator tools (AKILL network bans to start); the rest follow the same
|
|
shape.
|
|
|
|
Writing your own module — a service or a new ircd protocol — is documented in
|
|
[MODULES.md](MODULES.md); `modules/example/` is a minimal service to copy from.
|
|
|
|
## Replication
|
|
|
|
Each log entry carries an `origin`, a per-origin `seq`, and a Lamport clock. Peers
|
|
connect, exchange version vectors, and send each other the entries the other
|
|
lacks; a freshly committed entry is also pushed immediately, so a registration
|
|
propagates in milliseconds. Ingest is idempotent, so re-delivery and reconnect
|
|
after a split both converge. The log is compacted to one entry per account as it
|
|
grows. An account registered on any node works on all of them.
|
|
|
|
## Directory API (gRPC)
|
|
|
|
**`src/grpc.rs`** exposes the account/channel directory over gRPC (see
|
|
`proto/fedserv.proto`) so a website can mirror it without touching IRC: a
|
|
one-shot `Snapshot` for the initial load, then `Subscribe` for a live stream of
|
|
every change from that point on. It subscribes to the same committed-entry
|
|
channel gossip does, so a change reaches a subscriber in the same push, no
|
|
polling. Deliberately excludes anything credential-shaped (password hashes,
|
|
SCRAM verifiers, cert fingerprints) and the finer channel-ops-list events
|
|
(access/akick/mlock/entrymsg) — identity and metadata only. Bearer-token
|
|
authenticated, optional server TLS. Omit `[grpc]` in config.toml to run
|
|
without it.
|
|
|
|
The same port also serves **`Accounts`**, the write side: `Register`,
|
|
`Authenticate`, `SetPassword`, `SetEmail`, `Confirm`, `Drop`, `ForceLogout`,
|
|
`GroupNick`, `UngroupNick` — a trusted caller (e.g. a website's own backend,
|
|
already having done its own login/session check) managing accounts the same
|
|
way an IRC user does through NickServ, minus the command syntax. The bearer
|
|
token *is* the authorization: unlike the NickServ commands these mirror, most
|
|
calls do not re-check the account's own password (an admin-level override, the
|
|
same trust model a JSON-RPC integration to another services package would
|
|
use) — `Register` and `Authenticate` are the two exceptions, since the
|
|
password is the actual input there. A write committed this way replicates to
|
|
every other fedserv node exactly like an IRC-originated one does — gossip
|
|
doesn't know or care where it came from.
|
|
|
|
## Config
|
|
|
|
```toml
|
|
[uplink]
|
|
host = "127.0.0.1"
|
|
port = 7000
|
|
password = "linkpw"
|
|
|
|
[server]
|
|
name = "services.example"
|
|
sid = "42S"
|
|
|
|
[gossip] # omit to run a single, non-replicating node
|
|
bind = "0.0.0.0:16700"
|
|
secret = "shared-secret"
|
|
|
|
[gossip.tls] # omit for a plaintext link
|
|
cert = "certs/nodeA.crt"
|
|
key = "certs/nodeA.key"
|
|
ca = "certs/ca.crt" # a peer must present a certificate signed by this CA
|
|
|
|
[[peer]]
|
|
addr = "nodeB.example:16700"
|
|
name = "nodeB" # TLS name to expect; must match the peer certificate
|
|
```
|
|
|
|
Generate certificates with `scripts/gen-certs.sh`.
|
|
|
|
## Run
|
|
|
|
```sh
|
|
cp config.example.toml config.toml # edit uplink + link password
|
|
cargo run -- config.toml
|
|
```
|
|
|
|
## Status
|
|
|
|
Links to an InspIRCd uplink and runs NickServ (REGISTER, IDENTIFY, LOGOUT, CERT,
|
|
and SASL PLAIN / SCRAM-SHA-256/512 / EXTERNAL) and ChanServ over an event-sourced
|
|
account store replicated between nodes by gossip, with an optional mutually
|
|
authenticated TLS peer link, plus a gRPC directory API for websites to mirror
|
|
the account/channel directory (bearer-token authenticated, optional TLS).
|