use std::collections::HashMap;
use std::fmt;
use std::net::SocketAddr;
use iroh::{EndpointAddr, EndpointId, SecretKey, TransportAddr};
use serde::{Deserialize, Serialize};
use super::net::NodeId;
#[derive(Debug, Clone, Serialize, Deserialize, Eq)]
pub struct NodeIdentity {
pub name: String,
pub endpoint_id: NodeId,
pub host: String,
pub port: u16,
pub incarnation: u64,
}
impl NodeIdentity {
pub fn new(
name: impl Into<String>,
endpoint_id: EndpointId,
host: impl Into<String>,
port: u16,
) -> Self {
Self {
name: name.into(),
endpoint_id: NodeId(endpoint_id.to_string()),
host: host.into(),
port,
incarnation: rand::random(),
}
}
pub fn new_seeded(
name: impl Into<String>,
id: NodeId,
host: impl Into<String>,
port: u16,
incarnation: u64,
) -> Self {
Self {
name: name.into(),
endpoint_id: id,
host: host.into(),
port,
incarnation,
}
}
pub fn socket_addr(&self) -> SocketAddr {
let ip: std::net::IpAddr = self.host.parse().unwrap_or([127, 0, 0, 1].into());
SocketAddr::new(ip, self.port)
}
pub fn endpoint_addr(&self) -> EndpointAddr {
let id: EndpointId = self
.endpoint_id
.0
.parse()
.expect("NodeIdentity::endpoint_addr called on a non-iroh node id");
EndpointAddr::from_parts(id, [TransportAddr::Ip(self.socket_addr())])
}
pub fn node_id_string(&self) -> String {
format!("{}#{}", self.endpoint_id, self.incarnation)
}
pub fn display_id(&self) -> String {
let id = &self.endpoint_id.0;
let short = &id[..id.len().min(8)];
format!("{}@{}", self.name, short)
}
#[cfg(test)]
pub fn for_test(name: impl Into<String>, incarnation: u64) -> Self {
let name = name.into();
Self {
endpoint_id: Self::test_endpoint_id(&name),
name,
host: "127.0.0.1".to_string(),
port: 7100,
incarnation,
}
}
#[cfg(test)]
pub fn test_endpoint_id(name: &str) -> NodeId {
let mut seed = [0u8; 32];
for (i, b) in name.as_bytes().iter().enumerate() {
seed[i % 32] ^= *b;
}
seed[0] = seed[0].wrapping_add(1);
NodeId(SecretKey::from_bytes(&seed).public().to_string())
}
}
impl fmt::Display for NodeIdentity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}@{}#{}", self.name, self.endpoint_id, self.incarnation)
}
}
impl PartialEq for NodeIdentity {
fn eq(&self, other: &Self) -> bool {
self.endpoint_id == other.endpoint_id && self.incarnation == other.incarnation
}
}
impl std::hash::Hash for NodeIdentity {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.endpoint_id.hash(state);
self.incarnation.hash(state);
}
}
impl foca::Identity for NodeIdentity {
type Addr = NodeId;
fn addr(&self) -> NodeId {
self.endpoint_id.clone()
}
fn renew(&self) -> Option<Self> {
Some(Self {
name: self.name.clone(),
endpoint_id: self.endpoint_id.clone(),
host: self.host.clone(),
port: self.port,
incarnation: self.incarnation.wrapping_add(1),
})
}
fn win_addr_conflict(&self, other: &Self) -> bool {
self.incarnation > other.incarnation
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum NodeClass {
#[default]
Worker,
Coordinator,
Edge,
Custom(String),
}
impl std::fmt::Display for NodeClass {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Worker => write!(f, "worker"),
Self::Coordinator => write!(f, "coordinator"),
Self::Edge => write!(f, "edge"),
Self::Custom(name) => write!(f, "custom({name})"),
}
}
}
#[derive(Debug, Clone)]
pub struct TransportTuning {
pub initial_rtt_ms: u64,
pub max_idle_timeout_secs: u64,
pub keep_alive_interval_secs: Option<u64>,
pub max_concurrent_bidi_streams: u32,
pub stream_receive_window: u32,
pub receive_window: u32,
pub send_window: u64,
pub initial_mtu: u16,
}
impl Default for TransportTuning {
fn default() -> Self {
Self {
initial_rtt_ms: 1,
max_idle_timeout_secs: 30,
keep_alive_interval_secs: Some(5),
max_concurrent_bidi_streams: 1024,
stream_receive_window: 256 * 1024, receive_window: 2 * 1024 * 1024, send_window: 2 * 1024 * 1024, initial_mtu: 1452,
}
}
}
#[derive(Debug, Clone, Default)]
pub enum Discovery {
Mdns { service_name: String },
SeedNodes(Vec<EndpointAddr>),
Both {
service_name: String,
seed_nodes: Vec<EndpointAddr>,
},
#[default]
None,
}
#[derive(Debug, Clone, Default)]
pub enum AllowlistMode {
#[default]
Open,
Enforced(std::path::PathBuf),
}
#[derive(Debug, Clone)]
pub struct ClusterConfig {
pub identity: NodeIdentity,
pub secret_key: SecretKey,
pub key_path: std::path::PathBuf,
pub listen_addr: SocketAddr,
pub cookie: String,
pub discovery: Discovery,
pub allowlist: AllowlistMode,
pub node_class: NodeClass,
pub node_metadata: HashMap<String, String>,
pub transport: TransportTuning,
}
pub struct ClusterConfigBuilder {
name: Option<String>,
listen_addr: Option<SocketAddr>,
advertise_addr: Option<SocketAddr>,
cookie: Option<String>,
discovery: Discovery,
allowlist: AllowlistMode,
key_path: Option<std::path::PathBuf>,
secret_key: Option<SecretKey>,
node_class: NodeClass,
node_metadata: HashMap<String, String>,
transport: TransportTuning,
}
impl ClusterConfigBuilder {
pub fn new() -> Self {
Self {
name: None,
listen_addr: None,
advertise_addr: None,
cookie: None,
discovery: Discovery::default(),
allowlist: AllowlistMode::default(),
key_path: None,
secret_key: None,
node_class: NodeClass::default(),
node_metadata: HashMap::new(),
transport: TransportTuning::default(),
}
}
pub fn advertise(mut self, addr: impl Into<SocketAddr>) -> Self {
self.advertise_addr = Some(addr.into());
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn listen(mut self, addr: impl Into<SocketAddr>) -> Self {
self.listen_addr = Some(addr.into());
self
}
pub fn cookie(mut self, cookie: impl Into<String>) -> Self {
self.cookie = Some(cookie.into());
self
}
pub fn seed_nodes(mut self, seeds: impl IntoIterator<Item = EndpointAddr>) -> Self {
let seeds: Vec<EndpointAddr> = seeds.into_iter().collect();
self.discovery = match self.discovery {
Discovery::Mdns { service_name } => Discovery::Both {
service_name,
seed_nodes: seeds,
},
_ => Discovery::SeedNodes(seeds),
};
self
}
pub fn discovery(mut self, discovery: Discovery) -> Self {
self.discovery = discovery;
self
}
pub fn key_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
self.key_path = Some(path.into());
self
}
pub fn secret_key(mut self, key: SecretKey) -> Self {
self.secret_key = Some(key);
self
}
pub fn allowlist(mut self, path: impl Into<std::path::PathBuf>) -> Self {
self.allowlist = AllowlistMode::Enforced(path.into());
self
}
pub fn node_class(mut self, class: NodeClass) -> Self {
self.node_class = class;
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.node_metadata.insert(key.into(), value.into());
self
}
pub fn metadata_map(mut self, metadata: HashMap<String, String>) -> Self {
self.node_metadata = metadata;
self
}
pub fn transport(mut self, tuning: TransportTuning) -> Self {
self.transport = tuning;
self
}
pub fn build(self) -> Result<ClusterConfig, String> {
let listen_addr = self.listen_addr.ok_or("listen address is required")?;
let cookie = self.cookie.ok_or("cookie is required")?;
let key_path = self
.key_path
.unwrap_or_else(|| std::path::PathBuf::from("murmer-node.key"));
let secret_key = match self.secret_key {
Some(key) => key,
None => super::identity_key::load_or_generate(&key_path)
.map_err(|e| format!("failed to load node key: {e}"))?,
};
let endpoint_id = secret_key.public();
let peer_addr = self.advertise_addr.unwrap_or(listen_addr);
let name = self.name.unwrap_or_else(|| {
let host = peer_addr.ip();
let port = peer_addr.port();
format!("{host}-{port}")
});
let identity = NodeIdentity::new(
name,
endpoint_id,
peer_addr.ip().to_string(),
peer_addr.port(),
);
Ok(ClusterConfig {
identity,
secret_key,
key_path,
listen_addr,
cookie,
discovery: self.discovery,
allowlist: self.allowlist,
node_class: self.node_class,
node_metadata: self.node_metadata,
transport: self.transport,
})
}
}
impl Default for ClusterConfigBuilder {
fn default() -> Self {
Self::new()
}
}
impl ClusterConfig {
pub fn builder() -> ClusterConfigBuilder {
ClusterConfigBuilder::new()
}
}