Squashed 'components/mycelium/' content from commit afb32e0

git-subtree-dir: components/mycelium
git-subtree-split: afb32e0cdb2d4cdd17f22a5693278068d061f08c
This commit is contained in:
2025-08-16 21:12:34 +02:00
commit 10025f9fa5
132 changed files with 50951 additions and 0 deletions

1
myceliumd/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

4419
myceliumd/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

40
myceliumd/Cargo.toml Normal file
View File

@@ -0,0 +1,40 @@
[package]
name = "myceliumd"
version = "0.6.1"
edition = "2021"
license-file = "../LICENSE"
readme = "./README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "mycelium"
path = "src/main.rs"
[dependencies]
clap = { version = "4.5.41", features = ["derive"] }
tracing = { version = "0.1.41", features = ["release_max_level_debug"] }
tracing-logfmt = { version = "0.3.5", features = ["ansi_logs"] }
tracing-subscriber = { version = "0.3.19", features = [
"env-filter",
"nu-ansi-term",
] }
mycelium = { path = "../mycelium", features = ["message"] }
mycelium-metrics = { path = "../mycelium-metrics", features = ["prometheus"] }
mycelium-cli = { path = "../mycelium-cli/", features = ["message"] }
mycelium-api = { path = "../mycelium-api", features = ["message"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
tokio = { version = "1.46.1", features = [
"macros",
"rt-multi-thread",
"signal",
] }
reqwest = { version = "0.12.9", default-features = false, features = ["json"] }
base64 = "0.22.1"
prettytable-rs = "0.10.0"
urlencoding = "2.1.3"
byte-unit = "5.1.6"
config = "0.15.13"
dirs = "6.0.0"
toml = "0.9.2"

16
myceliumd/README.md Normal file
View File

@@ -0,0 +1,16 @@
# Myceliumd
This is the main binary to use for joining a/the public [`mycelium`] network. You can
either get the latest release from [the GitHub release page](https://github.com/threefoldtech/myceliumd/releases/latest),
or build the code yourself here. While we intend to keep the master branch as stable,
i.e. a build from latest master should succeed, this is not a hard guarantee. Additionally,
the master branch might not be compatible with the latest release, in case of a
breaking change.
Building, with the rust toolchain installed, can be done by running `cargo build`
in this directory. For compatibility reasons, the binary is renamed to `mycelium`.
Optionally, the `--release` flag can be used when building. This is recommended
if you are not just developing.
For more information about the project, please refer to [the README file in the
repository root](../README.md).

805
myceliumd/src/main.rs Normal file
View File

@@ -0,0 +1,805 @@
use std::io::{self, Read};
use std::net::Ipv4Addr;
use std::path::Path;
use std::sync::Arc;
use std::{
error::Error,
net::{IpAddr, SocketAddr},
path::PathBuf,
};
use std::{fmt::Display, str::FromStr};
use clap::{Args, Parser, Subcommand};
use mycelium::message::TopicConfig;
use serde::{Deserialize, Deserializer};
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[cfg(target_family = "unix")]
use tokio::signal::{self, unix::SignalKind};
use tokio::sync::Mutex;
use tracing::{debug, error, info, warn};
use crypto::PublicKey;
use mycelium::endpoint::Endpoint;
use mycelium::{crypto, Node};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
/// The default port on the underlay to listen on for incoming TCP connections.
const DEFAULT_TCP_LISTEN_PORT: u16 = 9651;
/// The default port on the underlay to listen on for incoming Quic connections.
const DEFAULT_QUIC_LISTEN_PORT: u16 = 9651;
/// The default port to use for IPv6 link local peer discovery (UDP).
const DEFAULT_PEER_DISCOVERY_PORT: u16 = 9650;
/// The default listening address for the HTTP API.
const DEFAULT_HTTP_API_SERVER_ADDRESS: SocketAddr =
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8989);
/// The default listening address for the JSON-RPC API.
const DEFAULT_JSONRPC_API_SERVER_ADDRESS: SocketAddr =
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8990);
const DEFAULT_KEY_FILE: &str = "priv_key.bin";
/// Default name of tun interface
#[cfg(not(target_os = "macos"))]
const TUN_NAME: &str = "mycelium";
/// Default name of tun interface
#[cfg(target_os = "macos")]
const TUN_NAME: &str = "utun0";
/// The logging formats that can be selected.
#[derive(Clone, PartialEq, Eq)]
enum LoggingFormat {
Compact,
Logfmt,
/// Same as Logfmt but with color statically disabled
Plain,
}
impl Display for LoggingFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
LoggingFormat::Compact => "compact",
LoggingFormat::Logfmt => "logfmt",
LoggingFormat::Plain => "plain",
}
)
}
}
impl FromStr for LoggingFormat {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"compact" => LoggingFormat::Compact,
"logfmt" => LoggingFormat::Logfmt,
"plain" => LoggingFormat::Plain,
_ => return Err("invalid logging format"),
})
}
}
#[derive(Parser)]
#[command(version)]
struct Cli {
/// Path to the private key file. This will be created if it does not exist. Default
/// [priv_key.bin].
#[arg(short = 'k', long = "key-file", global = true)]
key_file: Option<PathBuf>,
// Configuration file
#[arg(short = 'c', long = "config-file", global = true)]
config_file: Option<PathBuf>,
/// Enable debug logging. Does nothing if `--silent` is set.
#[arg(short = 'd', long = "debug", default_value_t = false)]
debug: bool,
/// Disable all logs except error logs.
#[arg(long = "silent", default_value_t = false)]
silent: bool,
/// The logging format to use. `logfmt` and `compact` is supported.
#[arg(long = "log-format", default_value_t = LoggingFormat::Compact)]
logging_format: LoggingFormat,
#[clap(flatten)]
node_args: NodeArguments,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Debug, Subcommand)]
pub enum Command {
/// Inspect a public key provided in hex format, or export the local public key if no key is
/// given.
Inspect {
/// Output in json format.
#[arg(long = "json")]
json: bool,
/// The key to inspect.
key: Option<String>,
},
/// Actions on the message subsystem
Message {
#[command(subcommand)]
command: MessageCommand,
},
/// Actions related to peers (list, remove, add)
Peers {
#[command(subcommand)]
command: PeersCommand,
},
/// Actions related to routes (selected, fallback, queried, no route)
Routes {
#[command(subcommand)]
command: RoutesCommand,
},
}
#[derive(Debug, Subcommand)]
pub enum MessageCommand {
Send {
/// Wait for a reply from the receiver.
#[arg(short = 'w', long = "wait", default_value_t = false)]
wait: bool,
/// An optional timeout to wait for. This does nothing if the `--wait` flag is not set. If
/// `--wait` is set and this flag isn't, wait forever for a reply.
#[arg(long = "timeout")]
timeout: Option<u64>,
/// Optional topic of the message. Receivers can filter on this to only receive messages
/// for a chosen topic.
#[arg(short = 't', long = "topic")]
topic: Option<String>,
/// Optional file to use as message body.
#[arg(long = "msg-path")]
msg_path: Option<PathBuf>,
/// Optional message ID to reply to.
#[arg(long = "reply-to")]
reply_to: Option<String>,
/// Destination of the message, either a hex encoded public key, or an IPv6 address in the
/// 400::/7 range.
destination: String,
/// The message to send. This is required if `--msg_path` is not set
message: Option<String>,
},
Receive {
/// An optional timeout to wait for a message. If this is not set, wait forever.
#[arg(long = "timeout")]
timeout: Option<u64>,
/// Optional topic of the message. Only messages with this topic will be received by this
/// command.
#[arg(short = 't', long = "topic")]
topic: Option<String>,
/// Optional file in which the message body will be saved.
#[arg(long = "msg-path")]
msg_path: Option<PathBuf>,
/// Don't print the metadata
#[arg(long = "raw")]
raw: bool,
},
}
#[derive(Debug, Subcommand)]
pub enum PeersCommand {
/// List the connected peers
List {
/// Print the peers list in JSON format
#[arg(long = "json", default_value_t = false)]
json: bool,
},
/// Add peer(s)
Add { peers: Vec<String> },
/// Remove peer(s)
Remove { peers: Vec<String> },
}
#[derive(Debug, Subcommand)]
pub enum RoutesCommand {
/// Print all selected routes
Selected {
/// Print selected routes in JSON format
#[arg(long = "json", default_value_t = false)]
json: bool,
},
/// Print all fallback routes
Fallback {
/// Print fallback routes in JSON format
#[arg(long = "json", default_value_t = false)]
json: bool,
},
/// Print the currently queried subnets
Queried {
/// Print queried subnets in JSON format
#[arg(long = "json", default_value_t = false)]
json: bool,
},
/// Print all subnets which are explicitly marked as not having a route
NoRoute {
/// Print subnets in JSON format
#[arg(long = "json", default_value_t = false)]
json: bool,
},
}
#[derive(Debug, Args)]
pub struct NodeArguments {
/// Peers to connect to.
#[arg(long = "peers", num_args = 1..)]
static_peers: Vec<Endpoint>,
/// Port to listen on for tcp connections.
#[arg(short = 't', long = "tcp-listen-port", default_value_t = DEFAULT_TCP_LISTEN_PORT)]
tcp_listen_port: u16,
/// Disable quic protocol for connecting to peers
#[arg(long = "disable-quic", default_value_t = false)]
disable_quic: bool,
/// Port to listen on for quic connections.
#[arg(short = 'q', long = "quic-listen-port", default_value_t = DEFAULT_QUIC_LISTEN_PORT)]
quic_listen_port: u16,
/// Port to use for link local peer discovery. This uses the UDP protocol.
#[arg(long = "peer-discovery-port", default_value_t = DEFAULT_PEER_DISCOVERY_PORT)]
peer_discovery_port: u16,
/// Disable peer discovery.
///
/// If this flag is passed, the automatic link local peer discovery will not be enabled, and
/// peers must be configured manually. If this is disabled on all local peers, communication
/// between them will go over configured external peers.
#[arg(long = "disable-peer-discovery", default_value_t = false)]
disable_peer_discovery: bool,
/// Address of the HTTP API server.
#[arg(long = "api-addr", default_value_t = DEFAULT_HTTP_API_SERVER_ADDRESS)]
api_addr: SocketAddr,
/// Address of the JSON-RPC API server.
#[arg(long = "jsonrpc-addr", default_value_t = DEFAULT_JSONRPC_API_SERVER_ADDRESS)]
jsonrpc_addr: SocketAddr,
/// Run without creating a TUN interface.
///
/// The system will participate in the network as usual, but won't be able to send out L3
/// packets. Inbound L3 traffic will be silently discarded. The message subsystem will still
/// work however.
#[arg(long = "no-tun", default_value_t = false)]
no_tun: bool,
/// Name to use for the TUN interface, if one is created.
///
/// Setting this only matters if a TUN interface is actually created, i.e. if the `--no-tun`
/// flag is **not** set. The name set here must be valid for the current platform, e.g. on OSX,
/// the name must start with `utun` and be followed by digits.
#[arg(long = "tun-name")]
tun_name: Option<String>,
/// The address on which to expose prometheus metrics, if desired.
///
/// Setting this flag will attempt to start an HTTP server on the provided address, to serve
/// prometheus metrics on the /metrics endpoint. If this flag is not set, metrics are also not
/// collected.
#[arg(long = "metrics-api-address")]
metrics_api_address: Option<SocketAddr>,
/// The firewall mark to set on the mycelium sockets.
///
/// This allows to identify packets that contain encapsulated mycelium packets so that
/// different routing policies can be applied to them.
/// This option only has an effect on Linux.
#[arg(long = "firewall-mark")]
firewall_mark: Option<u32>,
/// The amount of worker tasks to spawn to handle updates.
///
/// By default, updates are processed on a single task only. This is sufficient for most use
/// cases. In case you notice that the node can't keep up with the incoming updates (typically
/// because you are running a public node with a lot of connections), this value can be
/// increased to process updates in parallel.
#[arg(long = "update-workers", default_value_t = 1)]
update_workers: usize,
/// The topic configuration.
///
/// A .toml file containing topic configuration. This is a default action in case the topic is
/// not listed, and an explicit whitelist for allowed subnets/ips which are otherwise allowed
/// to use a topic.
#[arg(long = "topic-config")]
topic_config: Option<PathBuf>,
/// The cache directory for the mycelium CDN module
///
/// This directory will be used to cache reconstructed content blocks which were loaded through
/// the CDN functionallity for faster access next time.
#[arg(long = "cdn-cache")]
cdn_cache: Option<PathBuf>,
}
#[derive(Debug, Deserialize)]
pub struct MergedNodeConfig {
peers: Vec<Endpoint>,
tcp_listen_port: u16,
disable_quic: bool,
quic_listen_port: u16,
peer_discovery_port: u16,
disable_peer_discovery: bool,
api_addr: SocketAddr,
jsonrpc_addr: SocketAddr,
no_tun: bool,
tun_name: String,
metrics_api_address: Option<SocketAddr>,
firewall_mark: Option<u32>,
update_workers: usize,
topic_config: Option<PathBuf>,
cdn_cache: Option<PathBuf>,
}
#[derive(Debug, Deserialize, Default)]
struct MyceliumConfig {
#[serde(deserialize_with = "deserialize_optional_endpoint_str_from_toml")]
peers: Option<Vec<Endpoint>>,
tcp_listen_port: Option<u16>,
disable_quic: Option<bool>,
quic_listen_port: Option<u16>,
no_tun: Option<bool>,
tun_name: Option<String>,
disable_peer_discovery: Option<bool>,
peer_discovery_port: Option<u16>,
api_addr: Option<SocketAddr>,
jsonrpc_addr: Option<SocketAddr>,
metrics_api_address: Option<SocketAddr>,
firewall_mark: Option<u32>,
update_workers: Option<usize>,
topic_config: Option<PathBuf>,
cdn_cache: Option<PathBuf>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();
// Init default configuration
let mut mycelium_config = MyceliumConfig::default();
// Load configuration file
if let Some(config_file_path) = &cli.config_file {
if Path::new(config_file_path).exists() {
let config = config::Config::builder()
.add_source(config::File::new(
config_file_path.to_str().unwrap(),
config::FileFormat::Toml,
))
.build()?;
mycelium_config = config.try_deserialize()?;
} else {
let error_msg = format!("Config file {config_file_path:?} not found");
return Err(io::Error::new(io::ErrorKind::NotFound, error_msg).into());
}
} else if let Some(mut conf) = dirs::config_dir() {
// Windows: %APPDATA%/ThreeFold Tech/Mycelium/mycelium.conf
#[cfg(target_os = "windows")]
{
conf = conf
.join("ThreeFold Tech")
.join("Mycelium")
.join("mycelium.toml")
};
// Linux: $HOME/.config/mycelium/mycelium.conf
#[allow(clippy::unnecessary_operation)]
#[cfg(target_os = "linux")]
{
conf = conf.join("mycelium").join("mycelium.toml")
};
// MacOS: $HOME/Library/Application Support/ThreeFold Tech/Mycelium/mycelium.conf
#[cfg(target_os = "macos")]
{
conf = conf
.join("ThreeFold Tech")
.join("Mycelium")
.join("mycelium.toml")
};
if conf.exists() {
info!(
conf_dir = conf.to_str().unwrap(),
"Mycelium is starting with configuration file",
);
let config = config::Config::builder()
.add_source(config::File::new(
conf.to_str().unwrap(),
config::FileFormat::Toml,
))
.build()?;
mycelium_config = config.try_deserialize()?;
}
}
let level = if cli.silent {
tracing::Level::ERROR
} else if cli.debug {
tracing::Level::DEBUG
} else {
tracing::Level::INFO
};
tracing_subscriber::registry()
.with(
EnvFilter::builder()
.with_default_directive(level.into())
.from_env()
.expect("invalid RUST_LOG"),
)
.with(
(cli.logging_format == LoggingFormat::Compact)
.then(|| tracing_subscriber::fmt::Layer::new().compact()),
)
.with((cli.logging_format == LoggingFormat::Logfmt).then(tracing_logfmt::layer))
.with((cli.logging_format == LoggingFormat::Plain).then(|| {
tracing_logfmt::builder()
// Explicitly force color off
.with_ansi_color(false)
.layer()
}))
.init();
let key_path = cli
.key_file
.unwrap_or_else(|| PathBuf::from(DEFAULT_KEY_FILE));
match cli.command {
None => {
let merged_config = merge_config(cli.node_args, mycelium_config);
let topic_config = merged_config.topic_config.as_ref().and_then(|path| {
let mut content = String::new();
let mut file = std::fs::File::open(path).ok()?;
file.read_to_string(&mut content).ok()?;
toml::from_str::<TopicConfig>(&content).ok()
});
if topic_config.is_some() {
info!(path = ?merged_config.topic_config, "Loaded topic cofig");
}
let node_keys = get_node_keys(&key_path).await?;
let node_secret_key = if let Some((node_secret_key, _)) = node_keys {
node_secret_key
} else {
warn!("Node key file {key_path:?} not found, generating new keys");
let secret_key = crypto::SecretKey::new();
save_key_file(&secret_key, &key_path).await?;
secret_key
};
let _api = if let Some(metrics_api_addr) = merged_config.metrics_api_address {
let metrics = mycelium_metrics::PrometheusExporter::new();
let config = mycelium::Config {
node_key: node_secret_key,
peers: merged_config.peers,
no_tun: merged_config.no_tun,
tcp_listen_port: merged_config.tcp_listen_port,
quic_listen_port: if merged_config.disable_quic {
None
} else {
Some(merged_config.quic_listen_port)
},
peer_discovery_port: if merged_config.disable_peer_discovery {
None
} else {
Some(merged_config.peer_discovery_port)
},
tun_name: merged_config.tun_name,
private_network_config: None,
metrics: metrics.clone(),
firewall_mark: merged_config.firewall_mark,
update_workers: merged_config.update_workers,
topic_config,
cdn_cache: merged_config.cdn_cache,
};
metrics.spawn(metrics_api_addr);
let node = Arc::new(Mutex::new(Node::new(config).await?));
let http_api = mycelium_api::Http::spawn(node.clone(), merged_config.api_addr);
// Initialize the JSON-RPC server
let rpc_api =
mycelium_api::rpc::JsonRpc::spawn(node, merged_config.jsonrpc_addr).await;
(http_api, rpc_api)
} else {
let config = mycelium::Config {
node_key: node_secret_key,
peers: merged_config.peers,
no_tun: merged_config.no_tun,
tcp_listen_port: merged_config.tcp_listen_port,
quic_listen_port: if merged_config.disable_quic {
None
} else {
Some(merged_config.quic_listen_port)
},
peer_discovery_port: if merged_config.disable_peer_discovery {
None
} else {
Some(merged_config.peer_discovery_port)
},
tun_name: merged_config.tun_name,
private_network_config: None,
metrics: mycelium_metrics::NoMetrics,
firewall_mark: merged_config.firewall_mark,
update_workers: merged_config.update_workers,
topic_config,
cdn_cache: merged_config.cdn_cache,
};
let node = Arc::new(Mutex::new(Node::new(config).await?));
let http_api = mycelium_api::Http::spawn(node.clone(), merged_config.api_addr);
// Initialize the JSON-RPC server
let rpc_api =
mycelium_api::rpc::JsonRpc::spawn(node, merged_config.jsonrpc_addr).await;
(http_api, rpc_api)
};
// TODO: put in dedicated file so we can only rely on certain signals on unix platforms
#[cfg(target_family = "unix")]
{
let mut sigint = signal::unix::signal(SignalKind::interrupt())
.expect("Can install SIGINT handler");
let mut sigterm = signal::unix::signal(SignalKind::terminate())
.expect("Can install SIGTERM handler");
tokio::select! {
_ = sigint.recv() => { }
_ = sigterm.recv() => { }
}
}
#[cfg(not(target_family = "unix"))]
{
if let Err(e) = tokio::signal::ctrl_c().await {
error!("Failed to wait for SIGINT: {e}");
}
}
}
Some(cmd) => match cmd {
Command::Inspect { json, key } => {
let node_keys = get_node_keys(&key_path).await?;
let key = if let Some(key) = key {
PublicKey::try_from(key.as_str())?
} else if let Some((_, node_pub_key)) = node_keys {
node_pub_key
} else {
error!("No key to inspect provided and no key found at {key_path:?}");
return Err(io::Error::new(
io::ErrorKind::NotFound,
"no key to inspect and key file not found",
)
.into());
};
mycelium_cli::inspect(key, json)?;
return Ok(());
}
Command::Message { command } => match command {
MessageCommand::Send {
wait,
timeout,
topic,
msg_path,
reply_to,
destination,
message,
} => {
return mycelium_cli::send_msg(
destination,
message,
wait,
timeout,
reply_to,
topic,
msg_path,
cli.node_args.api_addr,
)
.await
}
MessageCommand::Receive {
timeout,
topic,
msg_path,
raw,
} => {
return mycelium_cli::recv_msg(
timeout,
topic,
msg_path,
raw,
cli.node_args.api_addr,
)
.await
}
},
Command::Peers { command } => match command {
PeersCommand::List { json } => {
return mycelium_cli::list_peers(cli.node_args.api_addr, json).await;
}
PeersCommand::Add { peers } => {
return mycelium_cli::add_peers(cli.node_args.api_addr, peers).await;
}
PeersCommand::Remove { peers } => {
return mycelium_cli::remove_peers(cli.node_args.api_addr, peers).await;
}
},
Command::Routes { command } => match command {
RoutesCommand::Selected { json } => {
return mycelium_cli::list_selected_routes(cli.node_args.api_addr, json).await;
}
RoutesCommand::Fallback { json } => {
return mycelium_cli::list_fallback_routes(cli.node_args.api_addr, json).await;
}
RoutesCommand::Queried { json } => {
return mycelium_cli::list_queried_subnets(cli.node_args.api_addr, json).await;
}
RoutesCommand::NoRoute { json } => {
return mycelium_cli::list_no_route_entries(cli.node_args.api_addr, json).await;
}
},
},
}
Ok(())
}
async fn get_node_keys(
key_path: &PathBuf,
) -> Result<Option<(crypto::SecretKey, crypto::PublicKey)>, io::Error> {
if key_path.exists() {
let sk = load_key_file(key_path).await?;
let pk = crypto::PublicKey::from(&sk);
debug!("Loaded key file at {key_path:?}");
Ok(Some((sk, pk)))
} else {
Ok(None)
}
}
async fn load_key_file<T>(path: &Path) -> Result<T, io::Error>
where
T: From<[u8; 32]>,
{
let mut file = File::open(path).await?;
let mut secret_bytes = [0u8; 32];
file.read_exact(&mut secret_bytes).await?;
Ok(T::from(secret_bytes))
}
async fn save_key_file(key: &crypto::SecretKey, path: &Path) -> io::Result<()> {
#[cfg(target_family = "unix")]
{
use tokio::fs::OpenOptions;
let mut file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.mode(0o600) // rw by the owner, not readable by group or others
.open(path)
.await?;
file.write_all(key.as_bytes()).await?;
}
#[cfg(not(target_family = "unix"))]
{
let mut file = File::create(path).await?;
file.write_all(key.as_bytes()).await?;
}
Ok(())
}
fn merge_config(cli_args: NodeArguments, file_config: MyceliumConfig) -> MergedNodeConfig {
MergedNodeConfig {
peers: if !cli_args.static_peers.is_empty() {
cli_args.static_peers
} else {
file_config.peers.unwrap_or_default()
},
tcp_listen_port: if cli_args.tcp_listen_port != DEFAULT_TCP_LISTEN_PORT {
cli_args.tcp_listen_port
} else {
file_config
.tcp_listen_port
.unwrap_or(DEFAULT_TCP_LISTEN_PORT)
},
disable_quic: cli_args.disable_quic || file_config.disable_quic.unwrap_or(false),
quic_listen_port: if cli_args.quic_listen_port != DEFAULT_QUIC_LISTEN_PORT {
cli_args.quic_listen_port
} else {
file_config
.quic_listen_port
.unwrap_or(DEFAULT_QUIC_LISTEN_PORT)
},
peer_discovery_port: if cli_args.peer_discovery_port != DEFAULT_PEER_DISCOVERY_PORT {
cli_args.peer_discovery_port
} else {
file_config
.peer_discovery_port
.unwrap_or(DEFAULT_PEER_DISCOVERY_PORT)
},
disable_peer_discovery: cli_args.disable_peer_discovery
|| file_config.disable_peer_discovery.unwrap_or(false),
api_addr: if cli_args.api_addr != DEFAULT_HTTP_API_SERVER_ADDRESS {
cli_args.api_addr
} else {
file_config
.api_addr
.unwrap_or(DEFAULT_HTTP_API_SERVER_ADDRESS)
},
jsonrpc_addr: if cli_args.jsonrpc_addr != DEFAULT_JSONRPC_API_SERVER_ADDRESS {
cli_args.jsonrpc_addr
} else {
file_config
.jsonrpc_addr
.unwrap_or(DEFAULT_JSONRPC_API_SERVER_ADDRESS)
},
no_tun: cli_args.no_tun || file_config.no_tun.unwrap_or(false),
tun_name: if let Some(tun_name_cli) = cli_args.tun_name {
tun_name_cli
} else if let Some(tun_name_config) = file_config.tun_name {
tun_name_config
} else {
TUN_NAME.to_string()
},
metrics_api_address: cli_args
.metrics_api_address
.or(file_config.metrics_api_address),
firewall_mark: cli_args.firewall_mark.or(file_config.firewall_mark),
update_workers: if cli_args.update_workers != 1 {
cli_args.update_workers
} else {
file_config.update_workers.unwrap_or(1)
},
topic_config: cli_args.topic_config.or(file_config.topic_config),
cdn_cache: cli_args.cdn_cache.or(file_config.cdn_cache),
}
}
/// Deserialize an optional list of endpoints from TOML format. The endpoints can be provided
/// either as a list `[...]`, or in case there is only 1 endpoint, it can also be provided as a
/// single string element. If no value is provided, it returns None.
fn deserialize_optional_endpoint_str_from_toml<'de, D>(
deserializer: D,
) -> Result<Option<Vec<Endpoint>>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrVec {
String(String),
Vec(Vec<String>),
}
Ok(match Option::<StringOrVec>::deserialize(deserializer)? {
Some(StringOrVec::Vec(v)) => Some(
v.into_iter()
.map(|s| {
<Endpoint as std::str::FromStr>::from_str(&s).map_err(serde::de::Error::custom)
})
.collect::<Result<Vec<_>, _>>()?,
),
Some(StringOrVec::String(s)) => Some(vec![
<Endpoint as std::str::FromStr>::from_str(&s).map_err(serde::de::Error::custom)?
]),
None => None,
})
}