/* 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),
}
}
}