feat(peer)!: cut over to authenticated catalog sharing

Replace address-only trust and pushed peer state with installation identities,
SPKI-pinned QUIC, candidate-only discovery, and bounded responder-owned
protocol-8 pulls. The runtime now owns each network generation and all admitted
work through shutdown.

Add exact bundled content identities, reproducible manifest publishing,
capability-confined downloads, streaming BLAKE3 verification, quarantine and
retry, and crash-recoverable download and install transactions. Ship generated
fixture catalogs and fail closed when production manifests are absent.

The Tauri backend exposes durable sharing policy, redacted identity state, and
attempt-keyed transfer snapshots. Frontend consumption follows in the next
commit. Repository-wide test certificates and protocol-7 paths are removed.

BREAKING CHANGE: peers must use protocol 8 and exact catalog content artifacts;
protocol-7 frames and shared-certificate identities are no longer accepted.

Test Plan:
- `just test` -- passed on the completed stack (708 workspace tests)
- `just clippy` -- passed on the completed stack
- `just build` -- passed with fixture catalogs on the completed stack
- `just catalog-check-production` -- failed closed because the external
  production manifest corpus is absent
- `git diff --cached --check` -- passed
This commit is contained in:
ddidderr committed 2026-08-10 13:59:18 +02:00
1 parent 36c4785775
commit 60fd7ba0c2
128 files changed
+51759 -10784

No files matched your search

+15 -22
View File
@@ -3,10 +3,10 @@
use std::{collections::HashMap, net::SocketAddr, time::Duration};
use lanspread_mdns::{DaemonEvent, LANSPREAD_SERVICE_TYPE, MdnsAdvertiser, MdnsMonitor};
use lanspread_proto::PROTOCOL_VERSION;
use lanspread_proto::{PROTOCOL_VERSION, PeerId};
use tokio_util::sync::CancellationToken;
use crate::{context::PeerCtx, network::select_advertise_ip};
use crate::{context::PeerCtx, network::select_advertise_ip, scoped_blocking::scoped_blocking};
pub(super) async fn start_mdns_advertiser(
ctx: &PeerCtx,
@@ -21,26 +21,29 @@ pub(super) async fn start_mdns_advertiser(
*guard = Some(advertise_addr);
}
let peer_id = ctx.peer_id.as_ref().clone();
let peer_id = ctx.peer_id;
let hostname = gethostname::gethostname().to_string_lossy().into_owned();
let advertised_name = advertised_service_name(&hostname, &peer_id);
let advertised_name = advertised_service_name(&hostname, peer_id);
let monitor_name = advertised_name.clone();
let properties = advertisement_properties(ctx, &hostname, &peer_id).await;
let properties = advertisement_properties(&hostname, peer_id);
let mdns = tokio::task::spawn_blocking(move || {
let mdns = scoped_blocking(move || {
MdnsAdvertiser::new(
LANSPREAD_SERVICE_TYPE,
&advertised_name,
advertise_addr,
Some(properties),
)
})
.await??;
})?;
log::info!("Registered mDNS service with name: {monitor_name}");
Ok(mdns)
}
pub(super) fn close_mdns_advertiser(advertiser: MdnsAdvertiser) -> eyre::Result<()> {
scoped_blocking(move || advertiser.close())
}
pub(super) async fn monitor_mdns_events(monitor: MdnsMonitor, shutdown: CancellationToken) {
loop {
let event = tokio::select! {
@@ -67,7 +70,8 @@ pub(super) async fn monitor_mdns_events(monitor: MdnsMonitor, shutdown: Cancella
}
}
fn advertised_service_name(hostname: &str, peer_id: &str) -> String {
fn advertised_service_name(hostname: &str, peer_id: PeerId) -> String {
let peer_id = peer_id.to_string();
let max_hostname_len = 63usize.saturating_sub(peer_id.len() + 1);
let truncated_hostname = if hostname.len() > max_hostname_len {
hostname.get(..max_hostname_len).unwrap_or(hostname)
@@ -76,27 +80,16 @@ fn advertised_service_name(hostname: &str, peer_id: &str) -> String {
};
if truncated_hostname.is_empty() {
peer_id.to_string()
peer_id
} else {
format!("{truncated_hostname}-{peer_id}")
}
}
async fn advertisement_properties(
ctx: &PeerCtx,
hostname: &str,
peer_id: &str,
) -> HashMap<String, String> {
let (library_rev, library_digest) = {
let library_guard = ctx.local_library.read().await;
(library_guard.revision, library_guard.digest)
};
fn advertisement_properties(hostname: &str, peer_id: PeerId) -> HashMap<String, String> {
let mut properties = HashMap::new();
properties.insert("peer_id".to_string(), peer_id.to_string());
properties.insert("proto_ver".to_string(), PROTOCOL_VERSION.to_string());
properties.insert("library_rev".to_string(), library_rev.to_string());
properties.insert("library_digest".to_string(), library_digest.to_string());
if !hostname.is_empty() {
properties.insert("hostname".to_string(), hostname.to_string());
}