1use std::collections::HashSet;
2use std::ops::Deref;
3use std::str::FromStr;
4use std::{fmt, net};
5
6use cyphernet::addr::PeerAddr;
7use localtime::LocalDuration;
8use serde::{Deserialize, Serialize};
9use serde_json as json;
10
11use crate::node;
12use crate::node::policy::{Scope, SeedingPolicy};
13use crate::node::{Address, Alias, NodeId};
14
15pub type ProtocolVersion = u8;
17
18pub mod seeds {
20 use std::{
21 net::{Ipv4Addr, Ipv6Addr},
22 str::FromStr,
23 sync::LazyLock,
24 };
25
26 use cyphernet::addr::{tor::OnionAddrV3, HostName, NetAddr};
27
28 use super::{ConnectAddress, NodeId, PeerAddr};
29
30 fn to_connect_addresses(id: NodeId, hostnames: Vec<HostName>) -> Vec<ConnectAddress> {
32 hostnames
33 .into_iter()
34 .map(|hostname| PeerAddr::new(id, NetAddr::new(hostname, 8776).into()).into())
35 .collect()
36 }
37
38 pub static RADICLE_NODE_BOOTSTRAP_IRIS: LazyLock<Vec<ConnectAddress>> = LazyLock::new(|| {
40 to_connect_addresses(
41 #[allow(clippy::unwrap_used)] NodeId::from_str("z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7").unwrap(),
43 vec![
44 HostName::Dns("iris.radicle.xyz".to_owned()),
45 Ipv6Addr::new(0x2a01, 0x4f9, 0xc010, 0xdfaa, 0, 0, 0, 1).into(),
46 Ipv4Addr::new(95, 217, 156, 6).into(),
47 #[allow(clippy::unwrap_used)] OnionAddrV3::from_str(
49 "irisradizskwweumpydlj4oammoshkxxjur3ztcmo7cou5emc6s5lfid.onion",
50 )
51 .unwrap()
52 .into(),
53 ],
54 )
55 });
56
57 pub static RADICLE_NODE_BOOTSTRAP_ROSA: LazyLock<Vec<ConnectAddress>> = LazyLock::new(|| {
59 to_connect_addresses(
60 #[allow(clippy::unwrap_used)] NodeId::from_str("z6Mkmqogy2qEM2ummccUthFEaaHvyYmYBYh3dbe9W4ebScxo").unwrap(),
62 vec![
63 HostName::Dns("rosa.radicle.xyz".to_owned()),
64 Ipv6Addr::new(0x2a01, 0x4ff, 0xf0, 0xabd3, 0, 0, 0, 1).into(),
65 Ipv4Addr::new(5, 161, 85, 124).into(),
66 #[allow(clippy::unwrap_used)] OnionAddrV3::from_str(
68 "rosarad5bxgdlgjnzzjygnsxrwxmoaj4vn7xinlstwglxvyt64jlnhyd.onion",
69 )
70 .unwrap()
71 .into(),
72 ],
73 )
74 });
75}
76
77#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
81pub enum Network {
82 #[default]
83 Main,
84 Test,
85}
86
87impl Network {
88 pub fn bootstrap(&self) -> Vec<(Alias, ProtocolVersion, Vec<ConnectAddress>)> {
90 match self {
91 Self::Main => [
92 (
93 "iris.radicle.xyz",
94 seeds::RADICLE_NODE_BOOTSTRAP_IRIS.clone(),
95 ),
96 (
97 "rosa.radicle.xyz",
98 seeds::RADICLE_NODE_BOOTSTRAP_ROSA.clone(),
99 ),
100 ]
101 .into_iter()
102 .map(|(a, s)| (Alias::new(a), 1, s))
103 .collect(),
104
105 Self::Test => vec![],
106 }
107 }
108
109 pub fn public_seeds(&self) -> Vec<ConnectAddress> {
111 match self {
112 Self::Main => {
113 let mut result = seeds::RADICLE_NODE_BOOTSTRAP_IRIS.clone();
114 result.extend(seeds::RADICLE_NODE_BOOTSTRAP_ROSA.clone());
115 result
116 }
117 Self::Test => vec![],
118 }
119 }
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, Default)]
124#[serde(default, rename_all = "camelCase")]
125#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
126pub struct Limits {
127 pub routing_max_size: LimitRoutingMaxSize,
129
130 pub routing_max_age: LimitRoutingMaxAge,
132
133 pub gossip_max_age: LimitGossipMaxAge,
135
136 pub fetch_concurrency: LimitFetchConcurrency,
138
139 pub max_open_files: LimitMaxOpenFiles,
141
142 pub rate: RateLimits,
144
145 pub connection: ConnectionLimits,
147
148 pub fetch_pack_receive: FetchPackSizeLimit,
150}
151
152#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157#[serde(into = "String", try_from = "String")]
158#[cfg_attr(
159 feature = "schemars",
160 derive(schemars::JsonSchema),
161 schemars(transparent),
162 schemars(!try_from),
165 schemars(!into),
166)]
167pub struct FetchPackSizeLimit {
168 #[cfg_attr(
169 feature = "schemars",
170 schemars(with = "crate::schemars_ext::bytesize::ByteSize")
171 )]
172 limit: bytesize::ByteSize,
173}
174
175impl From<bytesize::ByteSize> for FetchPackSizeLimit {
176 fn from(limit: bytesize::ByteSize) -> Self {
177 Self { limit }
178 }
179}
180
181impl From<FetchPackSizeLimit> for String {
182 fn from(limit: FetchPackSizeLimit) -> Self {
183 limit.to_string()
184 }
185}
186
187impl TryFrom<String> for FetchPackSizeLimit {
188 type Error = String;
189
190 fn try_from(s: String) -> Result<Self, Self::Error> {
191 s.parse()
192 }
193}
194
195impl FromStr for FetchPackSizeLimit {
196 type Err = String;
197
198 fn from_str(s: &str) -> Result<Self, Self::Err> {
199 Ok(FetchPackSizeLimit { limit: s.parse()? })
200 }
201}
202
203impl fmt::Display for FetchPackSizeLimit {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 write!(f, "{}", self.limit)
206 }
207}
208
209impl FetchPackSizeLimit {
210 pub fn bytes(size: u64) -> Self {
212 bytesize::ByteSize::b(size).into()
213 }
214
215 pub fn kibibytes(size: u64) -> Self {
217 bytesize::ByteSize::kib(size).into()
218 }
219
220 pub fn mebibytes(size: u64) -> Self {
222 bytesize::ByteSize::mib(size).into()
223 }
224
225 pub fn gibibytes(size: u64) -> Self {
227 bytesize::ByteSize::gib(size).into()
228 }
229
230 pub fn exceeded_by(&self, bytes: usize) -> bool {
232 bytes >= self.limit.as_u64() as usize
233 }
234}
235
236impl Default for FetchPackSizeLimit {
237 fn default() -> Self {
238 Self::mebibytes(500)
239 }
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, Default)]
244#[serde(default, rename_all = "camelCase")]
245#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
246pub struct ConnectionLimits {
247 pub inbound: LimitConnectionsInbound,
249
250 pub outbound: LimitConnectionsOutbound,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, Display)]
256#[display("RateLimit(fill_rate={fill_rate}, capacity={capacity})")]
257#[serde(rename_all = "camelCase")]
258#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
259pub struct RateLimit {
260 pub fill_rate: f64,
261 pub capacity: usize,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, Default)]
266#[serde(rename_all = "camelCase")]
267#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
268pub struct RateLimits {
269 pub inbound: LimitRateInbound,
270
271 pub outbound: LimitRateOutbound,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
276#[cfg_attr(
277 feature = "schemars",
278 derive(schemars::JsonSchema),
279 schemars(description = "\
280 A node address to connect to. Format: An Ed25519 public key in multibase encoding, \
281 followed by the symbol '@', followed by an IP address, or a DNS name, or a Tor onion \
282 name, followed by the symbol ':', followed by a TCP port number.\
283")
284)]
285pub struct ConnectAddress(
286 #[serde(with = "crate::serde_ext::string")]
287 #[cfg_attr(feature = "schemars", schemars(
288 with = "String",
289 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}))$"),
290 extend("examples" = [
291 "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@rosa.radicle.xyz:8776",
292 "z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C@xmrhfasfg5suueegrnc4gsgyi2tyclcy5oz7f5drnrodmdtob6t2ioyd.onion:8776",
293 "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi@seed.example.com:8776",
294 "z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5@192.0.2.0:31337",
295 ]),
296 ))]
297 PeerAddr<NodeId, Address>,
298);
299
300impl From<PeerAddr<NodeId, Address>> for ConnectAddress {
301 fn from(value: PeerAddr<NodeId, Address>) -> Self {
302 Self(value)
303 }
304}
305
306impl From<ConnectAddress> for (NodeId, Address) {
307 fn from(value: ConnectAddress) -> Self {
308 (value.0.id, value.0.addr)
309 }
310}
311
312impl From<(NodeId, Address)> for ConnectAddress {
313 fn from((id, addr): (NodeId, Address)) -> Self {
314 Self(PeerAddr { id, addr })
315 }
316}
317
318impl From<ConnectAddress> for Address {
319 fn from(value: ConnectAddress) -> Self {
320 value.0.addr
321 }
322}
323
324impl Deref for ConnectAddress {
325 type Target = PeerAddr<NodeId, Address>;
326
327 fn deref(&self) -> &Self::Target {
328 &self.0
329 }
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize, Default)]
334#[serde(rename_all = "camelCase", tag = "type")]
335#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
336pub enum PeerConfig {
337 Static,
339 #[default]
341 Dynamic,
342}
343
344#[derive(Debug, Copy, Clone, Default, Serialize, Deserialize)]
346#[serde(rename_all = "camelCase")]
347#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
348pub enum Relay {
349 Always,
351 Never,
353 #[default]
355 Auto,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
360#[serde(rename_all = "camelCase", tag = "mode")]
361#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
362pub enum AddressConfig {
363 Proxy {
365 address: net::SocketAddr,
367 },
368 Forward,
371}
372
373#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
375#[serde(rename_all = "camelCase", tag = "default")]
376#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
377pub enum DefaultSeedingPolicy {
378 Allow {
380 #[serde(default)]
382 scope: Scope,
383 },
384 #[default]
386 Block,
387}
388
389impl DefaultSeedingPolicy {
390 pub fn is_allow(&self) -> bool {
392 matches!(self, Self::Allow { .. })
393 }
394
395 pub fn permissive() -> Self {
397 Self::Allow { scope: Scope::All }
398 }
399}
400
401impl From<DefaultSeedingPolicy> for SeedingPolicy {
402 fn from(policy: DefaultSeedingPolicy) -> Self {
403 match policy {
404 DefaultSeedingPolicy::Block => Self::Block,
405 DefaultSeedingPolicy::Allow { scope } => Self::Allow { scope },
406 }
407 }
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
412#[serde(rename_all = "camelCase")]
413#[cfg_attr(
414 feature = "schemars",
415 derive(schemars::JsonSchema),
416 schemars(rename = "NodeConfig")
417)]
418pub struct Config {
419 pub alias: Alias,
421 #[serde(default)]
423 #[cfg_attr(feature = "schemars", schemars(example = &"127.0.0.1:8776"))]
424 pub listen: Vec<net::SocketAddr>,
425 #[serde(default)]
427 pub peers: PeerConfig,
428 #[serde(default)]
431 pub connect: HashSet<ConnectAddress>,
432 #[serde(default)]
434 pub external_addresses: Vec<Address>,
435 #[serde(default, skip_serializing_if = "Option::is_none")]
437 pub proxy: Option<net::SocketAddr>,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub onion: Option<AddressConfig>,
441 #[serde(default)]
443 pub network: Network,
444 #[serde(default)]
446 pub log: LogLevel,
447 #[serde(default, deserialize_with = "crate::serde_ext::ok_or_default")]
449 pub relay: Relay,
450 #[serde(default)]
452 pub limits: Limits,
453 #[serde(default)]
455 pub workers: Workers,
456 #[serde(default)]
458 pub seeding_policy: DefaultSeedingPolicy,
459 #[serde(flatten, skip_serializing)]
461 pub extra: json::Map<String, json::Value>,
462 #[serde(default, skip_serializing_if = "Option::is_none")]
469 pub secret: Option<std::path::PathBuf>,
470}
471
472impl Config {
473 pub fn test(alias: Alias) -> Self {
474 Self {
475 network: Network::Test,
476 ..Self::new(alias)
477 }
478 }
479
480 pub fn new(alias: Alias) -> Self {
481 Self {
482 alias,
483 peers: PeerConfig::default(),
484 listen: vec![],
485 connect: HashSet::default(),
486 external_addresses: vec![],
487 network: Network::default(),
488 proxy: None,
489 onion: None,
490 relay: Relay::default(),
491 limits: Limits::default(),
492 workers: Workers::default(),
493 log: LogLevel::default(),
494 seeding_policy: DefaultSeedingPolicy::default(),
495 extra: json::Map::default(),
496 secret: None,
497 }
498 }
499
500 pub fn peer(&self, id: &NodeId) -> Option<&Address> {
501 self.connect
502 .iter()
503 .find(|ca| &ca.id == id)
504 .map(|ca| &ca.addr)
505 }
506
507 pub fn peers(&self) -> impl Iterator<Item = NodeId> + '_ {
508 self.connect.iter().map(|p| p.0.id)
509 }
510
511 pub fn is_persistent(&self, id: &NodeId) -> bool {
512 self.peer(id).is_some()
513 }
514
515 pub fn is_relay(&self) -> bool {
517 match self.relay {
518 Relay::Auto => !self.external_addresses.is_empty(),
521 Relay::Never => false,
522 Relay::Always => true,
523 }
524 }
525
526 pub fn features(&self) -> node::Features {
527 node::Features::SEED
528 }
529}
530
531#[derive(Clone, Copy, Debug, Display, Deserialize, Serialize, From)]
532#[serde(transparent)]
533#[display("{0}")]
534#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
535pub struct LogLevel(
536 #[serde(with = "crate::serde_ext::string")]
537 #[cfg_attr(
538 feature = "schemars",
539 schemars(with = "crate::schemars_ext::log::Level")
540 )]
541 log::Level,
542);
543
544impl Default for LogLevel {
545 fn default() -> Self {
546 Self(log::Level::Info)
547 }
548}
549
550impl From<LogLevel> for log::Level {
551 fn from(value: LogLevel) -> Self {
552 value.0
553 }
554}
555
556#[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)]
557#[serde(transparent)]
558#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
559pub struct LimitRoutingMaxAge(
560 #[serde(with = "crate::serde_ext::localtime::duration")]
561 #[cfg_attr(
562 feature = "schemars",
563 schemars(with = "crate::schemars_ext::localtime::LocalDuration")
564 )]
565 localtime::LocalDuration,
566);
567
568impl Default for LimitRoutingMaxAge {
569 fn default() -> Self {
570 Self(localtime::LocalDuration::from_mins(7 * 24 * 60)) }
572}
573
574impl From<LimitRoutingMaxAge> for LocalDuration {
575 fn from(value: LimitRoutingMaxAge) -> Self {
576 value.0
577 }
578}
579
580impl From<LocalDuration> for LimitRoutingMaxAge {
581 fn from(value: LocalDuration) -> Self {
582 Self(value)
583 }
584}
585
586#[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)]
587#[serde(transparent)]
588#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
589pub struct LimitGossipMaxAge(
590 #[serde(with = "crate::serde_ext::localtime::duration")]
591 #[cfg_attr(
592 feature = "schemars",
593 schemars(with = "crate::schemars_ext::localtime::LocalDuration")
594 )]
595 localtime::LocalDuration,
596);
597
598impl Default for LimitGossipMaxAge {
599 fn default() -> Self {
600 Self(localtime::LocalDuration::from_mins(2 * 7 * 24 * 60)) }
602}
603
604impl From<LimitGossipMaxAge> for LocalDuration {
605 fn from(value: LimitGossipMaxAge) -> Self {
606 value.0
607 }
608}
609
610macro_rules! wrapper {
611 ($name:ident, $type:ty, $default:expr $(, $derive:ty)*) => {
612 #[derive(Clone, Debug, Deserialize, Display, Serialize, From $(, $derive)*)]
613 #[display("{0}")]
614 #[serde(transparent)]
615 #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
616 pub struct $name($type);
617
618 impl Default for $name {
619 fn default() -> Self {
620 Self($default)
621 }
622 }
623
624 impl From<$name> for $type {
625 fn from(value: $name) -> Self {
626 value.0
627 }
628 }
629 };
630}
631wrapper!(Workers, usize, 8, Copy);
632wrapper!(LimitConnectionsInbound, usize, 128, Copy);
633wrapper!(LimitConnectionsOutbound, usize, 16, Copy);
634wrapper!(LimitRoutingMaxSize, usize, 1000, Copy);
635wrapper!(LimitFetchConcurrency, usize, 1, Copy);
636wrapper!(
637 LimitRateInbound,
638 RateLimit,
639 RateLimit {
640 fill_rate: 5.0,
641 capacity: 1024,
642 }
643);
644wrapper!(LimitMaxOpenFiles, usize, 4096, Copy);
645wrapper!(
646 LimitRateOutbound,
647 RateLimit,
648 RateLimit {
649 fill_rate: 10.0,
650 capacity: 2048,
651 }
652);
653
654#[cfg(test)]
655#[allow(clippy::unwrap_used)]
656mod test {
657 #[test]
658 fn partial() {
659 use super::Config;
660 use serde_json::json;
661
662 let config: Config = serde_json::from_value(json!({
663 "alias": "example",
664 "limits": {
665 "connection": {
666 "inbound": 1337,
667 },
668 },
669 }
670 ))
671 .unwrap();
672 assert_eq!(config.limits.connection.inbound.0, 1337);
673 assert_eq!(
674 config.limits.connection.outbound.0,
675 super::LimitConnectionsOutbound::default().0,
676 );
677
678 let config: Config = serde_json::from_value(json!({
679 "alias": "example",
680 "limits": {
681 "connection": {
682 "outbound": 1337,
683 },
684 },
685 }
686 ))
687 .unwrap();
688 assert_eq!(
689 config.limits.connection.inbound.0,
690 super::LimitConnectionsInbound::default().0,
691 );
692 assert_eq!(config.limits.connection.outbound.0, 1337);
693 }
694}