Files
lanspread/crates/lanspread-peer/src/services/advertise.rs
T
ddidderr 60fd7ba0c2 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
2026-08-10 13:59:18 +02:00

98 lines
3.2 KiB
Rust

//! mDNS advertisement for the local peer server.
use std::{collections::HashMap, net::SocketAddr, time::Duration};
use lanspread_mdns::{DaemonEvent, LANSPREAD_SERVICE_TYPE, MdnsAdvertiser, MdnsMonitor};
use lanspread_proto::{PROTOCOL_VERSION, PeerId};
use tokio_util::sync::CancellationToken;
use crate::{context::PeerCtx, network::select_advertise_ip, scoped_blocking::scoped_blocking};
pub(super) async fn start_mdns_advertiser(
ctx: &PeerCtx,
server_addr: SocketAddr,
) -> eyre::Result<MdnsAdvertiser> {
let advertise_ip = select_advertise_ip()?;
let advertise_addr = SocketAddr::new(advertise_ip, server_addr.port());
log::info!("Advertising peer via mDNS from {advertise_addr}");
{
let mut guard = ctx.local_peer_addr.write().await;
*guard = Some(advertise_addr);
}
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 monitor_name = advertised_name.clone();
let properties = advertisement_properties(&hostname, peer_id);
let mdns = scoped_blocking(move || {
MdnsAdvertiser::new(
LANSPREAD_SERVICE_TYPE,
&advertised_name,
advertise_addr,
Some(properties),
)
})?;
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! {
() = shutdown.cancelled() => break,
event = monitor.recv_async() => event,
};
match event {
Ok(DaemonEvent::Error(err)) => {
log::error!("mDNS error: {err}");
tokio::select! {
() = shutdown.cancelled() => break,
() = tokio::time::sleep(Duration::from_secs(1)) => {}
}
}
Ok(other_event) => {
log::trace!("mDNS event: {other_event:?}");
}
Err(err) => {
log::debug!("mDNS monitor channel closed: {err}");
break;
}
}
}
}
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)
} else {
hostname
};
if truncated_hostname.is_empty() {
peer_id
} else {
format!("{truncated_hostname}-{peer_id}")
}
}
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());
if !hostname.is_empty() {
properties.insert("hostname".to_string(), hostname.to_string());
}
properties
}