mod actor;
mod conn;
mod events;
pub mod mem;
mod seed;
mod transport;
use std::fmt;
use std::io;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use arc_swap::ArcSwap;
use tokio::sync::{mpsc, oneshot};
use tokio::task::{AbortHandle, JoinHandle};
pub use events::{Event, Events};
pub use kinship_core::{
CommandError, Config, ConfigError, Identity, Key, KeyError, KeyId, Limits, Member, Metrics,
Security, State, WIRE_VERSION,
};
pub use seed::{Seed, SeedError};
pub use transport::{TokioTransport, Transport, default_advertise};
use crate::actor::{Actor, Clock, KeyChange, Request, Shared, Snapshot};
use crate::events::Hub;
#[derive(Debug, Clone)]
pub struct Settings {
pub core: Config,
pub name: String,
pub advertise: Option<SocketAddr>,
pub meta: Vec<u8>,
pub seeds: Vec<SocketAddr>,
pub seed_hosts: Vec<Seed>,
pub resolve_timeout: Duration,
pub max_inbound_streams: usize,
pub max_inbound_streams_per_ip: usize,
pub max_inbound_bytes_per_ip: Option<usize>,
pub tcp_header_timeout: Duration,
pub event_buffer: usize,
}
impl Settings {
pub const DEFAULT_MAX_INBOUND_STREAMS: usize = 64;
pub const DEFAULT_MAX_INBOUND_STREAMS_PER_IP: usize = 16;
pub const DEFAULT_TCP_HEADER_TIMEOUT: Duration = Duration::from_secs(1);
pub const DEFAULT_EVENT_BUFFER: usize = 1024;
pub const DEFAULT_RESOLVE_TIMEOUT: Duration = Duration::from_secs(30);
pub fn new(core: Config, name: impl Into<String>) -> Self {
Self {
core,
name: name.into(),
advertise: None,
meta: Vec::new(),
seeds: Vec::new(),
seed_hosts: Vec::new(),
resolve_timeout: Self::DEFAULT_RESOLVE_TIMEOUT,
max_inbound_streams: Self::DEFAULT_MAX_INBOUND_STREAMS,
max_inbound_streams_per_ip: Self::DEFAULT_MAX_INBOUND_STREAMS_PER_IP,
max_inbound_bytes_per_ip: None,
tcp_header_timeout: Self::DEFAULT_TCP_HEADER_TIMEOUT,
event_buffer: Self::DEFAULT_EVENT_BUFFER,
}
}
pub fn inbound_bytes_per_ip(&self) -> usize {
self.max_inbound_bytes_per_ip.unwrap_or_else(|| {
self.core
.limits
.max_stream_frame
.saturating_add(FRAME_PREFIX_LEN)
})
}
pub fn validate(&self) -> Result<(), ConfigError> {
let fail = |field, reason| Err(ConfigError { field, reason });
if self.max_inbound_streams == 0 {
return fail("max_inbound_streams", "must be at least 1");
}
if self.max_inbound_streams_per_ip == 0 {
return fail("max_inbound_streams_per_ip", "must be at least 1");
}
let frame = self.core.limits.max_stream_frame;
if self.inbound_bytes_per_ip() < frame.saturating_add(FRAME_PREFIX_LEN) {
return fail(
"max_inbound_bytes_per_ip",
"must hold one max_stream_frame frame and its 4-byte length prefix",
);
}
if self.tcp_header_timeout.is_zero() {
return fail("tcp_header_timeout", "must be positive");
}
if self.event_buffer == 0 {
return fail("event_buffer", "must be at least 1");
}
Ok(())
}
}
const FRAME_PREFIX_LEN: usize = 4;
fn all_seeds(settings: &Settings) -> Vec<Seed> {
let addrs = settings.seeds.iter().copied().map(Seed::Addr);
addrs.chain(settings.seed_hosts.iter().cloned()).collect()
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Stats {
pub metrics: Metrics,
pub local_health: u32,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
Config(ConfigError),
Io(io::Error),
JoinFailed,
Unresolved,
MetaTooLarge,
Left,
Timeout,
Closed,
NotEncrypted,
KeyNotInstalled,
KeyInUse,
LastKey,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Config(e) => e.fmt(f),
Self::Io(e) => write!(f, "I/O error: {e}"),
Self::JoinFailed => f.write_str("no seed answered"),
Self::Unresolved => f.write_str("no seed resolved to an address"),
Self::MetaTooLarge => f.write_str("metadata larger than max_meta_bytes"),
Self::Left => f.write_str("this node has left the cluster"),
Self::Timeout => f.write_str("timed out"),
Self::Closed => f.write_str("the node is closed"),
Self::NotEncrypted => f.write_str("this node runs without encryption and has no keys"),
Self::KeyNotInstalled => f.write_str("key is not installed"),
Self::KeyInUse => f.write_str("key is the one in use"),
Self::LastKey => f.write_str("key is the last one installed"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Config(e) => Some(e),
Self::Io(e) => Some(e),
_ => None,
}
}
}
impl From<ConfigError> for Error {
fn from(e: ConfigError) -> Self {
Self::Config(e)
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Self::Io(e)
}
}
impl From<CommandError> for Error {
fn from(e: CommandError) -> Self {
match e {
CommandError::MetaTooLarge => Self::MetaTooLarge,
CommandError::JoinFailed => Self::JoinFailed,
CommandError::Left => Self::Left,
CommandError::NotEncrypted => Self::NotEncrypted,
CommandError::KeyNotInstalled => Self::KeyNotInstalled,
CommandError::KeyInUse => Self::KeyInUse,
CommandError::LastKey => Self::LastKey,
_ => Self::Closed,
}
}
}
pub fn generate_key() -> io::Result<Key> {
let mut bytes = [0u8; Key::LEN];
let drawn = getrandom::fill(&mut bytes).map_err(|e| io::Error::other(e.to_string()));
let key = drawn.map(|()| Key::from_bytes(bytes));
zeroize::Zeroize::zeroize(&mut bytes);
key
}
pub fn default_name() -> String {
let host = ["HOSTNAME", "COMPUTERNAME"]
.iter()
.find_map(|v| std::env::var(v).ok())
.or_else(|| std::fs::read_to_string("/etc/hostname").ok())
.map(|h| h.trim().to_owned())
.filter(|h| !h.is_empty())
.unwrap_or_else(|| "kinship".to_owned());
let mut end = host.len().min(57);
while !host.is_char_boundary(end) {
end -= 1;
}
let suffix = getrandom::u32().unwrap_or_default() & 0xff_ffff;
format!("{}-{suffix:06x}", &host[..end])
}
const REQUEST_QUEUE: usize = 256;
pub struct Memberlist {
requests: mpsc::Sender<Request>,
shared: Arc<Shared>,
local_addr: SocketAddr,
resolve_timeout: Duration,
task: Mutex<Option<JoinHandle<()>>>,
abort: AbortHandle,
}
impl Memberlist {
pub async fn start<T: Transport>(settings: Settings, transport: T) -> Result<Self, Error> {
let seeds = all_seeds(&settings);
let ml = Self::spawn(settings, transport)?;
if !seeds.is_empty() {
match ml.join_seeds(seeds).await {
Ok(n) => tracing::debug!(seeds = n, "joined"),
Err(Error::Closed) => return Err(Error::Closed),
Err(Error::Unresolved) => {
tracing::error!("no seed resolved; stopping");
ml.close().await;
return Err(Error::Unresolved);
}
Err(e) => {
tracing::warn!(error = %e, "startup join failed; retrying in the background")
}
}
}
Ok(ml)
}
fn spawn<T: Transport>(settings: Settings, transport: T) -> Result<Self, Error> {
settings.validate()?;
let seeds = all_seeds(&settings);
let resolve_timeout = settings.resolve_timeout;
let max_inbound_bytes_per_ip = settings.inbound_bytes_per_ip();
let local_addr = transport.local_addr();
let advertise = settings
.advertise
.unwrap_or_else(|| default_advertise(local_addr));
let me = kinship_core::Identity::new(settings.name, advertise)?.with_meta(settings.meta);
let os = |e: getrandom::Error| Error::Io(io::Error::other(e.to_string()));
let seed = getrandom::u64().map_err(os)?;
let mut nonce_key = [0u8; 32];
getrandom::fill(&mut nonce_key).map_err(os)?;
let clock = Clock::new();
let limits = conn::Limits {
tcp_timeout: settings.core.tcp_timeout,
max_stream_frame: settings.core.limits.max_stream_frame,
header_timeout: settings.tcp_header_timeout,
};
let rejoin_interval = settings.core.rejoin_interval;
let node = kinship_core::Node::new(settings.core, me, clock.now(), seed, &nonce_key);
zeroize::Zeroize::zeroize(&mut nonce_key);
let node = node?;
tracing::info!(name = node.local().name, addr = %advertise, bind = %local_addr, "node started");
let shared = Arc::new(Shared {
snapshot: ArcSwap::from_pointee(Snapshot::of(&node)),
key_ids: ArcSwap::from_pointee(node.key_ids()),
stats: Mutex::default(),
hub: Hub::new(settings.event_buffer),
});
let (requests, rx) = mpsc::channel(REQUEST_QUEUE);
let opts = actor::Options {
limits,
max_inbound_streams: settings.max_inbound_streams,
max_inbound_streams_per_ip: settings.max_inbound_streams_per_ip,
max_inbound_bytes_per_ip,
seeds,
rejoin_interval,
};
let actor = Actor::new(
node,
clock,
Arc::new(transport),
Arc::clone(&shared),
rx,
opts,
);
let task = tokio::spawn(actor.run());
Ok(Self {
requests,
shared,
local_addr,
resolve_timeout,
abort: task.abort_handle(),
task: Mutex::new(Some(task)),
})
}
pub fn local_addr(&self) -> SocketAddr {
self.local_addr
}
pub fn local(&self) -> Member {
self.shared.snapshot.load().local().clone()
}
pub fn members(&self) -> Vec<Member> {
let snapshot = self.shared.snapshot.load();
snapshot
.all
.iter()
.filter(|m| m.state.is_live())
.cloned()
.collect()
}
pub fn member(&self, name: &str) -> Option<Member> {
let snapshot = self.shared.snapshot.load();
snapshot.all.iter().find(|m| m.name == name).cloned()
}
pub fn events(&self) -> Events {
self.shared.hub.subscribe()
}
pub fn stats(&self) -> Stats {
self.shared.stats().clone()
}
pub fn keyring(&self) -> Keyring {
Keyring {
requests: self.requests.clone(),
shared: Arc::clone(&self.shared),
}
}
pub async fn join(&self, seeds: impl IntoIterator<Item = SocketAddr>) -> Result<usize, Error> {
self.join_seeds(seeds.into_iter().map(Seed::Addr)).await
}
pub async fn join_seeds(&self, seeds: impl IntoIterator<Item = Seed>) -> Result<usize, Error> {
let seeds: Vec<Seed> = seeds.into_iter().collect();
let seeds = seed::resolve_for_join(&seeds, self.resolve_timeout)
.await
.ok_or(Error::Unresolved)?;
let rx = self.call(|reply| Request::Join { seeds, reply }).await?;
rx.await.unwrap_or(Err(Error::Closed))
}
pub async fn set_meta(&self, meta: impl Into<Vec<u8>>) -> Result<(), Error> {
let meta = meta.into();
let rx = self.call(|reply| Request::SetMeta { meta, reply }).await?;
rx.await.unwrap_or(Err(Error::Closed))
}
pub async fn leave(&self, timeout: Duration) -> Result<(), Error> {
let rx = self.call(|reply| Request::Leave { reply }).await?;
match tokio::time::timeout(timeout, rx).await {
Ok(r) => r.unwrap_or(Err(Error::Closed)),
Err(_) => Err(Error::Timeout),
}
}
pub async fn close(&self) {
let (reply, done) = oneshot::channel();
if self.requests.send(Request::Close { reply }).await.is_ok() {
let _ = done.await;
}
let task = self.task().take();
if let Some(task) = task {
if let Err(e) = task.await {
if e.is_panic() {
tracing::error!("the node's actor panicked");
}
}
}
}
pub fn abort(&self) {
drop(self.task().take());
self.abort.abort();
}
#[cfg(any(test, feature = "test-hooks"))]
#[doc(hidden)]
pub fn panic_actor(&self) {
let _ = self.requests.try_send(Request::Panic);
}
pub fn is_closed(&self) -> bool {
self.requests.is_closed()
}
async fn call<R>(
&self,
make: impl FnOnce(oneshot::Sender<R>) -> Request,
) -> Result<oneshot::Receiver<R>, Error> {
let (reply, rx) = oneshot::channel();
self.requests
.send(make(reply))
.await
.map_err(|_| Error::Closed)?;
Ok(rx)
}
fn task(&self) -> std::sync::MutexGuard<'_, Option<JoinHandle<()>>> {
self.task.lock().unwrap_or_else(PoisonError::into_inner)
}
}
#[derive(Clone)]
pub struct Keyring {
requests: mpsc::Sender<Request>,
shared: Arc<Shared>,
}
impl Keyring {
pub async fn install(&self, key: Key) -> Result<(), Error> {
self.change(KeyChange::Install(key)).await
}
pub async fn use_key(&self, key: Key) -> Result<(), Error> {
self.change(KeyChange::Use(key)).await
}
pub async fn remove(&self, key: Key) -> Result<(), Error> {
self.change(KeyChange::Remove(key)).await
}
pub fn key_ids(&self) -> Vec<KeyId> {
self.shared.key_ids.load().as_ref().clone()
}
async fn change(&self, change: KeyChange) -> Result<(), Error> {
let (reply, rx) = oneshot::channel();
self.requests
.send(Request::Keyring { change, reply })
.await
.map_err(|_| Error::Closed)?;
rx.await.unwrap_or(Err(Error::Closed))
}
}
impl fmt::Debug for Keyring {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Keyring")
.field("key_ids", &self.key_ids())
.finish()
}
}
impl Drop for Memberlist {
fn drop(&mut self) {
if self.task().is_some() && !self.is_closed() {
if self.local().state != State::Left {
tracing::warn!("node dropped without leave() or close(); peers will see a crash");
}
self.abort.abort();
}
}
}
impl fmt::Debug for Memberlist {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Memberlist")
.field("local_addr", &self.local_addr)
.field("closed", &self.is_closed())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exposes_wire_version() {
assert_eq!(WIRE_VERSION, 1);
}
#[test]
fn default_names_are_valid_and_distinct() {
let a = default_name();
assert!(!a.is_empty() && a.len() <= 64, "{a}");
assert_ne!(a, default_name());
}
#[tokio::test]
async fn an_actor_panic_fails_pending_and_later_calls_and_marks_events_failed() {
const WAIT: Duration = Duration::from_secs(10);
let addr = |port: u8| SocketAddr::from(([10, 0, 0, port], 7946));
let net = mem::MemNetwork::new();
let _silent = net.bind(addr(9)).unwrap();
let cfg = Config::local(Security::InsecurePlaintext);
let transport = net.bind(addr(1)).unwrap();
let ml = Memberlist::start(Settings::new(cfg, "a"), transport)
.await
.unwrap();
let mut events = ml.events();
let join = ml.join([addr(9)]);
tokio::pin!(join);
assert!(
tokio::time::timeout(Duration::ZERO, &mut join)
.await
.is_err()
);
ml.panic_actor();
let pending = tokio::time::timeout(WAIT, join).await.unwrap();
assert!(matches!(pending, Err(Error::Closed)), "{pending:?}");
assert_eq!(
tokio::time::timeout(WAIT, events.recv()).await.unwrap(),
None
);
assert!(events.failed());
assert!(ml.is_closed());
let later = ml.set_meta(b"x".to_vec()).await;
assert!(matches!(later, Err(Error::Closed)), "{later:?}");
let later = ml.keyring().install(Key::from_bytes([1; 32])).await;
assert!(matches!(later, Err(Error::Closed)), "{later:?}");
assert!(
ml.events().failed(),
"a later subscription sees the failure too"
);
ml.close().await;
}
#[test]
fn generated_keys_are_random_and_round_trip() {
let a = generate_key().unwrap();
let b = generate_key().unwrap();
assert_ne!(a, b);
assert_eq!(Key::from_base64(&a.to_base64()), Ok(a));
}
}