use std::collections::HashSet; use std::ops::Deref; use std::str::FromStr; use std::{fmt, net}; use cyphernet::addr::PeerAddr; use localtime::LocalDuration; use serde::{Deserialize, Serialize}; use serde_json as json; use crate::node; use crate::node::policy::{Scope, SeedingPolicy}; use crate::node::{Address, Alias, NodeId}; /// Peer-to-peer protocol version. pub type ProtocolVersion = u8; /// Configured public seeds. pub mod seeds { use std::{ net::{Ipv4Addr, Ipv6Addr}, str::FromStr, sync::LazyLock, }; use cyphernet::addr::{tor::OnionAddrV3, HostName, NetAddr}; use super::{ConnectAddress, NodeId, PeerAddr}; /// A helper to generate many connect addresses for a node, using port 8776. fn to_connect_addresses(id: NodeId, hostnames: Vec) -> Vec { hostnames .into_iter() .map(|hostname| PeerAddr::new(id, NetAddr::new(hostname, 8776).into()).into()) .collect() } /// A public Radicle seed node for the community. pub static RADICLE_NODE_BOOTSTRAP_IRIS: LazyLock> = LazyLock::new(|| { to_connect_addresses( #[allow(clippy::unwrap_used)] // Value is manually verified. NodeId::from_str("z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7").unwrap(), vec![ HostName::Dns("iris.radicle.xyz".to_owned()), Ipv6Addr::new(0x2a01, 0x4f9, 0xc010, 0xdfaa, 0, 0, 0, 1).into(), Ipv4Addr::new(95, 217, 156, 6).into(), #[allow(clippy::unwrap_used)] // Value is manually verified. OnionAddrV3::from_str( "irisradizskwweumpydlj4oammoshkxxjur3ztcmo7cou5emc6s5lfid.onion", ) .unwrap() .into(), ], ) }); /// A public Radicle seed node for the community. pub static RADICLE_NODE_BOOTSTRAP_ROSA: LazyLock> = LazyLock::new(|| { to_connect_addresses( #[allow(clippy::unwrap_used)] // Value is manually verified. NodeId::from_str("z6Mkmqogy2qEM2ummccUthFEaaHvyYmYBYh3dbe9W4ebScxo").unwrap(), vec![ HostName::Dns("rosa.radicle.xyz".to_owned()), Ipv6Addr::new(0x2a01, 0x4ff, 0xf0, 0xabd3, 0, 0, 0, 1).into(), Ipv4Addr::new(5, 161, 85, 124).into(), #[allow(clippy::unwrap_used)] // Value is manually verified. OnionAddrV3::from_str( "rosarad5bxgdlgjnzzjygnsxrwxmoaj4vn7xinlstwglxvyt64jlnhyd.onion", ) .unwrap() .into(), ], ) }); } /// Peer-to-peer network. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum Network { #[default] Main, Test, } impl Network { /// Bootstrap nodes for this network. pub fn bootstrap(&self) -> Vec<(Alias, ProtocolVersion, Vec)> { match self { Self::Main => [ ( "iris.radicle.xyz", seeds::RADICLE_NODE_BOOTSTRAP_IRIS.clone(), ), ( "rosa.radicle.xyz", seeds::RADICLE_NODE_BOOTSTRAP_ROSA.clone(), ), ] .into_iter() .map(|(a, s)| (Alias::new(a), 1, s)) .collect(), Self::Test => vec![], } } /// Public seeds for this network. pub fn public_seeds(&self) -> Vec { match self { Self::Main => { let mut result = seeds::RADICLE_NODE_BOOTSTRAP_IRIS.clone(); result.extend(seeds::RADICLE_NODE_BOOTSTRAP_ROSA.clone()); result } Self::Test => vec![], } } } /// Configuration parameters defining attributes of minima and maxima. #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct Limits { /// Number of routing table entries before we start pruning. pub routing_max_size: LimitRoutingMaxSize, /// How long to keep a routing table entry before being pruned. pub routing_max_age: LimitRoutingMaxAge, /// How long to keep a gossip message entry before pruning it. pub gossip_max_age: LimitGossipMaxAge, /// Maximum number of concurrent fetches per peer connection. pub fetch_concurrency: LimitFetchConcurrency, /// Maximum number of open files. pub max_open_files: LimitMaxOpenFiles, /// Rate limiter settings. pub rate: RateLimits, /// Connection limits. pub connection: ConnectionLimits, /// Channel limits. pub fetch_pack_receive: FetchPackSizeLimit, } /// Limiter for byte streams. /// /// Default: 500MiB #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[serde(into = "String", try_from = "String")] #[cfg_attr( feature = "schemars", derive(schemars::JsonSchema), schemars(transparent), // serde's transparent and try_from/into will conflict, so we tell schemars // to ignore them for its generation. schemars(!try_from), schemars(!into), )] pub struct FetchPackSizeLimit { #[cfg_attr( feature = "schemars", schemars(with = "crate::schemars_ext::bytesize::ByteSize") )] limit: bytesize::ByteSize, } impl From for FetchPackSizeLimit { fn from(limit: bytesize::ByteSize) -> Self { Self { limit } } } impl From for String { fn from(limit: FetchPackSizeLimit) -> Self { limit.to_string() } } impl TryFrom for FetchPackSizeLimit { type Error = String; fn try_from(s: String) -> Result { s.parse() } } impl FromStr for FetchPackSizeLimit { type Err = String; fn from_str(s: &str) -> Result { Ok(FetchPackSizeLimit { limit: s.parse()? }) } } impl fmt::Display for FetchPackSizeLimit { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.limit) } } impl FetchPackSizeLimit { /// New `FetchPackSizeLimit` in bytes. pub fn bytes(size: u64) -> Self { bytesize::ByteSize::b(size).into() } /// New `FetchPackSizeLimit` in kibibytes. pub fn kibibytes(size: u64) -> Self { bytesize::ByteSize::kib(size).into() } /// New `FetchPackSizeLimit` in mebibytes. pub fn mebibytes(size: u64) -> Self { bytesize::ByteSize::mib(size).into() } /// New `FetchPackSizeLimit` in gibibytes. pub fn gibibytes(size: u64) -> Self { bytesize::ByteSize::gib(size).into() } /// Check if this limit is exceeded by the number of `bytes` provided. pub fn exceeded_by(&self, bytes: usize) -> bool { bytes >= self.limit.as_u64() as usize } } impl Default for FetchPackSizeLimit { fn default() -> Self { Self::mebibytes(500) } } /// Connection limits. #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ConnectionLimits { /// Max inbound connections. pub inbound: LimitConnectionsInbound, /// Max outbound connections. Note that this can be higher than the *target* number. pub outbound: LimitConnectionsOutbound, } /// Rate limits for a single connection. #[derive(Debug, Clone, Serialize, Deserialize, Display)] #[display("RateLimit(fill_rate={fill_rate}, capacity={capacity})")] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct RateLimit { pub fill_rate: f64, pub capacity: usize, } /// Rate limits for inbound and outbound connections. #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct RateLimits { pub inbound: LimitRateInbound, pub outbound: LimitRateOutbound, } /// Full address used to connect to a remote node. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[cfg_attr( feature = "schemars", derive(schemars::JsonSchema), schemars(description = "\ A node address to connect to. Format: An Ed25519 public key in multibase encoding, \ followed by the symbol '@', followed by an IP address, or a DNS name, or a Tor onion \ name, followed by the symbol ':', followed by a TCP port number.\ ") )] pub struct ConnectAddress( #[serde(with = "crate::serde_ext::string")] #[cfg_attr(feature = "schemars", schemars( with = "String", regex(pattern = r"^.+@.+:((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$"), extend("examples" = [ "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@rosa.radicle.xyz:8776", "z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C@xmrhfasfg5suueegrnc4gsgyi2tyclcy5oz7f5drnrodmdtob6t2ioyd.onion:8776", "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi@seed.example.com:8776", "z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5@192.0.2.0:31337", ]), ))] PeerAddr, ); impl From> for ConnectAddress { fn from(value: PeerAddr) -> Self { Self(value) } } impl From for (NodeId, Address) { fn from(value: ConnectAddress) -> Self { (value.0.id, value.0.addr) } } impl From<(NodeId, Address)> for ConnectAddress { fn from((id, addr): (NodeId, Address)) -> Self { Self(PeerAddr { id, addr }) } } impl From for Address { fn from(value: ConnectAddress) -> Self { value.0.addr } } impl Deref for ConnectAddress { type Target = PeerAddr; fn deref(&self) -> &Self::Target { &self.0 } } /// Peer configuration. #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase", tag = "type")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum PeerConfig { /// Static peer set. Connect to the configured peers and maintain the connections. Static, /// Dynamic peer set. #[default] Dynamic, } /// Relay configuration. #[derive(Debug, Copy, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum Relay { /// Always relay messages. Always, /// Never relay messages. Never, /// Relay messages when applicable. #[default] Auto, } /// Proxy configuration. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase", tag = "mode")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum AddressConfig { /// Proxy connections to this address type. Proxy { /// Proxy address. address: net::SocketAddr, }, /// Forward address to the next layer. Either this is the global proxy, /// or the operating system, via DNS. Forward, } /// Default seeding policy. Applies when no repository policies for the given repo are found. #[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", tag = "default")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum DefaultSeedingPolicy { /// Allow seeding. Allow { /// Seeding scope. #[serde(default)] scope: Scope, }, /// Block seeding. #[default] Block, } impl DefaultSeedingPolicy { /// Is this an "allow" policy. pub fn is_allow(&self) -> bool { matches!(self, Self::Allow { .. }) } /// Seed everything from anyone. pub fn permissive() -> Self { Self::Allow { scope: Scope::All } } } impl From for SeedingPolicy { fn from(policy: DefaultSeedingPolicy) -> Self { match policy { DefaultSeedingPolicy::Block => Self::Block, DefaultSeedingPolicy::Allow { scope } => Self::Allow { scope }, } } } /// Service configuration. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[cfg_attr( feature = "schemars", derive(schemars::JsonSchema), schemars(rename = "NodeConfig") )] pub struct Config { /// Node alias. pub alias: Alias, /// Socket address (a combination of IPv4 or IPv6 address and TCP port) to listen on. #[serde(default)] #[cfg_attr(feature = "schemars", schemars(example = &"127.0.0.1:8776"))] pub listen: Vec, /// Peer configuration. #[serde(default)] pub peers: PeerConfig, /// Peers to connect to on startup. /// Connections to these peers will be maintained. #[serde(default)] pub connect: HashSet, /// Specify the node's public addresses #[serde(default)] pub external_addresses: Vec
, /// Global proxy. #[serde(default, skip_serializing_if = "Option::is_none")] pub proxy: Option, /// Onion address config. #[serde(default, skip_serializing_if = "Option::is_none")] pub onion: Option, /// Peer-to-peer network. #[serde(default)] pub network: Network, /// Log level. #[serde(default)] pub log: LogLevel, /// Whether or not our node should relay messages. #[serde(default, deserialize_with = "crate::serde_ext::ok_or_default")] pub relay: Relay, /// Configured service limits. #[serde(default)] pub limits: Limits, /// Number of worker threads to spawn. #[serde(default)] pub workers: Workers, /// Default seeding policy. #[serde(default)] pub seeding_policy: DefaultSeedingPolicy, /// Extra fields that aren't supported. #[serde(flatten, skip_serializing)] pub extra: json::Map, /// Path to a file containing an Ed25519 secret key, in OpenSSH format, i.e. /// with the `-----BEGIN OPENSSH PRIVATE KEY-----` header. The corresponding /// public key will be used as the Node ID. /// /// A decryption password cannot be configured, but passed at runtime via /// the environment variable `RAD_PASSPHRASE`. #[serde(default, skip_serializing_if = "Option::is_none")] pub secret: Option, } impl Config { pub fn test(alias: Alias) -> Self { Self { network: Network::Test, ..Self::new(alias) } } pub fn new(alias: Alias) -> Self { Self { alias, peers: PeerConfig::default(), listen: vec![], connect: HashSet::default(), external_addresses: vec![], network: Network::default(), proxy: None, onion: None, relay: Relay::default(), limits: Limits::default(), workers: Workers::default(), log: LogLevel::default(), seeding_policy: DefaultSeedingPolicy::default(), extra: json::Map::default(), secret: None, } } pub fn peer(&self, id: &NodeId) -> Option<&Address> { self.connect .iter() .find(|ca| &ca.id == id) .map(|ca| &ca.addr) } pub fn peers(&self) -> impl Iterator + '_ { self.connect.iter().map(|p| p.0.id) } pub fn is_persistent(&self, id: &NodeId) -> bool { self.peer(id).is_some() } /// Are we a relay node? This determines what we do with gossip messages from other peers. pub fn is_relay(&self) -> bool { match self.relay { // In "auto" mode, we only relay if we're a public seed node. // This reduces traffic for private nodes, as well as message redundancy. Relay::Auto => !self.external_addresses.is_empty(), Relay::Never => false, Relay::Always => true, } } pub fn features(&self) -> node::Features { node::Features::SEED } } #[derive(Clone, Copy, Debug, Display, Deserialize, Serialize, From)] #[serde(transparent)] #[display("{0}")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct LogLevel( #[serde(with = "crate::serde_ext::string")] #[cfg_attr( feature = "schemars", schemars(with = "crate::schemars_ext::log::Level") )] log::Level, ); impl Default for LogLevel { fn default() -> Self { Self(log::Level::Info) } } impl From for log::Level { fn from(value: LogLevel) -> Self { value.0 } } #[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)] #[serde(transparent)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct LimitRoutingMaxAge( #[serde(with = "crate::serde_ext::localtime::duration")] #[cfg_attr( feature = "schemars", schemars(with = "crate::schemars_ext::localtime::LocalDuration") )] localtime::LocalDuration, ); impl Default for LimitRoutingMaxAge { fn default() -> Self { Self(localtime::LocalDuration::from_mins(7 * 24 * 60)) // One week } } impl From for LocalDuration { fn from(value: LimitRoutingMaxAge) -> Self { value.0 } } impl From for LimitRoutingMaxAge { fn from(value: LocalDuration) -> Self { Self(value) } } #[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)] #[serde(transparent)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct LimitGossipMaxAge( #[serde(with = "crate::serde_ext::localtime::duration")] #[cfg_attr( feature = "schemars", schemars(with = "crate::schemars_ext::localtime::LocalDuration") )] localtime::LocalDuration, ); impl Default for LimitGossipMaxAge { fn default() -> Self { Self(localtime::LocalDuration::from_mins(2 * 7 * 24 * 60)) // Two weeks } } impl From for LocalDuration { fn from(value: LimitGossipMaxAge) -> Self { value.0 } } macro_rules! wrapper { ($name:ident, $type:ty, $default:expr $(, $derive:ty)*) => { #[derive(Clone, Debug, Deserialize, Display, Serialize, From $(, $derive)*)] #[display("{0}")] #[serde(transparent)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct $name($type); impl Default for $name { fn default() -> Self { Self($default) } } impl From<$name> for $type { fn from(value: $name) -> Self { value.0 } } }; } wrapper!(Workers, usize, 8, Copy); wrapper!(LimitConnectionsInbound, usize, 128, Copy); wrapper!(LimitConnectionsOutbound, usize, 16, Copy); wrapper!(LimitRoutingMaxSize, usize, 1000, Copy); wrapper!(LimitFetchConcurrency, usize, 1, Copy); wrapper!( LimitRateInbound, RateLimit, RateLimit { fill_rate: 5.0, capacity: 1024, } ); wrapper!(LimitMaxOpenFiles, usize, 4096, Copy); wrapper!( LimitRateOutbound, RateLimit, RateLimit { fill_rate: 10.0, capacity: 2048, } ); #[cfg(test)] #[allow(clippy::unwrap_used)] mod test { #[test] fn partial() { use super::Config; use serde_json::json; let config: Config = serde_json::from_value(json!({ "alias": "example", "limits": { "connection": { "inbound": 1337, }, }, } )) .unwrap(); assert_eq!(config.limits.connection.inbound.0, 1337); assert_eq!( config.limits.connection.outbound.0, super::LimitConnectionsOutbound::default().0, ); let config: Config = serde_json::from_value(json!({ "alias": "example", "limits": { "connection": { "outbound": 1337, }, }, } )) .unwrap(); assert_eq!( config.limits.connection.inbound.0, super::LimitConnectionsInbound::default().0, ); assert_eq!(config.limits.connection.outbound.0, 1337); } }