feat(bus): tines-bus transport and daemon over a host IPC bus
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
//! Shared helpers for the tines-bus integration tests.
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// A fresh, unique scratch directory under the system temp dir.
|
||||
pub fn unique_dir(tag: &str) -> PathBuf {
|
||||
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let dir = std::env::temp_dir().join(format!("tines-bus-{tag}-{}-{n}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
/// Spawn the bus daemon on a thread and return its socket path once bound.
|
||||
pub fn spawn_daemon(dir: &Path) -> PathBuf {
|
||||
let sock = dir.join("bus.sock");
|
||||
let s = sock.clone();
|
||||
std::thread::spawn(move || {
|
||||
if let Err(e) = tines_bus::daemon::serve(&s) {
|
||||
panic!("daemon failed: {e}");
|
||||
}
|
||||
});
|
||||
// Wait for the daemon to bind, so the first connect cannot race it.
|
||||
for _ in 0..400 {
|
||||
if sock.exists() {
|
||||
return sock;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
panic!("daemon did not bind in time");
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
//! Protocol-level tests driving raw Unix sockets straight at the daemon.
|
||||
mod common;
|
||||
|
||||
use common::{spawn_daemon, unique_dir};
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::time::{Duration, Instant};
|
||||
use tines_core::consts::MAX_FRAME;
|
||||
|
||||
type NodeId = [u8; 16];
|
||||
|
||||
fn connect(sock: &std::path::Path) -> UnixStream {
|
||||
let s = UnixStream::connect(sock).unwrap();
|
||||
s.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn send_envelope(s: &mut UnixStream, payload: &[u8]) {
|
||||
let mut buf = Vec::with_capacity(4 + payload.len());
|
||||
buf.extend_from_slice(&(payload.len() as u32).to_le_bytes());
|
||||
buf.extend_from_slice(payload);
|
||||
s.write_all(&buf).unwrap();
|
||||
}
|
||||
|
||||
fn hello(s: &mut UnixStream, id: &NodeId) {
|
||||
send_envelope(s, id);
|
||||
}
|
||||
|
||||
fn data(s: &mut UnixStream, from: &NodeId, to: &NodeId, frame: &[u8]) {
|
||||
let mut p = Vec::with_capacity(32 + frame.len());
|
||||
p.extend_from_slice(from);
|
||||
p.extend_from_slice(to);
|
||||
p.extend_from_slice(frame);
|
||||
send_envelope(s, &p);
|
||||
}
|
||||
|
||||
/// Read one envelope's payload; `Ok(None)` means EOF.
|
||||
fn read_envelope(s: &mut UnixStream) -> std::io::Result<Option<Vec<u8>>> {
|
||||
let mut len_buf = [0u8; 4];
|
||||
let n = s.read(&mut len_buf)?;
|
||||
if n == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
assert_eq!(n, 4, "short length header");
|
||||
let len = u32::from_le_bytes(len_buf) as usize;
|
||||
let mut payload = vec![0u8; len];
|
||||
s.read_exact(&mut payload)?;
|
||||
Ok(Some(payload))
|
||||
}
|
||||
|
||||
fn expect_control(s: &mut UnixStream, kind: u8, id: &NodeId) {
|
||||
let p = read_envelope(s)
|
||||
.unwrap()
|
||||
.expect("connection closed unexpectedly");
|
||||
assert_eq!(p.len(), 17, "control payload is kind + 16-byte id");
|
||||
assert_eq!(p[0], kind);
|
||||
assert_eq!(&p[1..17], id);
|
||||
}
|
||||
|
||||
/// Assert the socket stays silent for `window_ms`, then return.
|
||||
fn expect_no_data(s: &mut UnixStream, window_ms: u64) {
|
||||
s.set_read_timeout(Some(Duration::from_millis(100)))
|
||||
.unwrap();
|
||||
let deadline = Instant::now() + Duration::from_millis(window_ms);
|
||||
let mut buf = [0u8; 1];
|
||||
loop {
|
||||
match s.read(&mut buf) {
|
||||
Ok(0) => panic!("connection closed, expected silence"),
|
||||
Ok(_) => panic!("expected no data, got some"),
|
||||
Err(e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
|
||||
if Instant::now() >= deadline {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
Err(e) => panic!("unexpected read error: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_hello_replaces() {
|
||||
let dir = unique_dir("dup");
|
||||
let sock = spawn_daemon(&dir);
|
||||
|
||||
let mut s1 = connect(&sock);
|
||||
hello(&mut s1, &[1; 16]);
|
||||
let mut s2 = connect(&sock);
|
||||
hello(&mut s2, &[1; 16]);
|
||||
|
||||
// The first connection is closed by the daemon...
|
||||
let mut buf = [0u8; 1];
|
||||
assert_eq!(
|
||||
s1.read(&mut buf).unwrap(),
|
||||
0,
|
||||
"first connection must see EOF after replacement"
|
||||
);
|
||||
|
||||
// ...and the daemon stays up, serving the replacement: a third peer's
|
||||
// join reaches s2 as a control message.
|
||||
let mut s3 = connect(&sock);
|
||||
hello(&mut s3, &[2; 16]);
|
||||
expect_control(&mut s2, 0, &[2; 16]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mismatched_from_is_dropped() {
|
||||
let dir = unique_dir("mismatch");
|
||||
let sock = spawn_daemon(&dir);
|
||||
|
||||
let mut a = connect(&sock);
|
||||
hello(&mut a, &[1; 16]);
|
||||
let mut b = connect(&sock);
|
||||
hello(&mut b, &[2; 16]);
|
||||
|
||||
// A (registered as [1;16]) claims to be [2;16]: nothing may reach B.
|
||||
data(&mut a, &[2; 16], &[2; 16], &[0xAB; 10]);
|
||||
expect_no_data(&mut b, 300);
|
||||
|
||||
// The daemon survives and A is still connected: a well-formed message
|
||||
// is forwarded verbatim.
|
||||
data(&mut a, &[1; 16], &[2; 16], &[0xCD; 10]);
|
||||
let p = read_envelope(&mut b).unwrap().expect("connection closed");
|
||||
assert_eq!(p.len(), 42);
|
||||
assert_eq!(&p[0..16], &[1; 16]);
|
||||
assert_eq!(&p[16..32], &[2; 16]);
|
||||
assert_eq!(&p[32..42], &[0xCD; 10]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_dest_is_dropped() {
|
||||
let dir = unique_dir("unknown");
|
||||
let sock = spawn_daemon(&dir);
|
||||
|
||||
let mut a = connect(&sock);
|
||||
hello(&mut a, &[1; 16]);
|
||||
|
||||
// No [9;16] registered: dropped, and A stays connected.
|
||||
data(&mut a, &[1; 16], &[9; 16], &[0xAB; 10]);
|
||||
|
||||
let mut b = connect(&sock);
|
||||
hello(&mut b, &[2; 16]);
|
||||
// Wait until the daemon has registered B: A sees the join control.
|
||||
expect_control(&mut a, 0, &[2; 16]);
|
||||
data(&mut a, &[1; 16], &[2; 16], &[0xCD; 10]);
|
||||
let p = read_envelope(&mut b).unwrap().expect("connection closed");
|
||||
assert_eq!(p.len(), 42);
|
||||
assert_eq!(&p[32..42], &[0xCD; 10]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_frame_closes_connection() {
|
||||
let dir = unique_dir("oversize");
|
||||
let sock = spawn_daemon(&dir);
|
||||
|
||||
let mut a = connect(&sock);
|
||||
hello(&mut a, &[1; 16]);
|
||||
let mut b = connect(&sock);
|
||||
hello(&mut b, &[2; 16]);
|
||||
|
||||
// Handshake: once A has seen B join, both hellos are registered.
|
||||
expect_control(&mut a, 0, &[2; 16]);
|
||||
|
||||
// A frame one byte over MAX_FRAME: that connection is closed...
|
||||
data(&mut a, &[1; 16], &[2; 16], &vec![0u8; MAX_FRAME + 1]);
|
||||
let mut buf = [0u8; 1];
|
||||
assert_eq!(
|
||||
a.read(&mut buf).unwrap(),
|
||||
0,
|
||||
"oversized frame must close the connection"
|
||||
);
|
||||
|
||||
// ...and B learns A left; the daemon stays up for everyone else.
|
||||
expect_control(&mut b, 1, &[1; 16]);
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
//! End-to-end tests for the bus transport: real `BusTransport`s on both ends
|
||||
//! of a live daemon, exchanging core wire frames.
|
||||
mod common;
|
||||
|
||||
use common::{spawn_daemon, unique_dir};
|
||||
use std::path::Path;
|
||||
use std::time::{Duration, Instant};
|
||||
use tines_bus::BusTransport;
|
||||
use tines_core::{consts, Event, SendError, Transport};
|
||||
|
||||
type NodeId = [u8; 16];
|
||||
|
||||
/// A minimally valid wire frame declaring `from` as its sender:
|
||||
/// `WIRE_VERSION`, a variant tag byte, then the 16-byte sender id
|
||||
/// (`frame_sender` reads exactly those bytes).
|
||||
fn frame_from(from: &NodeId) -> Vec<u8> {
|
||||
let mut f = Vec::with_capacity(18);
|
||||
f.push(consts::WIRE_VERSION);
|
||||
f.push(0);
|
||||
f.extend_from_slice(from);
|
||||
f
|
||||
}
|
||||
|
||||
/// Poll `t` until `pred` accepts an event, or panic after `timeout`.
|
||||
fn poll_until(
|
||||
t: &mut BusTransport,
|
||||
mut pred: impl FnMut(&Event<NodeId>) -> bool,
|
||||
timeout: Duration,
|
||||
) -> Event<NodeId> {
|
||||
let deadline = Instant::now() + timeout;
|
||||
loop {
|
||||
if let Some(ev) = t.poll() {
|
||||
if pred(&ev) {
|
||||
return ev;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"timed out waiting for a bus event"
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert `t` yields no events for `window`, then give up.
|
||||
fn expect_quiet(t: &mut BusTransport, window: Duration) {
|
||||
let deadline = Instant::now() + window;
|
||||
while Instant::now() < deadline {
|
||||
assert!(t.poll().is_none(), "expected no events, got one");
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
|
||||
fn daemon_for(dir: &Path) -> std::path::PathBuf {
|
||||
spawn_daemon(dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_transports_exchange_frames() {
|
||||
let dir = unique_dir("xfer");
|
||||
let sock = daemon_for(&dir);
|
||||
let mut a = BusTransport::new([1; 16], None, &sock).unwrap();
|
||||
let mut b = BusTransport::new([2; 16], None, &sock).unwrap();
|
||||
|
||||
// A learns about B when B joins.
|
||||
match poll_until(
|
||||
&mut a,
|
||||
|ev| matches!(ev, Event::Contact(_)),
|
||||
Duration::from_secs(3),
|
||||
) {
|
||||
Event::Contact(id) => assert_eq!(id, [2; 16]),
|
||||
other => panic!("expected Contact, got {other:?}"),
|
||||
}
|
||||
|
||||
let frame = frame_from(&[1; 16]);
|
||||
a.send(&[2; 16], &frame).unwrap();
|
||||
|
||||
// B receives the frame from A with identical bytes.
|
||||
match poll_until(
|
||||
&mut b,
|
||||
|ev| matches!(ev, Event::Frame(_, _)),
|
||||
Duration::from_secs(3),
|
||||
) {
|
||||
Event::Frame(peer, fbuf) => {
|
||||
assert_eq!(peer, [1; 16]);
|
||||
assert_eq!(fbuf.as_slice(), frame.as_slice());
|
||||
}
|
||||
other => panic!("expected Frame, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spoofed_frame_is_dropped() {
|
||||
let dir = unique_dir("spoof");
|
||||
let sock = daemon_for(&dir);
|
||||
let mut a = BusTransport::new([1; 16], None, &sock).unwrap();
|
||||
let mut b = BusTransport::new([2; 16], None, &sock).unwrap();
|
||||
|
||||
poll_until(
|
||||
&mut a,
|
||||
|ev| matches!(ev, Event::Contact(_)),
|
||||
Duration::from_secs(3),
|
||||
);
|
||||
|
||||
// A (complicit) sends a frame that claims to be from C: the bus stamps
|
||||
// A as the envelope sender, so B must never surface it.
|
||||
let spoofed = frame_from(&[9; 16]);
|
||||
a.send(&[2; 16], &spoofed).unwrap();
|
||||
expect_quiet(&mut b, Duration::from_millis(400));
|
||||
|
||||
// The link is still healthy: a genuine frame from A arrives intact.
|
||||
let real = frame_from(&[1; 16]);
|
||||
a.send(&[2; 16], &real).unwrap();
|
||||
match poll_until(
|
||||
&mut b,
|
||||
|ev| matches!(ev, Event::Frame(_, _)),
|
||||
Duration::from_secs(3),
|
||||
) {
|
||||
Event::Frame(peer, fbuf) => {
|
||||
assert_eq!(peer, [1; 16]);
|
||||
assert_eq!(fbuf.as_slice(), real.as_slice());
|
||||
}
|
||||
other => panic!("expected Frame, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_and_leave_become_contact_and_lost() {
|
||||
let dir = unique_dir("joinleave");
|
||||
let sock = daemon_for(&dir);
|
||||
let mut a = BusTransport::new([1; 16], None, &sock).unwrap();
|
||||
let b = BusTransport::new([2; 16], None, &sock).unwrap();
|
||||
|
||||
match poll_until(
|
||||
&mut a,
|
||||
|ev| matches!(ev, Event::Contact(_)),
|
||||
Duration::from_secs(3),
|
||||
) {
|
||||
Event::Contact(id) => assert_eq!(id, [2; 16]),
|
||||
other => panic!("expected Contact, got {other:?}"),
|
||||
}
|
||||
|
||||
drop(b);
|
||||
match poll_until(
|
||||
&mut a,
|
||||
|ev| matches!(ev, Event::Lost(_)),
|
||||
Duration::from_secs(3),
|
||||
) {
|
||||
Event::Lost(id) => assert_eq!(id, [2; 16]),
|
||||
other => panic!("expected Lost, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allowlist_filters() {
|
||||
let dir = unique_dir("allow");
|
||||
let sock = daemon_for(&dir);
|
||||
let mut a = BusTransport::new([1; 16], Some([2; 16]), &sock).unwrap();
|
||||
let _b = BusTransport::new([2; 16], None, &sock).unwrap();
|
||||
let _c = BusTransport::new([3; 16], None, &sock).unwrap();
|
||||
|
||||
// Only the allowlisted peer surfaces.
|
||||
match poll_until(
|
||||
&mut a,
|
||||
|ev| matches!(ev, Event::Contact(_)),
|
||||
Duration::from_secs(3),
|
||||
) {
|
||||
Event::Contact(id) => assert_eq!(id, [2; 16]),
|
||||
other => panic!("expected Contact, got {other:?}"),
|
||||
}
|
||||
|
||||
// C's join is filtered out: nothing else surfaces.
|
||||
expect_quiet(&mut a, Duration::from_millis(400));
|
||||
|
||||
// And sending to a non-allowlisted peer is refused.
|
||||
let frame = frame_from(&[1; 16]);
|
||||
match a.send(&[3; 16], &frame) {
|
||||
Err(SendError::Unreachable) => {}
|
||||
other => panic!("expected Unreachable, got {other:?}"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user