prepared release
CI / check (push) Successful in 5m17s

This commit is contained in:
Ultradesu
2026-07-26 03:48:30 +03:00
parent a9012351dc
commit c6b2a066b4
36 changed files with 590 additions and 4801 deletions
+63 -184
View File
@@ -1,202 +1,81 @@
# federation-net
# frid
A reusable Rust library for direct peer-to-peer networking between equal
peers, built on [Iroh](https://iroh.computer). It establishes QUIC
connections that work through NATs (with hole punching and relay assistance),
and exchanges typed, application-defined messages over them.
Frid is the decentralized networking core behind Furumi. It provides reusable
Rust libraries for building equal-peer applications with no application
server, central peer registry, or privileged node.
The library contains **no domain logic** — no music, video, file libraries or
databases. The application defines its own message type; the engine treats it
as an opaque serde-serializable payload.
The workspace has two layers:
## What the first version does
- [`federation-net`](crates/federation-net) establishes authenticated,
NAT-traversing P2P connections over iroh, isolates independent networks and
message schemas, and optionally discovers peers through the BitTorrent
Mainline DHT.
- [`music-dht`](crates/music-dht) builds a Kademlia-style distributed music
directory on top of that transport, including catalog discovery, content-id
lookup, direct byte streams, and shared Furumi wire types.
* Persistent peer identity (`<data_dir>/identity.key`, created on first start).
* Connection establishment via a shareable string ticket (`fnet...`).
* Optional automatic peer discovery (**rendezvous**): peers of a network find
each other through the BitTorrent Mainline DHT knowing nothing but the
network id — no tickets and no bootstrap servers.
* An application-level handshake that isolates networks and schemas.
* Typed message exchange in both directions over one QUIC connection.
* Network events (connect, disconnect, message, protocol error).
* Graceful shutdown.
Each participant is a client, router, and storage peer. Automatic rendezvous
has no Frid-operated bootstrap service, while self-contained peer tickets
remain available for explicit or isolated connections.
## What it deliberately does not do (yet)
## Design principles
No gossip, broadcast overlay, content search, file/chunk/streaming transfer,
database sync, CRDTs, authorization, ACLs, HTTP APIs or metrics. The
architecture allows adding these later as separate modules or ALPN protocols.
- **Equal peers.** No node receives a permanent coordinator or server role.
- **Application-owned protocols.** The transport moves typed messages and
byte streams without owning domain state.
- **Bounded inputs.** Frames, tickets, routing tables, requests, and record
batches have explicit limits.
- **Offline-tolerant discovery.** Records are replicated, refreshed by their
owners, expired by TTL, and removed through revisioned tombstones.
- **Protocol isolation.** Network IDs, schema IDs, versions, and ALPNs prevent
unrelated or incompatible applications from being merged accidentally.
- **Local persistence.** Identity, routing knowledge, owned records, and
replicas survive restarts in application-controlled storage.
## Usage
See [ARCHITECTURE.md](ARCHITECTURE.md) for the protocol layers, trust model,
failure handling, and compatibility rules.
Define your own message type — any `serde`-serializable type works, no extra
traits to implement:
```rust
use federation_net::{NetworkConfig, NetworkEngine, NetworkEvent, NetworkId, SchemaId};
#[derive(Debug, serde::Serialize, serde::Deserialize)]
enum DemoMessage {
Text { sender: String, body: String },
Ping { nonce: u64 },
}
#[tokio::main]
async fn main() -> federation_net::Result<()> {
let config = NetworkConfig::builder()
.data_dir("./peer-a")
.network_id(NetworkId::from_name("example-network"))
.schema_id(SchemaId::from_name("demo-message-v1"))
.build()?;
let (engine, mut events) = NetworkEngine::<DemoMessage>::start(config).await?;
// Share this string with another peer out of band.
println!("Ticket: {}", engine.ticket().await?);
while let Some(event) = events.recv().await {
match event {
NetworkEvent::PeerConnected { peer_id, .. } => {
engine
.send(peer_id, &DemoMessage::Ping { nonce: 1 })
.await?;
}
NetworkEvent::MessageReceived { peer_id, message } => {
println!("{peer_id}: {message:?}");
}
_ => {}
}
}
engine.shutdown().await
}
```
## Running the demo
Start the first peer:
```bash
cargo run -p federation-net-demo -- \
--data-dir ./tmp/peer-a \
--network-id demo-network \
--name alice
```
It prints its endpoint id and a ticket:
## Workspace
```text
Endpoint ID: ...
Network ID: ...
Schema ID: ...
Ticket: fnet...
Waiting for peers...
crates/
federation-net/ generic iroh transport and rendezvous
music-dht/ distributed music catalog and content discovery
apps/
federation-net-demo/ small interactive transport example
```
Start the second peer with that ticket:
## Using the libraries
Until crates are published to a registry, depend on the repository directly:
```toml
[dependencies]
federation-net = { git = "https://gt.hexor.cy/ab/frid.git" }
music-dht = { git = "https://gt.hexor.cy/ab/frid.git" }
```
Start with the crate documentation:
- [`federation-net` guide](crates/federation-net/README.md)
- [`music-dht` guide](crates/music-dht/README.md)
## Development
Frid uses Rust edition 2024 and requires Rust 1.91 or newer.
```bash
cargo run -p federation-net-demo -- \
--data-dir ./tmp/peer-b \
--network-id demo-network \
--name bob \
--connect 'fnet...'
```
Type a line and press Enter to send it to all connected peers. Commands:
`/peers` lists connections, `/ticket` prints your ticket, `/ping` sends a
ping, `/quit` (or Ctrl+C) shuts down gracefully.
If the second peer uses a different `--network-id`, the connection is refused
with `Connection rejected: network id mismatch`; an incompatible schema is
refused with `Connection rejected: schema id mismatch`.
## NetworkId
A `NetworkId` identifies one distinct P2P network. It is 32 bytes, derived
deterministically from a name: `BLAKE3("federation-net:network:" + name)`.
The same name always yields the same id. Peers whose network ids differ
refuse to establish an application-level session, even though they share the
same transport protocol — this isolates independent deployments from each
other.
## SchemaId
A `SchemaId` identifies the wire format of the domain message type, derived
as `BLAKE3("federation-net:schema:" + name)` (e.g. `music-domain-v1`,
`demo-chat-v1`). Peers on the same network but with different schema ids
reject each other, because they could not decode each other's messages. Any
backwards-incompatible change to your message type requires a new schema
name.
## Rendezvous (peer discovery by network id)
Passing a `RendezvousConfig` to the config builder enables automatic peer
discovery: every peer periodically publishes its own Iroh address into a
shared BEP44 mutable record in the public BitTorrent Mainline DHT and dials
the addresses other peers published there. The record's signing key is
derived deterministically from the `NetworkId`, so knowing the network id is
enough to find and join the network; the first peer of a new network simply
publishes itself and waits.
```rust
let config = NetworkConfig::builder()
.data_dir("./peer-a")
.network_id(NetworkId::from_name("example-network"))
.schema_id(SchemaId::from_name("demo-message-v1"))
.rendezvous(federation_net::RendezvousConfig::default())
.build()?;
```
Because the rendezvous record is world-readable, the network id acts as a
public rendezvous token: anyone who knows it can discover and join the
network. Treat the id of a private network like a shared secret (e.g.
`myorg-prod-7f3a`); the handshake still rejects peers whose network id does
not match exactly. Rendezvous is off by default — without it, peers are
connected via tickets only.
## Tickets
A `PeerTicket` is a self-contained invitation string with the `fnet` prefix
(base32-encoded postcard, versioned). It carries the peer's Iroh address
(endpoint id, relay URL and direct addresses) plus the network id, schema id
and protocol version, so incompatibility is detected before any message is
exchanged. Tickets implement `Display`/`FromStr` and round-trip through their
string form. The remote peer's identity is always taken from the
authenticated Iroh connection, never trusted from the ticket payload.
## Events
The engine reports what happens on the network through a single bounded
event channel (`NetworkEventReceiver`):
* `PeerConnected { peer_id, direction }` — a handshake completed
(`Incoming` or `Outgoing`).
* `PeerDisconnected { peer_id, reason }` — a connection closed.
* `MessageReceived { peer_id, message }` — a domain message arrived and was
decoded into your type.
* `ProtocolError { peer_id, error }` — a per-connection error; the engine
itself keeps running.
Consume events promptly: the channel is bounded and the engine applies
back-pressure instead of buffering without limit.
## Example: distributed search (`artist-dht`)
The workspace also contains a bigger example built entirely on this library:
[`crates/artist-dht`](crates/artist-dht/README.md) — a Kademlia-style DHT
where every peer stores, routes and searches artist records without any
dedicated servers, plus its interactive CLI
[`apps/artist-dht-cli`](apps/artist-dht-cli). See its README for the
three-peer demo scenario.
## Verification
```bash
cargo fmt --check
cargo fmt --all -- --check
cargo check --workspace --all-targets --all-features
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace
```
The integration tests establish real connections between two engines in one
process (they may use Iroh's public relay/discovery infrastructure), so they
need network access and take a few seconds.
The integration tests open real local sockets and establish iroh connections.
Some rendezvous tests also use public discovery infrastructure, so a restricted
sandbox may need explicit network permission.
## License
Frid is released under the
[Do What The Fuck You Want To Public License, Version 2](LICENSE).