/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2026 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::collections::HashMap; use structopt::StructOpt; use url::Url; use crate::error::{Error, Result}; type BlacklistEntry = (String, Vec, Vec); /// Ban policies definitions. /// /// If the ban policy is set to `Relaxed` will not ban peers in case /// they send a message without a corresponding MessageDispatcher. /// This is useful for nodes that may not be subscribed to protocols, /// such as Lilith. For most uses this should be set to `Strict`. /// /// TODO: this will be deprecated when we introduce the p2p resource /// mananger. #[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "lowercase")] pub enum BanPolicy { #[default] Strict, Relaxed, } /// P2P network settings. The scope of this is a P2P network instance /// configured by the library user. #[derive(Debug, Clone)] pub struct Settings { /// Only used for debugging, compromises privacy when set pub node_id: String, /// P2P accept addresses the instance listens on for inbound connections pub inbound_addrs: Vec, /// P2P external addresses the instance advertises so other peers can /// reach us and connect to us, as long as inbound addrs are configured pub external_addrs: Vec, /// Peer nodes to manually connect to pub peers: Vec, /// Seed nodes to connect to for peer discovery and/or advertising our /// own external addresses pub seeds: Vec, /// Magic bytes should be unique per P2P network. /// Avoid bleeding of networks. pub magic_bytes: MagicBytes, /// Application version, used for convenient protocol matching pub app_version: semver::Version, /// Application Identifier pub app_name: String, /// Whitelisted network transports for outbound connections pub active_profiles: Vec, /// Transports allowed to be mixed (tcp, tcp+tls, tor, tor+tls) /// When transport is added to this list the corresponding transport /// in active_profiles is used to connect to the node. /// Supported mixing scenarios include /// active_profile | mixed_profile /// tor | tcp /// tor+tls | tcp+tls /// socks5 | tor /// socks5 | tcp /// socks5+tls | tor+tls /// socks5+tls | tcp+tls pub mixed_profiles: Vec, /// Tor socks5 proxy to connect to when socks5 or socks5+tls are added to active profiles /// and transport mixing is enabled pub tor_socks5_proxy: Option, /// Nym socks5 proxy to connect to when socks5 or socks5+tls are added to active profiles /// and transport mixing is enabled pub nym_socks5_proxy: Option, /// I2p Socks5 proxy to connect to i2p eepsite (hidden services) pub i2p_socks5_proxy: Url, /// Outbound connection slots number, this many connections will be /// attempted. (This does not include manual connections) pub outbound_connections: usize, /// Inbound connection slots number, this many active listening connections /// will be allowed. (This does not include manual connections) pub inbound_connections: usize, /// Allow localnet hosts pub localnet: bool, /// Cooling off time for peer discovery when unsuccessful pub outbound_peer_discovery_cooloff_time: u64, /// Time between peer discovery attempts pub outbound_peer_discovery_attempt_time: u64, /// Maximum number of addresses (with preferred transports) to receive from /// seeds and peers. /// If undefined, `outbound_connections` will be used instead. pub getaddrs_max: Option, /// P2P datastore path pub p2p_datastore: Option, /// Hostlist storage path pub hostlist: Option, /// Pause interval within greylist refinery process pub greylist_refinery_interval: u64, /// Percent of connections to come from the whitelist pub white_connect_percent: usize, /// Number of goldlist connections pub gold_connect_count: usize, /// If this is true, strictly follow the gold_connect_count and /// white_connect_percent settings. Otherwise, connect to greylist /// entries if we have no white or gold connections. pub slot_preference_strict: bool, /// Number of seconds with no connections after which refinery /// process is paused. pub time_with_no_connections: u64, /// Nodes to avoid interacting with for the duration of the program, /// in the format ["host", ["scheme", "scheme"], [port, port]] /// If scheme is left empty it will default to "tcp+tls". /// If ports are left empty all ports from this peer will be blocked. pub blacklist: Vec, /// Do not ban nodes that send messages without dispatchers if set /// to `Relaxed`. For most uses, should be set to `Strict`. pub ban_policy: BanPolicy, /// Mapping of transport/scheme to Network Profile pub profiles: HashMap, } impl Default for Settings { fn default() -> Self { let version = option_env!("CARGO_PKG_VERSION").unwrap_or("0.0.0"); let app_version = semver::Version::parse(version).unwrap(); let app_name = option_env!("CARGO_PKG_NAME").unwrap_or("").to_string(); Self { node_id: String::new(), inbound_addrs: vec![], external_addrs: vec![], magic_bytes: Default::default(), peers: vec![], seeds: vec![], app_version, app_name, active_profiles: vec![], mixed_profiles: vec![], tor_socks5_proxy: None, nym_socks5_proxy: None, i2p_socks5_proxy: Url::parse("socks5://127.0.0.1:4447").unwrap(), outbound_connections: 8, inbound_connections: 8, localnet: false, outbound_peer_discovery_cooloff_time: 30, outbound_peer_discovery_attempt_time: 5, getaddrs_max: None, p2p_datastore: None, hostlist: None, greylist_refinery_interval: 15, white_connect_percent: 70, gold_connect_count: 2, slot_preference_strict: false, time_with_no_connections: 30, blacklist: vec![], ban_policy: BanPolicy::Strict, profiles: HashMap::new(), } } } impl Settings { /// Returns `outbound_connect_timeout` for a specific profile. pub fn outbound_connect_timeout(&self, profile: &str) -> u64 { self.profiles.get(profile).unwrap_or(&NetworkProfile::default()).outbound_connect_timeout } /// Returns the maximum `outbound_connect_timeout` across all profiles, /// selecting a conservative value suitable for the slowest network profile. pub fn outbound_connect_timeout_max(&self) -> u64 { self.profiles .values() .map(|p| p.outbound_connect_timeout) .max() .unwrap_or(NetworkProfile::default().outbound_connect_timeout) } pub fn channel_heartbeat_interval(&self, profile: &str) -> u64 { self.profiles.get(profile).unwrap_or(&NetworkProfile::default()).channel_heartbeat_interval } pub fn channel_handshake_timeout(&self, profile: &str) -> u64 { self.profiles.get(profile).unwrap_or(&NetworkProfile::default()).channel_handshake_timeout } } /// Distinguishes distinct P2P networks #[derive(serde::Deserialize, Debug, Clone)] pub struct MagicBytes(pub [u8; 4]); impl Default for MagicBytes { fn default() -> Self { Self([0xd9, 0xef, 0xb6, 0x7d]) } } /// Defines the network settings so we can have P2P configurations in /// TOML files. #[derive(Clone, Debug, serde::Deserialize, structopt::StructOpt, structopt_toml::StructOptToml)] #[structopt()] pub struct SettingsOpt { /// P2P accept address node listens to for inbound connections #[serde(default)] #[structopt(long = "accept")] pub inbound: Vec, /// Outbound connection slots number #[structopt(long = "outbound-slots")] pub outbound_connections: Option, /// Inbound connection slots number #[structopt(long = "inbound-slots")] pub inbound_connections: Option, #[serde(default)] #[structopt(skip)] /// Magic bytes used to distinguish P2P distinct networks and /// avoid nodes bleeding due to user config error. pub magic_bytes: MagicBytes, /// P2P external addresses node advertises so other peers can /// reach us and connect to us, as long as inbound addresses /// are also configured #[serde(default)] #[structopt(long)] pub external_addrs: Vec, /// Peer nodes to manually connect to #[serde(default)] #[structopt(long)] pub peers: Vec, /// Seed nodes to connect to for peers retrieval and/or /// advertising our own external addresses #[serde(default)] #[structopt(long)] pub seeds: Vec, /// Connection establishment timeout in seconds #[structopt(skip)] pub outbound_connect_timeout: Option, /// Exchange versions (handshake) timeout in seconds #[structopt(skip)] pub channel_handshake_timeout: Option, /// Ping-pong exchange execution interval in seconds #[structopt(skip)] pub channel_heartbeat_interval: Option, /// Only used for debugging. Compromises privacy when set. #[serde(default)] #[structopt(skip)] pub node_id: String, /// Preferred transports for outbound connections #[serde(default)] #[structopt(long = "network-profiles")] pub active_profiles: Option>, /// Transports allowed to be mixed (tcp, tcp+tls, tor, tor+tls). /// When transport is added to this list the corresponding transport /// in active_profiles is used to connect to the node. /// Supported mixing scenarios include /// tor => tcp, tor+tls => tcp+tls, /// socks5 => tor, socks5 => tcp, /// socks5+tls => tor+tls, socks5+tls => tcp+tls /// where the first one overrides the second. #[serde(default)] #[structopt(long = "mixed-profiles")] pub mixed_profiles: Option>, /// Tor socks5 proxy to connect to when socks5 or socks5+tls are added to active profiles /// and transport mixing is enabled #[structopt(long)] pub tor_socks5_proxy: Option, /// Nym socks5 proxy to connect to when socks5 or socks5+tls are added to active profiles /// and transport mixing is enabled #[structopt(long)] pub nym_socks5_proxy: Option, /// I2p Socks5 proxy to connect to i2p eepsite (hidden services) #[structopt(long)] pub i2p_socks5_proxy: Option, /// If this is true, strictly follow the gold_connect_count and /// white_connect_percent settings. Otherwise, connect to greylist /// entries if we have no white or gold connections. #[serde(default)] #[structopt(long)] pub localnet: bool, /// Cooling off time for peer discovery when unsuccessful #[structopt(skip)] pub outbound_peer_discovery_cooloff_time: Option, /// Time between peer discovery attempts #[structopt(skip)] pub outbound_peer_discovery_attempt_time: Option, /// Maximum number of addresses (with preferred transports) to receive from /// seeds and peers. /// If undefined, `outbound_connections` will be used instead. #[structopt(skip)] pub getaddrs_max: Option, /// P2P datastore path #[serde(default)] #[structopt(long)] pub p2p_datastore: Option, /// Hosts .tsv file to use #[serde(default)] #[structopt(long)] pub hostlist: Option, /// Pause interval within greylist refinery process #[structopt(skip)] pub greylist_refinery_interval: Option, /// Number of whitelist connections #[structopt(skip)] pub white_connect_percent: Option, /// Number of goldlist connections #[structopt(skip)] pub gold_connect_count: Option, /// Allow localnet hosts #[serde(default)] #[structopt(long)] pub slot_preference_strict: bool, /// Number of seconds with no connections after which refinery /// process is paused. #[structopt(skip)] pub time_with_no_connections: Option, /// Nodes to avoid interacting with for the duration of the program, /// in the format ["host", ["scheme", "scheme"], [port, port]] /// If scheme is left empty it will default to "tcp+tls". /// If ports are left empty all ports from this peer will be blocked. #[serde(default)] #[structopt(skip)] pub blacklist: Vec, /// Do not ban nodes that send messages without dispatchers if set /// to `Relaxed`. For most uses, should be set to `Strict`. #[serde(default)] #[structopt(skip)] pub ban_policy: BanPolicy, /// Network Profile for each transport #[serde(default)] #[structopt(skip)] pub profiles: HashMap, } impl TryFrom<(&str, &str, SettingsOpt)> for Settings { type Error = Error; fn try_from(st: (&str, &str, SettingsOpt)) -> Result { let app_name = st.0.to_string(); let app_version = semver::Version::parse(st.1)?; let opt = st.2; let def = Settings::default(); let mut inbound_addrs = opt.inbound; let mut external_addrs = opt.external_addrs; let mut peers = opt.peers; let mut seeds = opt.seeds; let active_profiles = opt.active_profiles.unwrap_or(def.active_profiles); let mixed_profiles = opt.mixed_profiles.unwrap_or(def.mixed_profiles); // check all the active profiles that are not mixed are found in net.profiles for name in &active_profiles { if !mixed_profiles.contains(name) && !opt.profiles.contains_key(name) { return Err(Error::ConfigError(format!( "Active profile '{name}' not defined in net.profiles" ))); } } let profiles: HashMap = opt .profiles .into_iter() .filter(|(k, _)| active_profiles.contains(k) && !mixed_profiles.contains(k)) .map(|(k, v)| { inbound_addrs.extend_from_slice(&v.inbound); external_addrs.extend_from_slice(&v.external_addrs); peers.extend_from_slice(&v.peers); seeds.extend_from_slice(&v.seeds); (k.clone(), NetworkProfile::from_with_profile(v, &k)) }) .collect(); Ok(Self { node_id: opt.node_id, inbound_addrs, external_addrs, magic_bytes: opt.magic_bytes, peers, seeds, app_version, app_name, active_profiles, mixed_profiles, tor_socks5_proxy: opt.tor_socks5_proxy, nym_socks5_proxy: opt.nym_socks5_proxy, i2p_socks5_proxy: opt.i2p_socks5_proxy.unwrap_or(def.i2p_socks5_proxy), outbound_connections: opt.outbound_connections.unwrap_or(def.outbound_connections), inbound_connections: opt.inbound_connections.unwrap_or(def.inbound_connections), localnet: opt.localnet, outbound_peer_discovery_cooloff_time: opt .outbound_peer_discovery_cooloff_time .unwrap_or(def.outbound_peer_discovery_cooloff_time), outbound_peer_discovery_attempt_time: opt .outbound_peer_discovery_attempt_time .unwrap_or(def.outbound_peer_discovery_attempt_time), getaddrs_max: opt.getaddrs_max, p2p_datastore: opt.p2p_datastore, hostlist: opt.hostlist, greylist_refinery_interval: opt .greylist_refinery_interval .unwrap_or(def.greylist_refinery_interval), white_connect_percent: opt.white_connect_percent.unwrap_or(def.white_connect_percent), gold_connect_count: opt.gold_connect_count.unwrap_or(def.gold_connect_count), slot_preference_strict: opt.slot_preference_strict, time_with_no_connections: opt .time_with_no_connections .unwrap_or(def.time_with_no_connections), blacklist: opt.blacklist, ban_policy: opt.ban_policy, profiles, }) } } #[derive(Clone, Debug, serde::Deserialize, structopt::StructOpt, structopt_toml::StructOptToml)] #[structopt()] pub struct NetworkProfileOpt { /// P2P accept address node listens to for inbound connections #[serde(default)] #[structopt(long = "accept")] pub inbound: Vec, /// P2P external addresses node advertises so other peers can /// reach us and connect to us, as long as inbound addresses /// are also configured #[serde(default)] #[structopt(long)] pub external_addrs: Vec, /// Peer nodes to manually connect to #[serde(default)] #[structopt(long)] pub peers: Vec, /// Seed nodes to connect to for peers retrieval and/or /// advertising our own external addresses #[serde(default)] #[structopt(long)] pub seeds: Vec, /// Connection establishment timeout in seconds #[structopt(skip)] pub outbound_connect_timeout: Option, /// Exchange versions (handshake) timeout in seconds #[structopt(skip)] pub channel_handshake_timeout: Option, /// Ping-pong exchange execution interval in seconds #[structopt(skip)] pub channel_heartbeat_interval: Option, } /// Network Profile info unique for each profile/transport #[derive(Debug, Clone)] pub struct NetworkProfile { /// Outbound connection timeout (in seconds) pub outbound_connect_timeout: u64, /// Exchange versions (handshake) timeout (in seconds) pub channel_handshake_timeout: u64, /// Ping-pong exchange execution interval (in seconds) pub channel_heartbeat_interval: u64, } impl Default for NetworkProfile { fn default() -> Self { Self { outbound_connect_timeout: 15, channel_handshake_timeout: 10, channel_heartbeat_interval: 30, } } } impl NetworkProfile { /// Creates default [`NetworkProfile`] for non-clearnet profiles pub fn tor_default() -> Self { Self { outbound_connect_timeout: 65, channel_handshake_timeout: 55, channel_heartbeat_interval: 90, } } /// Creates [`NetworkProfile`] from [`NetworkProfileOpt`] based on the profile fn from_with_profile(opt: NetworkProfileOpt, profile: &str) -> Self { let def = if ["tcp", "tcp+tls", "quic"].contains(&profile) { NetworkProfile::default() } else { NetworkProfile::tor_default() }; Self { outbound_connect_timeout: opt .outbound_connect_timeout .unwrap_or(def.outbound_connect_timeout), channel_handshake_timeout: opt .channel_handshake_timeout .unwrap_or(def.channel_handshake_timeout), channel_heartbeat_interval: opt .channel_heartbeat_interval .unwrap_or(def.channel_heartbeat_interval), } } }