Milestone 6: DiscoveryProvider seam, signed announcements, redoal sketch

The discovery module defines the seam: DiscoveryProvider (subscribe/
announce over 32-byte TopicKeys) and a signed Announcement carrying
root hash, ed25519 author, metadata and provider addresses. Signatures
cover a deterministic postcard encoding including the topic (no cross-
channel replay) and the provider identities (addresses stay refreshable
hints). LanDiscovery conforms to the trait — mdns sightings become
locally-authored announcements, one per pinned root — and the daemon's
auto-sync now runs entirely through it: verify, index unconditionally,
fetch only for trusted authors from allowlisted providers on already-
pinned incomplete roots. The milestone-4 real-mdns sync test passes
unchanged through the new path. Verification unit tests cover round
trip, tampered root, wrong topic, forged author, mismatched signing
key, and serde survival. docs/redoal-integration.md sketches the
gesture-topic gossip provider against this contract.

Also: fix a flaky hang in the socket-activation test (dup2(3,3) leaves
CLOEXEC set when the listener already sits on fd 3; parent's listener
copy masked daemon death), and give the test client a read-timeout hang
guard. Add a top-level README.

Dependencies: ed25519-dalek (Signature type; same implementation iroh
keys use), postcard (deterministic signed encoding, iroh's canonical
compact codec).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 09:51:57 +02:00
parent 871280553e
commit 258fa072aa
11 changed files with 617 additions and 13 deletions
+18 -3
View File
@@ -13,8 +13,11 @@ use varde_proto::{PinPolicy, Request, ResponseData};
extern "C" {
fn dup2(oldfd: i32, newfd: i32) -> i32;
fn fcntl(fd: i32, cmd: i32, arg: i32) -> i32;
}
const F_SETFD: i32 = 2;
/// Spawn the daemon with an activation socket on fd 3, the LISTEN_FDS
/// protocol systemd uses.
#[test]
@@ -38,17 +41,28 @@ fn systemd_socket_activation() {
.env("VARDE_DISCOVERY", "false")
.env("LISTEN_FDS", "1")
.env_remove("LISTEN_PID");
// SAFETY: dup2 is async-signal-safe; we place the listener on fd 3
// in the child, which also clears CLOEXEC on the duplicate.
// SAFETY: dup2/fcntl are async-signal-safe; we place the listener on
// fd 3 in the child. dup2 clears CLOEXEC on the duplicate, but when
// the listener already *is* fd 3, dup2(3,3) is a no-op that leaves
// CLOEXEC set and exec would close the socket — clear it explicitly.
unsafe {
command.pre_exec(move || {
if dup2(fd, 3) < 0 {
let rc = if fd == 3 {
fcntl(3, F_SETFD, 0)
} else {
dup2(fd, 3)
};
if rc < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
let mut child = command.spawn().expect("spawning daemon");
// Drop our copy of the listener: if the daemon dies, connects must
// fail (and this test fail cleanly) rather than queue in the backlog
// of a socket nobody accepts on.
drop(listener);
// The daemon must answer on the activation socket.
let deadline = Instant::now() + Duration::from_secs(10);
@@ -56,6 +70,7 @@ fn systemd_socket_activation() {
while Instant::now() < deadline {
if std::os::unix::net::UnixStream::connect(&socket_path).is_ok() {
let mut client = support::Client::connect(&socket_path);
client.set_read_timeout(Duration::from_secs(5));
let reply = client.request(&Request::Status { hash: None });
if reply.ok {
answered = true;
+5
View File
@@ -101,6 +101,11 @@ pub struct Client {
impl Client {
pub fn connect(socket: &Path) -> Client {
let stream = UnixStream::connect(socket).expect("connecting to daemon");
// Hang guard: a daemon that accepts but never answers should fail
// the test, not wedge the whole suite.
stream
.set_read_timeout(Some(Duration::from_secs(120)))
.expect("setting read timeout");
let reader = BufReader::new(stream.try_clone().expect("cloning stream"));
Client {
reader,