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::SeedingPolicy;
use crate::node::{Address, Alias, NodeId};
use crate::storage::refs::FeatureLevel;
use super::policy;
pub type ProtocolVersion = u8;
pub mod seeds {
use std::{str::FromStr, sync::LazyLock};
use cyphernet::addr::{tor::OnionAddrV3, HostName, NetAddr};
use super::{ConnectAddress, NodeId, PeerAddr};
fn to_connect_addresses(id: NodeId, hostnames: Vec<HostName>) -> Vec<ConnectAddress> {
hostnames
.into_iter()
.map(|hostname| PeerAddr::new(id, NetAddr::new(hostname, 8776).into()).into())
.collect()
}
pub static RADICLE_NODE_BOOTSTRAP_IRIS: LazyLock<Vec<ConnectAddress>> = LazyLock::new(|| {
to_connect_addresses(
#[allow(clippy::unwrap_used)] NodeId::from_str("z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7").unwrap(),
vec![
HostName::Dns("iris.radicle.xyz".to_owned()),
#[allow(clippy::unwrap_used)] OnionAddrV3::from_str(
"irisradizskwweumpydlj4oammoshkxxjur3ztcmo7cou5emc6s5lfid.onion",
)
.unwrap()
.into(),
],
)
});
pub static RADICLE_NODE_BOOTSTRAP_ROSA: LazyLock<Vec<ConnectAddress>> = LazyLock::new(|| {
to_connect_addresses(
#[allow(clippy::unwrap_used)] NodeId::from_str("z6Mkmqogy2qEM2ummccUthFEaaHvyYmYBYh3dbe9W4ebScxo").unwrap(),
vec![
HostName::Dns("rosa.radicle.xyz".to_owned()),
#[allow(clippy::unwrap_used)] OnionAddrV3::from_str(
"rosarad5bxgdlgjnzzjygnsxrwxmoaj4vn7xinlstwglxvyt64jlnhyd.onion",
)
.unwrap()
.into(),
],
)
});
}
#[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 {
pub fn bootstrap(&self) -> Vec<(Alias, ProtocolVersion, Vec<ConnectAddress>)> {
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![],
}
}
pub fn public_seeds(&self) -> Vec<ConnectAddress> {
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![],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Limits {
pub routing_max_size: LimitRoutingMaxSize,
pub routing_max_age: LimitRoutingMaxAge,
pub gossip_max_age: LimitGossipMaxAge,
pub fetch_concurrency: LimitFetchConcurrency,
pub max_open_files: LimitMaxOpenFiles,
pub rate: RateLimits,
pub connection: ConnectionLimits,
pub fetch_pack_receive: FetchPackSizeLimit,
}
#[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<bytesize::ByteSize> for FetchPackSizeLimit {
fn from(limit: bytesize::ByteSize) -> Self {
Self { limit }
}
}
impl From<FetchPackSizeLimit> for String {
fn from(limit: FetchPackSizeLimit) -> Self {
limit.to_string()
}
}
impl TryFrom<String> for FetchPackSizeLimit {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
impl FromStr for FetchPackSizeLimit {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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 {
pub fn bytes(size: u64) -> Self {
bytesize::ByteSize::b(size).into()
}
pub fn kibibytes(size: u64) -> Self {
bytesize::ByteSize::kib(size).into()
}
pub fn mebibytes(size: u64) -> Self {
bytesize::ByteSize::mib(size).into()
}
pub fn gibibytes(size: u64) -> Self {
bytesize::ByteSize::gib(size).into()
}
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)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ConnectionLimits {
pub inbound: LimitConnectionsInbound,
pub outbound: LimitConnectionsOutbound,
}
#[derive(Debug, Clone, Copy, 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,
}
#[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,
}
#[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<NodeId, Address>,
);
impl From<PeerAddr<NodeId, Address>> for ConnectAddress {
fn from(value: PeerAddr<NodeId, Address>) -> Self {
Self(value)
}
}
impl From<ConnectAddress> 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<ConnectAddress> for Address {
fn from(value: ConnectAddress) -> Self {
value.0.addr
}
}
impl Deref for ConnectAddress {
type Target = PeerAddr<NodeId, Address>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase", tag = "type")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum PeerConfig {
Static,
#[default]
Dynamic,
}
#[derive(Debug, Copy, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Relay {
Always,
Never,
#[default]
Auto,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "mode")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum AddressConfig {
Proxy {
address: net::SocketAddr,
},
Forward,
}
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "default", rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum DefaultSeedingPolicy {
Allow {
#[serde(skip_serializing_if = "Scope::is_implicit")]
#[cfg_attr(feature = "schemars", schemars(flatten))]
scope: Scope,
},
#[default]
Block,
}
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct Scope(Option<policy::Scope>);
impl Scope {
pub fn implicit() -> Self {
Self(None)
}
pub fn explicit(scope: policy::Scope) -> Self {
Self(Some(scope))
}
pub fn into_inner(self) -> policy::Scope {
self.0.unwrap_or(policy::Scope::All)
}
pub fn is_implicit(&self) -> bool {
self.0.is_none()
}
fn all() -> Self {
Self::explicit(policy::Scope::All)
}
fn followed() -> Self {
Self::explicit(policy::Scope::Followed)
}
}
impl DefaultSeedingPolicy {
pub fn is_allow(&self) -> bool {
matches!(self, Self::Allow { .. })
}
pub fn permissive() -> Self {
Self::Allow {
scope: Scope::all(),
}
}
pub fn followed() -> Self {
Self::Allow {
scope: Scope::followed(),
}
}
}
impl From<DefaultSeedingPolicy> for SeedingPolicy {
fn from(policy: DefaultSeedingPolicy) -> Self {
match policy {
DefaultSeedingPolicy::Block => Self::Block,
DefaultSeedingPolicy::Allow { scope } => SeedingPolicy::Allow {
scope: scope.into_inner(),
},
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct FeatureLevelConfig {
#[serde(
default,
rename = "minimum",
skip_serializing_if = "crate::serde_ext::is_default"
)]
min: FeatureLevel,
}
impl FeatureLevelConfig {
pub fn min(&self) -> FeatureLevel {
self.min
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Fetch {
#[serde(default, skip_serializing_if = "crate::serde_ext::is_default")]
signed_references: SignedReferencesConfig,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SignedReferencesConfig {
#[serde(default, skip_serializing_if = "crate::serde_ext::is_default")]
feature_level: FeatureLevelConfig,
}
impl Fetch {
pub fn feature_level_min(&self) -> FeatureLevel {
self.signed_references.feature_level.min()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(
feature = "schemars",
derive(schemars::JsonSchema),
schemars(rename = "NodeConfig")
)]
pub struct Config {
pub alias: Alias,
#[serde(default)]
#[cfg_attr(feature = "schemars", schemars(example = &"127.0.0.1:8776"))]
pub listen: Vec<net::SocketAddr>,
#[serde(default)]
pub peers: PeerConfig,
#[serde(default)]
pub connect: HashSet<ConnectAddress>,
#[serde(default)]
pub external_addresses: Vec<Address>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proxy: Option<net::SocketAddr>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub onion: Option<AddressConfig>,
#[serde(default)]
pub network: Network,
#[serde(default)]
pub log: LogLevel,
#[serde(default, deserialize_with = "crate::serde_ext::ok_or_default")]
pub relay: Relay,
#[serde(default)]
pub limits: Limits,
#[serde(default)]
pub workers: Workers,
#[serde(default)]
pub seeding_policy: DefaultSeedingPolicy,
#[serde(default, skip_serializing_if = "crate::serde_ext::is_default")]
pub database: node::db::config::Config,
#[serde(default, skip_serializing_if = "crate::serde_ext::is_default")]
pub fetch: Fetch,
#[serde(flatten, skip_serializing)]
pub extra: json::Map<String, json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub secret: Option<std::path::PathBuf>,
}
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(),
database: node::db::config::Config::default(),
extra: json::Map::default(),
fetch: Fetch::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<Item = NodeId> + '_ {
self.connect.iter().map(|p| p.0.id)
}
pub fn is_persistent(&self, id: &NodeId) -> bool {
self.peer(id).is_some()
}
pub fn is_relay(&self) -> bool {
match self.relay {
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<LogLevel> 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(localtime::LocalDuration);
impl Default for LimitRoutingMaxAge {
fn default() -> Self {
Self(localtime::LocalDuration::from_mins(7 * 24 * 60)) }
}
impl From<LimitRoutingMaxAge> for LocalDuration {
fn from(value: LimitRoutingMaxAge) -> Self {
value.0
}
}
impl From<LocalDuration> 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(localtime::LocalDuration);
impl Default for LimitGossipMaxAge {
fn default() -> Self {
Self(localtime::LocalDuration::from_mins(2 * 7 * 24 * 60)) }
}
impl From<LimitGossipMaxAge> 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,
},
Copy
);
wrapper!(LimitMaxOpenFiles, usize, 4096, Copy);
wrapper!(
LimitRateOutbound,
RateLimit,
RateLimit {
fill_rate: 10.0,
capacity: 2048,
},
Copy
);
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use super::{DefaultSeedingPolicy, Scope};
use crate::node::{policy, Alias};
use serde_json::json;
#[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);
}
#[test]
fn deserialize_migrating_scope() {
let seeding_policy: DefaultSeedingPolicy = serde_json::from_value(json!({
"default": "allow"
}))
.unwrap();
assert_eq!(
seeding_policy,
DefaultSeedingPolicy::Allow { scope: Scope(None) }
);
let seeding_policy: DefaultSeedingPolicy = serde_json::from_value(json!({
"default": "allow",
"scope": null
}))
.unwrap();
assert_eq!(
seeding_policy,
DefaultSeedingPolicy::Allow { scope: Scope(None) }
);
let seeding_policy: DefaultSeedingPolicy = serde_json::from_value(json!({
"default": "allow",
"scope": "all"
}))
.unwrap();
assert_eq!(
seeding_policy,
DefaultSeedingPolicy::Allow {
scope: Scope(Some(policy::Scope::All))
}
);
let seeding_policy: DefaultSeedingPolicy = serde_json::from_value(json!({
"default": "allow",
"scope": "followed"
}))
.unwrap();
assert_eq!(
seeding_policy,
DefaultSeedingPolicy::Allow {
scope: Scope(Some(policy::Scope::Followed))
}
)
}
#[test]
fn serialize_migrating_scope() {
assert_eq!(
json!({
"default": "allow"
}),
serde_json::to_value(DefaultSeedingPolicy::Allow { scope: Scope(None) }).unwrap()
);
assert_eq!(
json!({
"default": "allow",
"scope": "all"
}),
serde_json::to_value(DefaultSeedingPolicy::Allow {
scope: Scope(Some(policy::Scope::All))
})
.unwrap()
);
assert_eq!(
json!({
"default": "allow",
"scope": "followed"
}),
serde_json::to_value(DefaultSeedingPolicy::Allow {
scope: Scope(Some(policy::Scope::Followed))
})
.unwrap()
);
}
#[test]
fn regression_ipv6_address_brackets() {
let address = "[2001:db8::1]:5976".to_string();
let config = json!({
"alias": "radicle",
"externalAddresses": [address],
});
let got: super::Config = serde_json::from_value(config).unwrap();
let mut expected = super::Config::new(Alias::new("radicle"));
expected.external_addresses = vec![address.parse().unwrap()];
assert_eq!(got.alias, expected.alias);
assert_eq!(got.external_addresses, expected.external_addresses);
}
#[test]
fn regression_ipv6_address_no_brackets() {
let address = "2001:db8::1:5976".to_string();
let config = json!({
"alias": "radicle",
"externalAddresses": [address],
});
let got: super::Config = serde_json::from_value(config).unwrap();
let mut expected = super::Config::new(Alias::new("radicle"));
expected.external_addresses = vec![address.parse().unwrap()];
assert_eq!(got.alias, expected.alias);
assert_eq!(got.external_addresses, expected.external_addresses);
}
#[test]
fn fetch_level_min() {
let config = json!({
"alias": "radicle",
"fetch": {
"signedReferences": {
"featureLevel": {
"minimum": "parent"
}
}
},
});
let got: super::Config = serde_json::from_value(config).unwrap();
let expected = super::Config::new(Alias::new("radicle"));
assert_eq!(got.alias, expected.alias);
assert_eq!(
got.fetch.feature_level_min(),
crate::storage::refs::FeatureLevel::Parent
);
}
}