#![cfg(feature = "local")]
use kunobi_daemon::local::peer::PeerCredentials;
use kunobi_daemon::peer::{
Credentials, ExpectedProcess, First, Rejected, SameUser, UserEvidence, authenticate,
};
use kunobi_daemon::{Candidate, ProcessId};
use std::{
io::{Read, Write},
process::{Command, Stdio},
sync::mpsc,
time::{Duration, Instant},
};
const BUDGET: Duration = Duration::from_secs(20);
const ENDPOINT_ENV: &str = "KUNOBI_DAEMON_PEER_TEST_ENDPOINT";
const SERVER_PID_ENV: &str = "KUNOBI_DAEMON_PEER_TEST_SERVER";
fn own_pid() -> ProcessId {
ProcessId::new(std::process::id()).unwrap()
}
struct Reaped(Candidate);
impl Reaped {
fn spawn(endpoint: &str) -> Self {
let child = Command::new(std::env::current_exe().unwrap())
.args(["--exact", "peer_fixture_process", "--ignored"])
.env(ENDPOINT_ENV, endpoint)
.env(SERVER_PID_ENV, std::process::id().to_string())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap();
Self(Candidate::new(child))
}
fn pid(&self) -> ProcessId {
self.0.id()
}
fn assert_succeeds(&mut self) {
let status = self
.0
.wait_until(Instant::now() + BUDGET)
.unwrap()
.expect("the child did not exit");
assert!(status.success(), "the child's own checks failed: {status}");
}
}
#[test]
#[ignore = "child-process entry point, invoked by the evidence tests"]
fn peer_fixture_process() {
let endpoint = std::env::var(ENDPOINT_ENV).unwrap();
let server: u32 = std::env::var(SERVER_PID_ENV).unwrap().parse().unwrap();
let mut stream = connect(&endpoint);
let server_evidence = client_evidence(&stream);
assert_eq!(
(
server_evidence.pid,
server_evidence.same_user,
server_evidence.user_evidence
),
(ProcessId::new(server), true, CLIENT_USER_EVIDENCE)
);
let accepted = authenticate(
&stream,
server_evidence,
&ExpectedProcess::new(move || ProcessId::new(server)),
);
assert!(accepted.is_ok(), "the child refused its server");
stream.write_all(&std::process::id().to_le_bytes()).unwrap();
let _ = stream.read(&mut [0u8; 1]);
}
const CLIENT_USER_EVIDENCE: UserEvidence = if cfg!(windows) {
UserEvidence::PidLookup
} else {
UserEvidence::Connection
};
fn check_accepted(peer: Credentials, child: ProcessId, announced: [u8; 4]) {
assert_eq!(
(peer.pid, peer.same_user, peer.user_evidence),
(Some(child), true, UserEvidence::Connection)
);
assert_eq!(peer.pid, ProcessId::new(u32::from_le_bytes(announced)));
assert_ne!(peer.pid, Some(own_pid()));
let tiers = |expected: Option<ProcessId>| {
First::new()
.then(ExpectedProcess::new(move || expected), "full")
.then(SameUser, "redacted")
};
assert_eq!(
authenticate((), peer.clone(), &tiers(Some(child))).map(|a| *a.grant()),
Ok("full")
);
assert_eq!(
authenticate((), peer.clone(), &tiers(Some(own_pid()))).map(|a| *a.grant()),
Ok("redacted")
);
assert_eq!(
authenticate((), peer, &ExpectedProcess::new(|| Some(own_pid()))).unwrap_err(),
Rejected::OtherProcess {
expected: own_pid(),
observed: child
}
);
}
fn accept_within_budget<S: Send + 'static>(
accept: impl FnOnce() -> std::io::Result<S> + Send + 'static,
) -> S {
let (sent, accepted) = mpsc::channel();
std::thread::spawn(move || {
let _ = sent.send(accept());
});
accepted
.recv_timeout(BUDGET)
.expect("the child did not connect")
.unwrap()
}
#[cfg(unix)]
mod platform {
use super::*;
use std::os::unix::net::{UnixListener, UnixStream};
pub(super) fn connect(endpoint: &str) -> UnixStream {
let stream = UnixStream::connect(endpoint).unwrap();
stream.set_read_timeout(Some(BUDGET)).unwrap();
stream
}
pub(super) fn client_evidence(stream: &UnixStream) -> Credentials {
stream.credentials().unwrap()
}
#[test]
fn both_ends_of_a_unix_socket_see_the_other_process() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("peer.sock");
let listener = UnixListener::bind(&path).unwrap();
let mut child = Reaped::spawn(path.to_str().unwrap());
let mut stream = accept_within_budget(move || listener.accept().map(|(s, _)| s));
stream.set_read_timeout(Some(BUDGET)).unwrap();
let mut announced = [0u8; 4];
stream.read_exact(&mut announced).unwrap();
check_accepted(stream.credentials().unwrap(), child.pid(), announced);
drop(stream);
child.assert_succeeds();
}
}
#[cfg(windows)]
mod platform {
use super::*;
use interprocess::local_socket::traits::{Listener as _, Stream as _};
use interprocess::local_socket::{GenericNamespaced, Stream, ToNsName};
use kunobi_daemon::local::{Bound, socket::acquire};
use std::path::Path;
pub(super) fn connect(endpoint: &str) -> Stream {
Stream::connect(endpoint.to_ns_name::<GenericNamespaced>().unwrap()).unwrap()
}
pub(super) fn client_evidence(stream: &Stream) -> Credentials {
stream.credentials().unwrap()
}
#[test]
fn both_ends_of_a_named_pipe_see_the_other_process() {
let unique = tempfile::tempdir().unwrap();
let name = format!(
"daemon-peer-{}-{}",
std::process::id(),
unique.path().file_name().unwrap().to_string_lossy()
);
let Bound::Won(listener) = acquire(Path::new(&name)).unwrap() else {
panic!("first owner")
};
let mut child = Reaped::spawn(&name);
let mut stream = accept_within_budget(move || listener.accept());
let mut announced = [0u8; 4];
stream.read_exact(&mut announced).unwrap();
check_accepted(stream.credentials().unwrap(), child.pid(), announced);
drop(stream);
child.assert_succeeds();
}
}
use platform::{client_evidence, connect};