radicle/node/
config.rs

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
15/// Peer-to-peer protocol version.
16pub type ProtocolVersion = u8;
17
18/// Configured public seeds.
19pub 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    /// A helper to generate many connect addresses for a node, using port 8776.
31    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    /// A public Radicle seed node for the community.
39    pub static RADICLE_NODE_BOOTSTRAP_IRIS: LazyLock<Vec<ConnectAddress>> = LazyLock::new(|| {
40        to_connect_addresses(
41            #[allow(clippy::unwrap_used)] // Value is manually verified.
42            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)] // Value is manually verified.
48                OnionAddrV3::from_str(
49                    "irisradizskwweumpydlj4oammoshkxxjur3ztcmo7cou5emc6s5lfid.onion",
50                )
51                .unwrap()
52                .into(),
53            ],
54        )
55    });
56
57    /// A public Radicle seed node for the community.
58    pub static RADICLE_NODE_BOOTSTRAP_ROSA: LazyLock<Vec<ConnectAddress>> = LazyLock::new(|| {
59        to_connect_addresses(
60            #[allow(clippy::unwrap_used)] // Value is manually verified.
61            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)] // Value is manually verified.
67                OnionAddrV3::from_str(
68                    "rosarad5bxgdlgjnzzjygnsxrwxmoaj4vn7xinlstwglxvyt64jlnhyd.onion",
69                )
70                .unwrap()
71                .into(),
72            ],
73        )
74    });
75}
76
77/// Peer-to-peer network.
78#[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    /// Bootstrap nodes for this network.
89    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    /// Public seeds for this network.
110    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/// Configuration parameters defining attributes of minima and maxima.
123#[derive(Debug, Clone, Serialize, Deserialize, Default)]
124#[serde(default, rename_all = "camelCase")]
125#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
126pub struct Limits {
127    /// Number of routing table entries before we start pruning.
128    pub routing_max_size: LimitRoutingMaxSize,
129
130    /// How long to keep a routing table entry before being pruned.
131    pub routing_max_age: LimitRoutingMaxAge,
132
133    /// How long to keep a gossip message entry before pruning it.
134    pub gossip_max_age: LimitGossipMaxAge,
135
136    /// Maximum number of concurrent fetches per peer connection.
137    pub fetch_concurrency: LimitFetchConcurrency,
138
139    /// Maximum number of open files.
140    pub max_open_files: LimitMaxOpenFiles,
141
142    /// Rate limitter settings.
143    pub rate: RateLimits,
144
145    /// Connection limits.
146    pub connection: ConnectionLimits,
147
148    /// Channel limits.
149    pub fetch_pack_receive: FetchPackSizeLimit,
150}
151
152/// Limiter for byte streams.
153///
154/// Default: 500MiB
155#[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    // serde's transparent and try_from/into will conflict, so we tell schemars
163    // to ignore them for its generation.
164    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    /// New `FetchPackSizeLimit` in bytes.
211    pub fn bytes(size: u64) -> Self {
212        bytesize::ByteSize::b(size).into()
213    }
214
215    /// New `FetchPackSizeLimit` in kibibytes.
216    pub fn kibibytes(size: u64) -> Self {
217        bytesize::ByteSize::kib(size).into()
218    }
219
220    /// New `FetchPackSizeLimit` in mebibytes.
221    pub fn mebibytes(size: u64) -> Self {
222        bytesize::ByteSize::mib(size).into()
223    }
224
225    /// New `FetchPackSizeLimit` in gibibytes.
226    pub fn gibibytes(size: u64) -> Self {
227        bytesize::ByteSize::gib(size).into()
228    }
229
230    /// Check if this limit is exceeded by the number of `bytes` provided.
231    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/// Connection limits.
243#[derive(Debug, Clone, Serialize, Deserialize, Default)]
244#[serde(default, rename_all = "camelCase")]
245#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
246pub struct ConnectionLimits {
247    /// Max inbound connections.
248    pub inbound: LimitConnectionsInbound,
249
250    /// Max outbound connections. Note that this can be higher than the *target* number.
251    pub outbound: LimitConnectionsOutbound,
252}
253
254/// Rate limts for a single connection.
255#[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/// Rate limits for inbound and outbound connections.
265#[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/// Full address used to connect to a remote node.
275#[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/// Peer configuration.
333#[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 peer set. Connect to the configured peers and maintain the connections.
338    Static,
339    /// Dynamic peer set.
340    #[default]
341    Dynamic,
342}
343
344/// Relay configuration.
345#[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 relay messages.
350    Always,
351    /// Never relay messages.
352    Never,
353    /// Relay messages when applicable.
354    #[default]
355    Auto,
356}
357
358/// Proxy configuration.
359#[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 connections to this address type.
364    Proxy {
365        /// Proxy address.
366        address: net::SocketAddr,
367    },
368    /// Forward address to the next layer. Either this is the global proxy,
369    /// or the operating system, via DNS.
370    Forward,
371}
372
373/// Default seeding policy. Applies when no repository policies for the given repo are found.
374#[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 seeding.
379    Allow {
380        /// Seeding scope.
381        #[serde(default)]
382        scope: Scope,
383    },
384    /// Block seeding.
385    #[default]
386    Block,
387}
388
389impl DefaultSeedingPolicy {
390    /// Is this an "allow" policy.
391    pub fn is_allow(&self) -> bool {
392        matches!(self, Self::Allow { .. })
393    }
394
395    /// Seed everything from anyone.
396    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/// Service configuration.
411#[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    /// Node alias.
420    pub alias: Alias,
421    /// Socket address (a combination of IPv4 or IPv6 address and TCP port) to listen on.
422    #[serde(default)]
423    #[cfg_attr(feature = "schemars", schemars(example = &"127.0.0.1:8776"))]
424    pub listen: Vec<net::SocketAddr>,
425    /// Peer configuration.
426    #[serde(default)]
427    pub peers: PeerConfig,
428    /// Peers to connect to on startup.
429    /// Connections to these peers will be maintained.
430    #[serde(default)]
431    pub connect: HashSet<ConnectAddress>,
432    /// Specify the node's public addresses
433    #[serde(default)]
434    pub external_addresses: Vec<Address>,
435    /// Global proxy.
436    #[serde(default, skip_serializing_if = "Option::is_none")]
437    pub proxy: Option<net::SocketAddr>,
438    /// Onion address config.
439    #[serde(default, skip_serializing_if = "Option::is_none")]
440    pub onion: Option<AddressConfig>,
441    /// Peer-to-peer network.
442    #[serde(default)]
443    pub network: Network,
444    /// Log level.
445    #[serde(default)]
446    pub log: LogLevel,
447    /// Whether or not our node should relay messages.
448    #[serde(default, deserialize_with = "crate::serde_ext::ok_or_default")]
449    pub relay: Relay,
450    /// Configured service limits.
451    #[serde(default)]
452    pub limits: Limits,
453    /// Number of worker threads to spawn.
454    #[serde(default)]
455    pub workers: Workers,
456    /// Default seeding policy.
457    #[serde(default)]
458    pub seeding_policy: DefaultSeedingPolicy,
459    /// Extra fields that aren't supported.
460    #[serde(flatten, skip_serializing)]
461    pub extra: json::Map<String, json::Value>,
462    /// Path to a file containing an Ed25519 secret key, in OpenSSH format, i.e.
463    /// with the `-----BEGIN OPENSSH PRIVATE KEY-----` header. The corresponding
464    /// public key will be used as the Node ID.
465    ///
466    /// A decryption password cannot be configured, but passed at runtime via
467    /// the environment variable `RAD_PASSPHRASE`.
468    #[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    /// Are we a relay node? This determines what we do with gossip messages from other peers.
516    pub fn is_relay(&self) -> bool {
517        match self.relay {
518            // In "auto" mode, we only relay if we're a public seed node.
519            // This reduces traffic for private nodes, as well as message redundancy.
520            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)) // One week
571    }
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)) // Two weeks
601    }
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}