mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-11-10 21:20:56 +08:00
2014 lines
60 KiB
Rust
2014 lines
60 KiB
Rust
//! Complete EasyTier TOML configuration model.
|
|
|
|
use std::{
|
|
net::{IpAddr, SocketAddr},
|
|
path::PathBuf,
|
|
sync::{Arc, Mutex},
|
|
};
|
|
|
|
use super::normalize_secure_mode_config;
|
|
pub use super::{EncryptionAlgorithm, gateway::PortForwardConfig};
|
|
use anyhow::Context;
|
|
#[cfg(feature = "rich-config-errors")]
|
|
use ariadne::{CharSet, Config as AriadneConfig, IndexType, Label, Report, ReportKind, Source};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::proto::{
|
|
acl::Acl,
|
|
common::{CompressionAlgoPb, SecureModeConfig},
|
|
};
|
|
|
|
pub const DEFAULT_ET_DNS_ZONE: &str = "et.net.";
|
|
|
|
pub type Flags = crate::proto::common::FlagsInConfig;
|
|
|
|
pub(crate) fn default_instance_name() -> String {
|
|
"default".to_owned()
|
|
}
|
|
|
|
pub fn gen_default_flags() -> Flags {
|
|
#[allow(deprecated)]
|
|
Flags {
|
|
default_protocol: "tcp".to_string(),
|
|
dev_name: "".to_string(),
|
|
enable_encryption: true,
|
|
enable_ipv6: true,
|
|
mtu: 1380,
|
|
latency_first: false,
|
|
enable_exit_node: false,
|
|
proxy_forward_by_system: false,
|
|
no_tun: false,
|
|
use_smoltcp: false,
|
|
relay_network_whitelist: "*".to_string(),
|
|
disable_p2p: false,
|
|
p2p_only: false,
|
|
lazy_p2p: false,
|
|
relay_all_peer_rpc: false,
|
|
disable_tcp_hole_punching: false,
|
|
disable_udp_hole_punching: false,
|
|
multi_thread: true,
|
|
data_compress_algo: CompressionAlgoPb::None.into(),
|
|
bind_device: true,
|
|
enable_kcp_proxy: false,
|
|
disable_kcp_input: false,
|
|
disable_relay_kcp: false,
|
|
enable_relay_foreign_network_kcp: false,
|
|
accept_dns: false,
|
|
private_mode: false,
|
|
enable_quic_proxy: false,
|
|
disable_quic_input: false,
|
|
disable_relay_quic: false,
|
|
enable_relay_foreign_network_quic: false,
|
|
foreign_relay_bps_limit: u64::MAX,
|
|
multi_thread_count: 2,
|
|
encryption_algorithm: EncryptionAlgorithm::default().to_string(),
|
|
disable_sym_hole_punching: false,
|
|
tld_dns_zone: DEFAULT_ET_DNS_ZONE.to_string(),
|
|
|
|
quic_listen_port: u32::MAX,
|
|
need_p2p: false,
|
|
instance_recv_bps_limit: u64::MAX,
|
|
disable_upnp: false,
|
|
disable_relay_data: false,
|
|
prefer_peer_relay: false,
|
|
enable_udp_broadcast_relay: false,
|
|
socket_mark: None,
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "config-write")]
|
|
macro_rules! define_flags_diff {
|
|
(
|
|
fields: [$($field:ident),* $(,)?],
|
|
u64s: [$($u64_field:ident),* $(,)?],
|
|
enums: [$($enum_field:ident),* $(,)?]
|
|
) => {
|
|
#[allow(deprecated)]
|
|
fn flags_diff_from_default(flags: &Flags) -> serde_json::Map<String, serde_json::Value> {
|
|
let defaults = gen_default_flags();
|
|
let mut changed = serde_json::Map::new();
|
|
$(
|
|
if flags.$field != defaults.$field {
|
|
changed.insert(
|
|
stringify!($field).to_owned(),
|
|
serde_json::to_value(&flags.$field)
|
|
.expect("FlagsInConfig field should serialize to JSON"),
|
|
);
|
|
}
|
|
)*
|
|
$(
|
|
if flags.$u64_field != defaults.$u64_field {
|
|
changed.insert(
|
|
stringify!($u64_field).to_owned(),
|
|
serde_json::json!(flags.$u64_field.to_string()),
|
|
);
|
|
}
|
|
)*
|
|
$(
|
|
if flags.$enum_field != defaults.$enum_field {
|
|
let value = CompressionAlgoPb::try_from(flags.$enum_field)
|
|
.map(|value| serde_json::to_value(value).expect("enum should serialize"))
|
|
.unwrap_or_else(|_| serde_json::json!(flags.$enum_field));
|
|
changed.insert(stringify!($enum_field).to_owned(), value);
|
|
}
|
|
)*
|
|
changed
|
|
}
|
|
|
|
#[cfg(all(test, feature = "config-write"))]
|
|
const FLAGS_DIFF_FIELDS: &[&str] = &[
|
|
$(stringify!($field),)*
|
|
$(stringify!($u64_field),)*
|
|
$(stringify!($enum_field),)*
|
|
];
|
|
};
|
|
}
|
|
|
|
#[cfg(feature = "config-write")]
|
|
define_flags_diff! {
|
|
fields: [
|
|
default_protocol,
|
|
dev_name,
|
|
enable_encryption,
|
|
enable_ipv6,
|
|
mtu,
|
|
latency_first,
|
|
enable_exit_node,
|
|
no_tun,
|
|
use_smoltcp,
|
|
relay_network_whitelist,
|
|
disable_p2p,
|
|
relay_all_peer_rpc,
|
|
disable_udp_hole_punching,
|
|
multi_thread,
|
|
bind_device,
|
|
enable_kcp_proxy,
|
|
disable_kcp_input,
|
|
disable_relay_kcp,
|
|
proxy_forward_by_system,
|
|
accept_dns,
|
|
private_mode,
|
|
enable_quic_proxy,
|
|
disable_quic_input,
|
|
disable_relay_quic,
|
|
quic_listen_port,
|
|
multi_thread_count,
|
|
enable_relay_foreign_network_kcp,
|
|
enable_relay_foreign_network_quic,
|
|
encryption_algorithm,
|
|
disable_sym_hole_punching,
|
|
tld_dns_zone,
|
|
p2p_only,
|
|
disable_tcp_hole_punching,
|
|
lazy_p2p,
|
|
need_p2p,
|
|
disable_upnp,
|
|
disable_relay_data,
|
|
prefer_peer_relay,
|
|
enable_udp_broadcast_relay,
|
|
socket_mark,
|
|
],
|
|
u64s: [foreign_relay_bps_limit, instance_recv_bps_limit],
|
|
enums: [data_compress_algo]
|
|
}
|
|
|
|
#[auto_impl::auto_impl(Box, &)]
|
|
pub trait ConfigLoader: Send + Sync {
|
|
fn get_id(&self) -> uuid::Uuid;
|
|
fn set_id(&self, id: uuid::Uuid);
|
|
|
|
fn get_hostname(&self) -> String;
|
|
fn set_hostname(&self, name: Option<String>);
|
|
|
|
fn get_inst_name(&self) -> String;
|
|
fn set_inst_name(&self, name: String);
|
|
|
|
fn get_netns(&self) -> Option<String>;
|
|
fn set_netns(&self, ns: Option<String>);
|
|
|
|
fn get_ipv4(&self) -> Option<cidr::Ipv4Inet>;
|
|
fn set_ipv4(&self, addr: Option<cidr::Ipv4Inet>);
|
|
|
|
fn get_ipv6(&self) -> Option<cidr::Ipv6Inet>;
|
|
fn set_ipv6(&self, addr: Option<cidr::Ipv6Inet>);
|
|
|
|
fn get_ipv6_public_addr_provider(&self) -> bool;
|
|
fn set_ipv6_public_addr_provider(&self, enabled: bool);
|
|
|
|
fn get_ipv6_public_addr_auto(&self) -> bool;
|
|
fn set_ipv6_public_addr_auto(&self, enabled: bool);
|
|
|
|
fn get_ipv6_public_addr_prefix(&self) -> Option<cidr::Ipv6Cidr>;
|
|
fn set_ipv6_public_addr_prefix(&self, prefix: Option<cidr::Ipv6Cidr>);
|
|
|
|
fn get_dhcp(&self) -> bool;
|
|
fn set_dhcp(&self, dhcp: bool);
|
|
|
|
fn add_proxy_cidr(
|
|
&self,
|
|
cidr: cidr::Ipv4Cidr,
|
|
mapped_cidr: Option<cidr::Ipv4Cidr>,
|
|
) -> Result<(), anyhow::Error>;
|
|
fn remove_proxy_cidr(&self, cidr: cidr::Ipv4Cidr);
|
|
fn clear_proxy_cidrs(&self);
|
|
fn get_proxy_cidrs(&self) -> Vec<ProxyNetworkConfig>;
|
|
|
|
fn get_network_identity(&self) -> NetworkIdentity;
|
|
fn set_network_identity(&self, identity: NetworkIdentity);
|
|
|
|
fn get_listener_uris(&self) -> Vec<url::Url>;
|
|
|
|
fn get_peers(&self) -> Vec<PeerConfig>;
|
|
fn set_peers(&self, peers: Vec<PeerConfig>);
|
|
|
|
fn get_listeners(&self) -> Option<Vec<url::Url>>;
|
|
fn set_listeners(&self, listeners: Vec<url::Url>);
|
|
|
|
fn get_mapped_listeners(&self) -> Vec<url::Url>;
|
|
fn set_mapped_listeners(&self, listeners: Option<Vec<url::Url>>);
|
|
|
|
fn get_vpn_portal_config(&self) -> Option<VpnPortalConfig>;
|
|
fn set_vpn_portal_config(&self, config: VpnPortalConfig);
|
|
|
|
fn get_flags(&self) -> Flags;
|
|
fn set_flags(&self, flags: Flags);
|
|
|
|
fn get_exit_nodes(&self) -> Vec<IpAddr>;
|
|
fn set_exit_nodes(&self, nodes: Vec<IpAddr>);
|
|
|
|
fn get_routes(&self) -> Option<Vec<cidr::Ipv4Cidr>>;
|
|
fn set_routes(&self, routes: Option<Vec<cidr::Ipv4Cidr>>);
|
|
|
|
fn get_socks5_portal(&self) -> Option<url::Url>;
|
|
fn set_socks5_portal(&self, addr: Option<url::Url>);
|
|
|
|
fn get_port_forwards(&self) -> Vec<PortForwardConfig>;
|
|
fn set_port_forwards(&self, forwards: Vec<PortForwardConfig>);
|
|
|
|
fn get_acl(&self) -> Option<Acl>;
|
|
fn set_acl(&self, acl: Option<Acl>);
|
|
|
|
fn get_tcp_whitelist(&self) -> Vec<String>;
|
|
fn set_tcp_whitelist(&self, whitelist: Vec<String>);
|
|
|
|
fn get_udp_whitelist(&self) -> Vec<String>;
|
|
fn set_udp_whitelist(&self, whitelist: Vec<String>);
|
|
|
|
fn get_stun_servers(&self) -> Option<Vec<String>>;
|
|
fn set_stun_servers(&self, servers: Option<Vec<String>>);
|
|
|
|
fn get_tcp_stun_servers(&self) -> Option<Vec<String>> {
|
|
None
|
|
}
|
|
fn set_tcp_stun_servers(&self, _servers: Option<Vec<String>>) {}
|
|
|
|
fn get_stun_servers_v6(&self) -> Option<Vec<String>>;
|
|
fn set_stun_servers_v6(&self, servers: Option<Vec<String>>);
|
|
|
|
fn get_secure_mode(&self) -> Option<SecureModeConfig>;
|
|
fn set_secure_mode(&self, secure_mode: Option<SecureModeConfig>);
|
|
|
|
fn get_credential_file(&self) -> Option<std::path::PathBuf> {
|
|
None
|
|
}
|
|
fn set_credential_file(&self, _path: Option<std::path::PathBuf>) {}
|
|
|
|
fn get_managed_credentials(&self) -> Vec<ManagedCredentialConfig> {
|
|
Vec::new()
|
|
}
|
|
fn set_managed_credentials(&self, _credentials: Vec<ManagedCredentialConfig>) {}
|
|
|
|
fn get_network_config_source(&self) -> ConfigSource {
|
|
ConfigSource::User
|
|
}
|
|
fn set_network_config_source(&self, _source: Option<ConfigSource>) {}
|
|
|
|
fn dump(&self) -> String;
|
|
fn dump_redacted(&self) -> String {
|
|
self.dump()
|
|
}
|
|
}
|
|
|
|
pub trait LoggingConfigLoader {
|
|
fn get_file_logger_config(&self) -> FileLoggerConfig;
|
|
|
|
fn get_console_logger_config(&self) -> ConsoleLoggerConfig;
|
|
}
|
|
|
|
use super::NetworkSecretDigest;
|
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
pub struct NetworkIdentity {
|
|
pub network_name: String,
|
|
pub network_secret: Option<String>,
|
|
#[serde(skip)]
|
|
pub network_secret_digest: Option<NetworkSecretDigest>,
|
|
}
|
|
|
|
impl From<super::NetworkIdentity> for NetworkIdentity {
|
|
fn from(value: super::NetworkIdentity) -> Self {
|
|
Self {
|
|
network_name: value.network_name,
|
|
network_secret: value.network_secret,
|
|
network_secret_digest: value.network_secret_digest,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<&NetworkIdentity> for super::NetworkIdentity {
|
|
fn from(value: &NetworkIdentity) -> Self {
|
|
Self {
|
|
network_name: value.network_name.clone(),
|
|
network_secret: value.network_secret.clone(),
|
|
network_secret_digest: value.network_secret_digest,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<NetworkIdentity> for super::NetworkIdentity {
|
|
fn from(value: NetworkIdentity) -> Self {
|
|
Self {
|
|
network_name: value.network_name,
|
|
network_secret: value.network_secret,
|
|
network_secret_digest: value.network_secret_digest,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ConfigSource {
|
|
#[default]
|
|
User,
|
|
Web,
|
|
}
|
|
|
|
impl ConfigSource {
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::User => "user",
|
|
Self::Web => "web",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::str::FromStr for ConfigSource {
|
|
type Err = String;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s {
|
|
"user" => Ok(Self::User),
|
|
"web" => Ok(Self::Web),
|
|
other => Err(format!("unknown network config source: {other}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
|
struct ConfigSourceConfig {
|
|
source: ConfigSource,
|
|
}
|
|
|
|
impl PartialEq for NetworkIdentity {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
super::NetworkIdentity::from(self) == super::NetworkIdentity::from(other)
|
|
}
|
|
}
|
|
|
|
impl Eq for NetworkIdentity {}
|
|
|
|
impl std::hash::Hash for NetworkIdentity {
|
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
std::hash::Hash::hash(&super::NetworkIdentity::from(self), state);
|
|
}
|
|
}
|
|
|
|
impl NetworkIdentity {
|
|
pub fn new(network_name: String, network_secret: String) -> Self {
|
|
super::NetworkIdentity::new(network_name, network_secret).into()
|
|
}
|
|
|
|
/// Create a NetworkIdentity for a credential node (no network_secret).
|
|
/// The node identifies by network_name only and authenticates via credential keypair.
|
|
pub fn new_credential(network_name: String) -> Self {
|
|
super::NetworkIdentity::new_credential(network_name).into()
|
|
}
|
|
}
|
|
|
|
impl Default for NetworkIdentity {
|
|
fn default() -> Self {
|
|
super::NetworkIdentity::default().into()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
|
pub struct PeerConfig {
|
|
pub uri: url::Url,
|
|
pub peer_public_key: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
|
pub struct ProxyNetworkConfig {
|
|
pub cidr: cidr::Ipv4Cidr, // the CIDR of the proxy network
|
|
pub mapped_cidr: Option<cidr::Ipv4Cidr>, // allow remap the proxy CIDR to another CIDR
|
|
pub allow: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
|
|
pub struct FileLoggerConfig {
|
|
pub level: Option<String>,
|
|
pub file: Option<String>,
|
|
pub dir: Option<String>,
|
|
pub size_mb: Option<u64>,
|
|
pub count: Option<usize>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
|
|
pub struct ConsoleLoggerConfig {
|
|
pub level: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, bon::Builder)]
|
|
pub struct LoggingConfig {
|
|
#[builder(into)]
|
|
pub file_logger: Option<FileLoggerConfig>,
|
|
#[builder(into)]
|
|
pub console_logger: Option<ConsoleLoggerConfig>,
|
|
}
|
|
|
|
impl LoggingConfigLoader for &LoggingConfig {
|
|
fn get_file_logger_config(&self) -> FileLoggerConfig {
|
|
self.file_logger.clone().unwrap_or_default()
|
|
}
|
|
|
|
fn get_console_logger_config(&self) -> ConsoleLoggerConfig {
|
|
self.console_logger.clone().unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, Serialize, PartialEq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct VpnPortalConfig {
|
|
pub wireguard_listen: SocketAddr,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub wireguard_private_key: Option<String>,
|
|
#[serde(default)]
|
|
pub clients: Vec<VpnPortalClientConfig>,
|
|
}
|
|
|
|
impl std::fmt::Debug for VpnPortalConfig {
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
formatter
|
|
.debug_struct("VpnPortalConfig")
|
|
.field("wireguard_listen", &self.wireguard_listen)
|
|
.field(
|
|
"wireguard_private_key",
|
|
&self.wireguard_private_key.as_ref().map(|_| "<redacted>"),
|
|
)
|
|
.field("clients", &self.clients)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct VpnPortalClientConfig {
|
|
pub name: String,
|
|
pub virtual_ip: cidr::Ipv4Inet,
|
|
#[serde(default)]
|
|
pub groups: Vec<String>,
|
|
}
|
|
|
|
fn default_true() -> bool {
|
|
true
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, Serialize, PartialEq, Eq)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ManagedCredentialConfig {
|
|
pub credential_id: String,
|
|
pub credential_secret: String,
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub groups: Vec<String>,
|
|
#[serde(default)]
|
|
pub allow_relay: bool,
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub allowed_proxy_cidrs: Vec<String>,
|
|
pub expiry_unix: i64,
|
|
#[serde(default = "default_true")]
|
|
pub reusable: bool,
|
|
}
|
|
|
|
impl std::fmt::Debug for ManagedCredentialConfig {
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
formatter
|
|
.debug_struct("ManagedCredentialConfig")
|
|
.field("credential_id", &self.credential_id)
|
|
.field("credential_secret", &"<redacted>")
|
|
.field("groups", &self.groups)
|
|
.field("allow_relay", &self.allow_relay)
|
|
.field("allowed_proxy_cidrs", &self.allowed_proxy_cidrs)
|
|
.field("expiry_unix", &self.expiry_unix)
|
|
.field("reusable", &self.reusable)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
#[cfg_attr(feature = "config-write", derive(Serialize))]
|
|
struct Config {
|
|
netns: Option<String>,
|
|
hostname: Option<String>,
|
|
instance_name: Option<String>,
|
|
instance_id: Option<uuid::Uuid>,
|
|
ipv4: Option<String>,
|
|
ipv6: Option<String>,
|
|
ipv6_public_addr_provider: Option<bool>,
|
|
ipv6_public_addr_auto: Option<bool>,
|
|
ipv6_public_addr_prefix: Option<String>,
|
|
dhcp: Option<bool>,
|
|
network_identity: Option<NetworkIdentity>,
|
|
listeners: Option<Vec<url::Url>>,
|
|
mapped_listeners: Option<Vec<url::Url>>,
|
|
exit_nodes: Option<Vec<IpAddr>>,
|
|
|
|
peer: Option<Vec<PeerConfig>>,
|
|
proxy_network: Option<Vec<ProxyNetworkConfig>>,
|
|
|
|
vpn_portal_config: Option<VpnPortalConfig>,
|
|
|
|
routes: Option<Vec<cidr::Ipv4Cidr>>,
|
|
|
|
socks5_proxy: Option<url::Url>,
|
|
|
|
port_forward: Option<Vec<PortForwardConfig>>,
|
|
|
|
secure_mode: Option<SecureModeConfig>,
|
|
|
|
flags: Option<serde_json::Map<String, serde_json::Value>>,
|
|
|
|
#[serde(skip)]
|
|
flags_struct: Option<Flags>,
|
|
|
|
acl: Option<Acl>,
|
|
|
|
tcp_whitelist: Option<Vec<String>>,
|
|
udp_whitelist: Option<Vec<String>>,
|
|
stun_servers: Option<Vec<String>>,
|
|
tcp_stun_servers: Option<Vec<String>>,
|
|
stun_servers_v6: Option<Vec<String>>,
|
|
|
|
credential_file: Option<PathBuf>,
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
managed_credentials: Vec<ManagedCredentialConfig>,
|
|
source: Option<ConfigSourceConfig>,
|
|
}
|
|
|
|
#[cfg(feature = "rich-config-errors")]
|
|
fn format_toml_parse_error(source_name: &str, config_str: &str, error: &toml::de::Error) -> String {
|
|
let message = format!("failed to parse config TOML from {source_name}");
|
|
|
|
let Some(span) = error.span() else {
|
|
return format!("{message}\ndetail: {error}");
|
|
};
|
|
|
|
let mut output = Vec::new();
|
|
let report = Report::build(ReportKind::Error, (source_name, span.clone()))
|
|
.with_config(
|
|
AriadneConfig::default()
|
|
.with_color(false)
|
|
.with_char_set(CharSet::Ascii)
|
|
.with_index_type(IndexType::Byte),
|
|
)
|
|
.with_message(&message)
|
|
.with_label(Label::new((source_name, span)).with_message(error.message()))
|
|
.finish();
|
|
|
|
if report
|
|
.write((source_name, Source::from(config_str)), &mut output)
|
|
.is_ok()
|
|
{
|
|
String::from_utf8_lossy(&output).into_owned()
|
|
} else {
|
|
format!("{message}\ndetail: {error}")
|
|
}
|
|
}
|
|
|
|
#[cfg(not(feature = "rich-config-errors"))]
|
|
fn format_toml_parse_error(
|
|
source_name: &str,
|
|
_config_str: &str,
|
|
error: &toml::de::Error,
|
|
) -> String {
|
|
format!("failed to parse config TOML from {source_name}: {error}")
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct TomlConfig {
|
|
config: Arc<Mutex<Config>>,
|
|
}
|
|
|
|
impl Default for TomlConfig {
|
|
fn default() -> Self {
|
|
TomlConfig::new_from_str("").unwrap()
|
|
}
|
|
}
|
|
|
|
impl TomlConfig {
|
|
fn normalize_config_source(config: &mut Config) {
|
|
if matches!(
|
|
config.source.as_ref().map(|source| source.source),
|
|
Some(ConfigSource::User)
|
|
) {
|
|
config.source = None;
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "config-write")]
|
|
fn config_for_dump(&self) -> Config {
|
|
let mut config = self.config.lock().unwrap().clone();
|
|
Self::normalize_config_source(&mut config);
|
|
config.flags = Some(flags_diff_from_default(&self.get_flags()));
|
|
config
|
|
}
|
|
|
|
#[cfg(feature = "config-write")]
|
|
fn redact_secrets(config: &mut Config) {
|
|
const REDACTED: &str = "<redacted>";
|
|
|
|
if let Some(secret) = config
|
|
.network_identity
|
|
.as_mut()
|
|
.and_then(|identity| identity.network_secret.as_mut())
|
|
&& !secret.is_empty()
|
|
{
|
|
*secret = REDACTED.to_owned();
|
|
}
|
|
if let Some(private_key) = config
|
|
.secure_mode
|
|
.as_mut()
|
|
.and_then(|secure_mode| secure_mode.local_private_key.as_mut())
|
|
&& !private_key.is_empty()
|
|
{
|
|
*private_key = REDACTED.to_owned();
|
|
}
|
|
if let Some(private_key) = config
|
|
.vpn_portal_config
|
|
.as_mut()
|
|
.and_then(|portal| portal.wireguard_private_key.as_mut())
|
|
&& !private_key.is_empty()
|
|
{
|
|
*private_key = REDACTED.to_owned();
|
|
}
|
|
if let Some(declarations) = config
|
|
.acl
|
|
.as_mut()
|
|
.and_then(|acl| acl.acl_v1.as_mut())
|
|
.and_then(|acl| acl.group.as_mut())
|
|
.map(|group| &mut group.declares)
|
|
{
|
|
for declaration in declarations {
|
|
if !declaration.group_secret.is_empty() {
|
|
declaration.group_secret = REDACTED.to_owned();
|
|
}
|
|
}
|
|
}
|
|
for credential in &mut config.managed_credentials {
|
|
if !credential.credential_secret.is_empty() {
|
|
credential.credential_secret = REDACTED.to_owned();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn new_from_str(config_str: &str) -> Result<Self, anyhow::Error> {
|
|
Self::new_from_str_with_source("inline config", config_str)
|
|
}
|
|
|
|
pub fn new_from_str_with_source(
|
|
source_name: &str,
|
|
config_str: &str,
|
|
) -> Result<Self, anyhow::Error> {
|
|
let mut config = toml::de::from_str::<Config>(config_str).map_err(|err| {
|
|
let message = format_toml_parse_error(source_name, config_str, &err);
|
|
anyhow::Error::new(err).context(message)
|
|
})?;
|
|
|
|
Self::normalize_config_source(&mut config);
|
|
|
|
Self::new_from_config(config).map_err(|err| {
|
|
let message = format!("failed to load config from {source_name}: {err}");
|
|
err.context(message)
|
|
})
|
|
}
|
|
|
|
fn new_from_config(mut config: Config) -> Result<Self, anyhow::Error> {
|
|
config.flags_struct = Some(
|
|
Self::gen_flags(config.flags.clone().unwrap_or_default())
|
|
.context("failed to parse flags")?,
|
|
);
|
|
config.secure_mode = config
|
|
.secure_mode
|
|
.take()
|
|
.map(normalize_secure_mode_config)
|
|
.transpose()
|
|
.context("failed to normalize [secure_mode] config")?;
|
|
let has_network_identity = config.network_identity.is_some();
|
|
|
|
let config = TomlConfig {
|
|
config: Arc::new(Mutex::new(config)),
|
|
};
|
|
|
|
let old_ns = config.get_network_identity();
|
|
|
|
// Detect credential mode: secure_mode enabled + no network_secret in TOML
|
|
let is_credential = has_network_identity
|
|
&& config
|
|
.get_secure_mode()
|
|
.map(|sm| sm.enabled)
|
|
.unwrap_or(false)
|
|
&& old_ns
|
|
.network_secret
|
|
.as_deref()
|
|
.is_none_or(|s| s.is_empty());
|
|
|
|
if is_credential {
|
|
config.set_network_identity(NetworkIdentity::new_credential(old_ns.network_name));
|
|
} else {
|
|
config.set_network_identity(NetworkIdentity::new(
|
|
old_ns.network_name,
|
|
old_ns.network_secret.unwrap_or_default(),
|
|
));
|
|
}
|
|
|
|
Ok(config)
|
|
}
|
|
|
|
fn gen_flags(
|
|
flags_hashmap: serde_json::Map<String, serde_json::Value>,
|
|
) -> serde_json::Result<Flags> {
|
|
let mut merged_hashmap = match serde_json::to_value(gen_default_flags()) {
|
|
Ok(serde_json::Value::Object(map)) => map,
|
|
_ => serde_json::Map::new(),
|
|
};
|
|
merged_hashmap.extend(flags_hashmap);
|
|
serde_json::from_value(serde_json::Value::Object(merged_hashmap))
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "web-client")]
|
|
mod snapshot;
|
|
|
|
impl ConfigLoader for TomlConfig {
|
|
fn get_inst_name(&self) -> String {
|
|
self.config
|
|
.lock()
|
|
.unwrap()
|
|
.instance_name
|
|
.clone()
|
|
.unwrap_or_else(default_instance_name)
|
|
}
|
|
|
|
fn set_inst_name(&self, name: String) {
|
|
self.config.lock().unwrap().instance_name = Some(name);
|
|
}
|
|
|
|
fn get_hostname(&self) -> String {
|
|
let hostname = self.config.lock().unwrap().hostname.clone();
|
|
|
|
match hostname {
|
|
Some(hostname) => {
|
|
let hostname = hostname
|
|
.chars()
|
|
.filter(|c| !c.is_control())
|
|
.take(32)
|
|
.collect::<String>();
|
|
|
|
if !hostname.is_empty() {
|
|
self.set_hostname(Some(hostname.clone()));
|
|
hostname
|
|
} else {
|
|
self.set_hostname(None);
|
|
String::new()
|
|
}
|
|
}
|
|
None => String::new(),
|
|
}
|
|
}
|
|
|
|
fn set_hostname(&self, name: Option<String>) {
|
|
self.config.lock().unwrap().hostname = name;
|
|
}
|
|
|
|
fn get_netns(&self) -> Option<String> {
|
|
self.config.lock().unwrap().netns.clone()
|
|
}
|
|
|
|
fn set_netns(&self, ns: Option<String>) {
|
|
self.config.lock().unwrap().netns = ns;
|
|
}
|
|
|
|
fn get_ipv4(&self) -> Option<cidr::Ipv4Inet> {
|
|
let locked_config = self.config.lock().unwrap();
|
|
locked_config
|
|
.ipv4
|
|
.as_ref()
|
|
.and_then(|s| s.parse().ok())
|
|
.map(|c: cidr::Ipv4Inet| {
|
|
if c.network_length() == 32 {
|
|
cidr::Ipv4Inet::new(c.address(), 24).unwrap()
|
|
} else {
|
|
c
|
|
}
|
|
})
|
|
}
|
|
|
|
fn set_ipv4(&self, addr: Option<cidr::Ipv4Inet>) {
|
|
self.config.lock().unwrap().ipv4 = addr.map(|addr| addr.to_string());
|
|
}
|
|
|
|
fn get_ipv6(&self) -> Option<cidr::Ipv6Inet> {
|
|
let locked_config = self.config.lock().unwrap();
|
|
locked_config.ipv6.as_ref().and_then(|s| s.parse().ok())
|
|
}
|
|
|
|
fn set_ipv6(&self, addr: Option<cidr::Ipv6Inet>) {
|
|
self.config.lock().unwrap().ipv6 = addr.map(|addr| addr.to_string());
|
|
}
|
|
|
|
fn get_ipv6_public_addr_provider(&self) -> bool {
|
|
self.config
|
|
.lock()
|
|
.unwrap()
|
|
.ipv6_public_addr_provider
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn set_ipv6_public_addr_provider(&self, enabled: bool) {
|
|
self.config.lock().unwrap().ipv6_public_addr_provider = Some(enabled);
|
|
}
|
|
|
|
fn get_ipv6_public_addr_auto(&self) -> bool {
|
|
self.config
|
|
.lock()
|
|
.unwrap()
|
|
.ipv6_public_addr_auto
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn set_ipv6_public_addr_auto(&self, enabled: bool) {
|
|
self.config.lock().unwrap().ipv6_public_addr_auto = Some(enabled);
|
|
}
|
|
|
|
fn get_ipv6_public_addr_prefix(&self) -> Option<cidr::Ipv6Cidr> {
|
|
let locked_config = self.config.lock().unwrap();
|
|
locked_config
|
|
.ipv6_public_addr_prefix
|
|
.as_ref()
|
|
.and_then(|s| s.parse().ok())
|
|
}
|
|
|
|
fn set_ipv6_public_addr_prefix(&self, prefix: Option<cidr::Ipv6Cidr>) {
|
|
self.config.lock().unwrap().ipv6_public_addr_prefix =
|
|
prefix.map(|prefix| prefix.to_string());
|
|
}
|
|
|
|
fn get_dhcp(&self) -> bool {
|
|
self.config.lock().unwrap().dhcp.unwrap_or_default()
|
|
}
|
|
|
|
fn set_dhcp(&self, dhcp: bool) {
|
|
self.config.lock().unwrap().dhcp = Some(dhcp);
|
|
}
|
|
|
|
fn add_proxy_cidr(
|
|
&self,
|
|
cidr: cidr::Ipv4Cidr,
|
|
mapped_cidr: Option<cidr::Ipv4Cidr>,
|
|
) -> Result<(), anyhow::Error> {
|
|
let mut locked_config = self.config.lock().unwrap();
|
|
if locked_config.proxy_network.is_none() {
|
|
locked_config.proxy_network = Some(vec![]);
|
|
}
|
|
if let Some(mapped_cidr) = mapped_cidr.as_ref()
|
|
&& cidr.network_length() != mapped_cidr.network_length()
|
|
{
|
|
return Err(anyhow::anyhow!(
|
|
"Mapped CIDR must have the same network length as the original CIDR: {} != {}",
|
|
cidr.network_length(),
|
|
mapped_cidr.network_length()
|
|
));
|
|
}
|
|
// insert if no duplicate
|
|
if !locked_config
|
|
.proxy_network
|
|
.as_ref()
|
|
.unwrap()
|
|
.iter()
|
|
.any(|c| c.cidr == cidr && c.mapped_cidr == mapped_cidr)
|
|
{
|
|
locked_config
|
|
.proxy_network
|
|
.as_mut()
|
|
.unwrap()
|
|
.push(ProxyNetworkConfig {
|
|
cidr,
|
|
mapped_cidr,
|
|
allow: None,
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn remove_proxy_cidr(&self, cidr: cidr::Ipv4Cidr) {
|
|
let mut locked_config = self.config.lock().unwrap();
|
|
if let Some(proxy_cidrs) = &mut locked_config.proxy_network {
|
|
proxy_cidrs.retain(|c| c.cidr != cidr);
|
|
}
|
|
}
|
|
|
|
fn clear_proxy_cidrs(&self) {
|
|
let mut locked_config = self.config.lock().unwrap();
|
|
locked_config.proxy_network = None;
|
|
}
|
|
|
|
fn get_proxy_cidrs(&self) -> Vec<ProxyNetworkConfig> {
|
|
self.config
|
|
.lock()
|
|
.unwrap()
|
|
.proxy_network
|
|
.as_ref()
|
|
.cloned()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn get_id(&self) -> uuid::Uuid {
|
|
let mut locked_config = self.config.lock().unwrap();
|
|
match locked_config.instance_id {
|
|
Some(id) => id,
|
|
None => {
|
|
let id = uuid::Uuid::new_v4();
|
|
locked_config.instance_id = Some(id);
|
|
id
|
|
}
|
|
}
|
|
}
|
|
|
|
fn set_id(&self, id: uuid::Uuid) {
|
|
self.config.lock().unwrap().instance_id = Some(id);
|
|
}
|
|
|
|
fn get_network_identity(&self) -> NetworkIdentity {
|
|
self.config
|
|
.lock()
|
|
.unwrap()
|
|
.network_identity
|
|
.clone()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn set_network_identity(&self, identity: NetworkIdentity) {
|
|
self.config.lock().unwrap().network_identity = Some(identity);
|
|
}
|
|
|
|
fn get_listener_uris(&self) -> Vec<url::Url> {
|
|
self.config
|
|
.lock()
|
|
.unwrap()
|
|
.listeners
|
|
.clone()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn get_peers(&self) -> Vec<PeerConfig> {
|
|
self.config.lock().unwrap().peer.clone().unwrap_or_default()
|
|
}
|
|
|
|
fn set_peers(&self, peers: Vec<PeerConfig>) {
|
|
self.config.lock().unwrap().peer = Some(peers);
|
|
}
|
|
|
|
fn get_listeners(&self) -> Option<Vec<url::Url>> {
|
|
self.config.lock().unwrap().listeners.clone()
|
|
}
|
|
|
|
fn set_listeners(&self, listeners: Vec<url::Url>) {
|
|
self.config.lock().unwrap().listeners = Some(listeners);
|
|
}
|
|
|
|
fn get_mapped_listeners(&self) -> Vec<url::Url> {
|
|
self.config
|
|
.lock()
|
|
.unwrap()
|
|
.mapped_listeners
|
|
.clone()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn set_mapped_listeners(&self, listeners: Option<Vec<url::Url>>) {
|
|
self.config.lock().unwrap().mapped_listeners = listeners;
|
|
}
|
|
|
|
fn get_vpn_portal_config(&self) -> Option<VpnPortalConfig> {
|
|
self.config.lock().unwrap().vpn_portal_config.clone()
|
|
}
|
|
fn set_vpn_portal_config(&self, config: VpnPortalConfig) {
|
|
self.config.lock().unwrap().vpn_portal_config = Some(config);
|
|
}
|
|
|
|
fn get_flags(&self) -> Flags {
|
|
self.config
|
|
.lock()
|
|
.unwrap()
|
|
.flags_struct
|
|
.clone()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn set_flags(&self, flags: Flags) {
|
|
self.config.lock().unwrap().flags_struct = Some(flags);
|
|
}
|
|
|
|
fn get_exit_nodes(&self) -> Vec<IpAddr> {
|
|
self.config
|
|
.lock()
|
|
.unwrap()
|
|
.exit_nodes
|
|
.clone()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn set_exit_nodes(&self, nodes: Vec<IpAddr>) {
|
|
self.config.lock().unwrap().exit_nodes = Some(nodes);
|
|
}
|
|
|
|
fn get_routes(&self) -> Option<Vec<cidr::Ipv4Cidr>> {
|
|
self.config.lock().unwrap().routes.clone()
|
|
}
|
|
|
|
fn set_routes(&self, routes: Option<Vec<cidr::Ipv4Cidr>>) {
|
|
self.config.lock().unwrap().routes = routes;
|
|
}
|
|
|
|
fn get_socks5_portal(&self) -> Option<url::Url> {
|
|
self.config.lock().unwrap().socks5_proxy.clone()
|
|
}
|
|
|
|
fn set_socks5_portal(&self, addr: Option<url::Url>) {
|
|
self.config.lock().unwrap().socks5_proxy = addr;
|
|
}
|
|
|
|
fn get_port_forwards(&self) -> Vec<PortForwardConfig> {
|
|
self.config
|
|
.lock()
|
|
.unwrap()
|
|
.port_forward
|
|
.clone()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn set_port_forwards(&self, forwards: Vec<PortForwardConfig>) {
|
|
self.config.lock().unwrap().port_forward = Some(forwards);
|
|
}
|
|
|
|
fn get_acl(&self) -> Option<Acl> {
|
|
self.config.lock().unwrap().acl.clone()
|
|
}
|
|
|
|
fn set_acl(&self, acl: Option<Acl>) {
|
|
self.config.lock().unwrap().acl = acl;
|
|
}
|
|
|
|
fn get_tcp_whitelist(&self) -> Vec<String> {
|
|
self.config
|
|
.lock()
|
|
.unwrap()
|
|
.tcp_whitelist
|
|
.clone()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn set_tcp_whitelist(&self, whitelist: Vec<String>) {
|
|
self.config.lock().unwrap().tcp_whitelist = Some(whitelist);
|
|
}
|
|
|
|
fn get_udp_whitelist(&self) -> Vec<String> {
|
|
self.config
|
|
.lock()
|
|
.unwrap()
|
|
.udp_whitelist
|
|
.clone()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn set_udp_whitelist(&self, whitelist: Vec<String>) {
|
|
self.config.lock().unwrap().udp_whitelist = Some(whitelist);
|
|
}
|
|
|
|
fn get_stun_servers(&self) -> Option<Vec<String>> {
|
|
self.config.lock().unwrap().stun_servers.clone()
|
|
}
|
|
|
|
fn set_stun_servers(&self, servers: Option<Vec<String>>) {
|
|
self.config.lock().unwrap().stun_servers = servers;
|
|
}
|
|
|
|
fn get_tcp_stun_servers(&self) -> Option<Vec<String>> {
|
|
self.config.lock().unwrap().tcp_stun_servers.clone()
|
|
}
|
|
|
|
fn set_tcp_stun_servers(&self, servers: Option<Vec<String>>) {
|
|
self.config.lock().unwrap().tcp_stun_servers = servers;
|
|
}
|
|
|
|
fn get_stun_servers_v6(&self) -> Option<Vec<String>> {
|
|
self.config.lock().unwrap().stun_servers_v6.clone()
|
|
}
|
|
|
|
fn set_stun_servers_v6(&self, servers: Option<Vec<String>>) {
|
|
self.config.lock().unwrap().stun_servers_v6 = servers;
|
|
}
|
|
|
|
fn get_secure_mode(&self) -> Option<SecureModeConfig> {
|
|
self.config.lock().unwrap().secure_mode.clone()
|
|
}
|
|
|
|
fn set_secure_mode(&self, secure_mode: Option<SecureModeConfig>) {
|
|
self.config.lock().unwrap().secure_mode = secure_mode;
|
|
}
|
|
|
|
fn get_credential_file(&self) -> Option<PathBuf> {
|
|
self.config.lock().unwrap().credential_file.clone()
|
|
}
|
|
|
|
fn set_credential_file(&self, path: Option<PathBuf>) {
|
|
self.config.lock().unwrap().credential_file = path;
|
|
}
|
|
|
|
fn get_managed_credentials(&self) -> Vec<ManagedCredentialConfig> {
|
|
self.config.lock().unwrap().managed_credentials.clone()
|
|
}
|
|
|
|
fn set_managed_credentials(&self, credentials: Vec<ManagedCredentialConfig>) {
|
|
self.config.lock().unwrap().managed_credentials = credentials;
|
|
}
|
|
|
|
fn get_network_config_source(&self) -> ConfigSource {
|
|
self.config
|
|
.lock()
|
|
.unwrap()
|
|
.source
|
|
.as_ref()
|
|
.map(|source| source.source)
|
|
.unwrap_or(ConfigSource::User)
|
|
}
|
|
|
|
fn set_network_config_source(&self, source: Option<ConfigSource>) {
|
|
self.config.lock().unwrap().source = source.and_then(|source| match source {
|
|
ConfigSource::User => None,
|
|
other => Some(ConfigSourceConfig { source: other }),
|
|
});
|
|
}
|
|
|
|
fn dump(&self) -> String {
|
|
#[cfg(feature = "config-write")]
|
|
{
|
|
toml::to_string_pretty(&self.config_for_dump()).unwrap()
|
|
}
|
|
#[cfg(not(feature = "config-write"))]
|
|
{
|
|
panic!("this build does not include TOML configuration serialization")
|
|
}
|
|
}
|
|
|
|
fn dump_redacted(&self) -> String {
|
|
#[cfg(feature = "config-write")]
|
|
{
|
|
let mut config = self.config_for_dump();
|
|
Self::redact_secrets(&mut config);
|
|
toml::to_string_pretty(&config).unwrap()
|
|
}
|
|
#[cfg(not(feature = "config-write"))]
|
|
{
|
|
panic!("this build does not include TOML configuration serialization")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Transitional name retained while native consumers migrate to [`TomlConfig`].
|
|
pub type TomlConfigLoader = TomlConfig;
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parse_error_preserves_source_and_location() {
|
|
let error =
|
|
TomlConfig::new_from_str_with_source("fixture.toml", "dhcp = \"yes\"").unwrap_err();
|
|
let display = error.to_string();
|
|
|
|
assert!(display.contains("fixture.toml"));
|
|
assert!(display.contains("dhcp = \"yes\""));
|
|
assert!(display.contains("invalid type: string"));
|
|
assert!(
|
|
error
|
|
.chain()
|
|
.any(|cause| cause.downcast_ref::<toml::de::Error>().is_some())
|
|
);
|
|
}
|
|
|
|
#[cfg(feature = "config-write")]
|
|
#[test]
|
|
fn toml_round_trip_preserves_config_and_non_default_flags() {
|
|
let config = TomlConfig::new_from_str(
|
|
r#"
|
|
instance_name = "node-a"
|
|
instance_id = "018f85a8-a9d0-7d4c-b73d-4ab62c048a20"
|
|
hostname = "host-a"
|
|
listeners = ["tcp://0.0.0.0:11010"]
|
|
|
|
[network_identity]
|
|
network_name = "network-a"
|
|
network_secret = "secret-a"
|
|
|
|
[flags]
|
|
mtu = 1420
|
|
socket_mark = 0
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let dumped = config.dump();
|
|
let restored = TomlConfig::new_from_str(&dumped).unwrap();
|
|
|
|
assert_eq!(restored.get_id(), config.get_id());
|
|
assert_eq!(restored.get_hostname(), "host-a");
|
|
assert_eq!(
|
|
restored.get_network_identity(),
|
|
config.get_network_identity()
|
|
);
|
|
assert_eq!(restored.get_listener_uris(), config.get_listener_uris());
|
|
assert_eq!(restored.get_flags().mtu, 1420);
|
|
assert_eq!(restored.get_flags().socket_mark, Some(0));
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_vpn_portal_client_cidr_is_rejected_explicitly() {
|
|
let error = TomlConfig::new_from_str(
|
|
r#"
|
|
[vpn_portal_config]
|
|
client_cidr = "10.14.14.0/24"
|
|
wireguard_listen = "0.0.0.0:51820"
|
|
"#,
|
|
)
|
|
.unwrap_err()
|
|
.to_string();
|
|
|
|
assert!(error.contains("client_cidr"), "{error}");
|
|
}
|
|
|
|
#[cfg(feature = "config-write")]
|
|
#[test]
|
|
fn vpn_portal_round_trip_and_redacted_dump_preserve_dump_semantics() {
|
|
let config = TomlConfig::new_from_str(
|
|
r#"
|
|
[network_identity]
|
|
network_name = "network-a"
|
|
network_secret = "network-secret"
|
|
|
|
[secure_mode]
|
|
enabled = true
|
|
local_private_key = "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="
|
|
|
|
[vpn_portal_config]
|
|
wireguard_listen = "0.0.0.0:51820"
|
|
wireguard_private_key = "wireguard-private-key"
|
|
|
|
[[vpn_portal_config.clients]]
|
|
name = "alice"
|
|
virtual_ip = "10.144.144.10/24"
|
|
groups = ["staff"]
|
|
|
|
[acl.acl_v1.group]
|
|
|
|
[[acl.acl_v1.group.declares]]
|
|
group_name = "staff"
|
|
group_secret = "group-secret"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let dumped = config.dump();
|
|
assert!(dumped.contains("network-secret"));
|
|
assert!(dumped.contains("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="));
|
|
assert!(dumped.contains("wireguard-private-key"));
|
|
assert!(dumped.contains("group-secret"));
|
|
assert_eq!(
|
|
TomlConfig::new_from_str(&dumped)
|
|
.unwrap()
|
|
.get_vpn_portal_config(),
|
|
config.get_vpn_portal_config()
|
|
);
|
|
|
|
let redacted = config.dump_redacted();
|
|
assert!(!redacted.contains("network-secret"));
|
|
assert!(!redacted.contains("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="));
|
|
assert!(!redacted.contains("wireguard-private-key"));
|
|
assert!(!redacted.contains("group-secret"));
|
|
assert_eq!(redacted.matches("<redacted>").count(), 4);
|
|
}
|
|
|
|
#[cfg(feature = "config-write")]
|
|
#[test]
|
|
fn managed_credentials_round_trip_and_redact_secret() {
|
|
let config = TomlConfig::new_from_str(
|
|
r#"
|
|
[[managed_credentials]]
|
|
credential_id = "managed-a"
|
|
credential_secret = "private-key-material"
|
|
groups = ["ops"]
|
|
allow_relay = true
|
|
allowed_proxy_cidrs = ["10.0.0.0/24"]
|
|
expiry_unix = 2000000000
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let dumped = config.dump();
|
|
let restored = TomlConfig::new_from_str(&dumped).unwrap();
|
|
assert_eq!(
|
|
restored.get_managed_credentials(),
|
|
config.get_managed_credentials()
|
|
);
|
|
assert!(dumped.contains("private-key-material"));
|
|
|
|
let redacted = config.dump_redacted();
|
|
assert!(!redacted.contains("private-key-material"));
|
|
assert!(redacted.contains("<redacted>"));
|
|
assert!(!TomlConfig::default().dump().contains("managed_credentials"));
|
|
}
|
|
|
|
#[test]
|
|
fn hostname_normalization_is_portable_and_has_no_host_fallback() {
|
|
let absent = TomlConfig::default();
|
|
assert_eq!(absent.get_hostname(), "");
|
|
|
|
let configured = TomlConfig::new_from_str("hostname = \"node\\u0007-name\"").unwrap();
|
|
assert_eq!(configured.get_hostname(), "node-name");
|
|
}
|
|
|
|
#[test]
|
|
fn credential_mode_does_not_synthesize_a_network_secret() {
|
|
let config = TomlConfig::new_from_str(
|
|
r#"
|
|
[network_identity]
|
|
network_name = "credential-network"
|
|
|
|
[secure_mode]
|
|
enabled = true
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let identity = config.get_network_identity();
|
|
assert_eq!(identity.network_name, "credential-network");
|
|
assert_eq!(identity.network_secret, None);
|
|
}
|
|
|
|
#[cfg(feature = "config-write")]
|
|
#[test]
|
|
fn user_source_is_implicit_while_web_source_round_trips() {
|
|
let user = TomlConfig::new_from_str(
|
|
r#"
|
|
[source]
|
|
source = "user"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(user.get_network_config_source(), ConfigSource::User);
|
|
assert!(!user.dump().contains("[source]"));
|
|
|
|
let web = TomlConfig::new_from_str(
|
|
r#"
|
|
[source]
|
|
source = "web"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(web.get_network_config_source(), ConfigSource::Web);
|
|
assert!(web.dump().contains("source = \"web\""));
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod compatibility_tests {
|
|
use super::*;
|
|
use base64::{Engine as _, prelude::BASE64_STANDARD};
|
|
|
|
#[cfg(feature = "config-write")]
|
|
#[test]
|
|
fn flags_diff_covers_every_protobuf_field() {
|
|
use prost::Message as _;
|
|
|
|
let descriptor_set =
|
|
prost_types::FileDescriptorSet::decode(crate::proto::DESCRIPTOR_POOL_BYTES).unwrap();
|
|
let proto_fields = descriptor_set
|
|
.file
|
|
.iter()
|
|
.find(|file| file.package.as_deref() == Some("common"))
|
|
.and_then(|file| {
|
|
file.message_type
|
|
.iter()
|
|
.find(|message| message.name.as_deref() == Some("FlagsInConfig"))
|
|
})
|
|
.unwrap()
|
|
.field
|
|
.iter()
|
|
.map(|field| field.name.as_deref().unwrap())
|
|
.collect::<std::collections::BTreeSet<_>>();
|
|
let diff_fields = FLAGS_DIFF_FIELDS
|
|
.iter()
|
|
.copied()
|
|
.collect::<std::collections::BTreeSet<_>>();
|
|
|
|
assert_eq!(diff_fields, proto_fields);
|
|
}
|
|
|
|
#[test]
|
|
fn socket_mark_config_file_roundtrip_none_some_and_zero() {
|
|
// Omitting the flag leaves socket_mark unset (None) -> SO_MARK untouched.
|
|
let cfg = TomlConfigLoader::new_from_str(
|
|
r#"
|
|
[network_identity]
|
|
network_name = "n"
|
|
network_secret = "s"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(cfg.get_flags().socket_mark, None);
|
|
|
|
// socket_mark = 0 is a legitimate value distinct from "unset".
|
|
let cfg = TomlConfigLoader::new_from_str(
|
|
r#"
|
|
[network_identity]
|
|
network_name = "n"
|
|
network_secret = "s"
|
|
|
|
[flags]
|
|
socket_mark = 0
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(cfg.get_flags().socket_mark, Some(0));
|
|
|
|
// A non-zero mark round-trips as Some(v).
|
|
let cfg = TomlConfigLoader::new_from_str(
|
|
r#"
|
|
[network_identity]
|
|
network_name = "n"
|
|
network_secret = "s"
|
|
|
|
[flags]
|
|
socket_mark = 66
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(cfg.get_flags().socket_mark, Some(66));
|
|
|
|
// set_flags(None) must serialize back through gen_config without
|
|
// resurrecting a value (guards the gen_flags merge against dropping
|
|
// the key when the serialized default is null).
|
|
cfg.set_flags(Flags {
|
|
socket_mark: None,
|
|
..cfg.get_flags()
|
|
});
|
|
assert_eq!(cfg.get_flags().socket_mark, None);
|
|
}
|
|
|
|
#[cfg(feature = "config-write")]
|
|
#[test]
|
|
fn dump_preserves_flags_that_differ_from_easytier_defaults() {
|
|
let cfg = TomlConfigLoader::default();
|
|
let mut flags = gen_default_flags();
|
|
flags.dev_name = "et_test".to_string();
|
|
flags.enable_quic_proxy = true;
|
|
flags.disable_tcp_hole_punching = true;
|
|
flags.disable_sym_hole_punching = true;
|
|
flags.multi_thread = false;
|
|
flags.bind_device = false;
|
|
flags.enable_ipv6 = false;
|
|
flags.relay_network_whitelist = "".to_string();
|
|
flags.prefer_peer_relay = true;
|
|
flags.mtu = 0;
|
|
flags.foreign_relay_bps_limit = u64::MAX - 1;
|
|
flags.instance_recv_bps_limit = u64::MAX - 2;
|
|
flags.data_compress_algo = CompressionAlgoPb::Zstd.into();
|
|
flags.socket_mark = Some(0);
|
|
cfg.set_flags(flags);
|
|
|
|
let dumped = cfg.dump();
|
|
|
|
assert!(dumped.contains("dev_name = \"et_test\""));
|
|
assert!(dumped.contains("enable_quic_proxy = true"));
|
|
assert!(dumped.contains("disable_tcp_hole_punching = true"));
|
|
assert!(dumped.contains("disable_sym_hole_punching = true"));
|
|
assert!(dumped.contains("multi_thread = false"));
|
|
assert!(dumped.contains("bind_device = false"));
|
|
assert!(dumped.contains("enable_ipv6 = false"));
|
|
assert!(dumped.contains("relay_network_whitelist = \"\""));
|
|
assert!(dumped.contains("mtu = 0"));
|
|
assert!(dumped.contains("foreign_relay_bps_limit = \"18446744073709551614\""));
|
|
assert!(dumped.contains("instance_recv_bps_limit = \"18446744073709551613\""));
|
|
assert!(dumped.contains("data_compress_algo = \"Zstd\""));
|
|
assert!(dumped.contains("socket_mark = 0"));
|
|
|
|
let reloaded = TomlConfigLoader::new_from_str(&dumped).unwrap();
|
|
let reloaded_flags = reloaded.get_flags();
|
|
assert_eq!(reloaded_flags.dev_name, "et_test");
|
|
assert!(reloaded_flags.enable_quic_proxy);
|
|
assert!(reloaded_flags.disable_tcp_hole_punching);
|
|
assert!(reloaded_flags.disable_sym_hole_punching);
|
|
assert!(!reloaded_flags.multi_thread);
|
|
assert!(!reloaded_flags.bind_device);
|
|
assert!(!reloaded_flags.enable_ipv6);
|
|
assert_eq!(reloaded_flags.relay_network_whitelist, "");
|
|
assert!(reloaded_flags.prefer_peer_relay);
|
|
assert_eq!(reloaded_flags.mtu, 0);
|
|
assert_eq!(reloaded_flags.foreign_relay_bps_limit, u64::MAX - 1);
|
|
assert_eq!(reloaded_flags.instance_recv_bps_limit, u64::MAX - 2);
|
|
assert_eq!(
|
|
reloaded_flags.data_compress_algo,
|
|
i32::from(CompressionAlgoPb::Zstd)
|
|
);
|
|
assert_eq!(reloaded_flags.socket_mark, Some(0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_stun_servers_config() {
|
|
let config = TomlConfigLoader::default();
|
|
let stun_servers = config.get_stun_servers();
|
|
assert!(stun_servers.is_none());
|
|
assert!(config.get_tcp_stun_servers().is_none());
|
|
|
|
// Test setting custom stun servers
|
|
let custom_servers = vec!["txt:stun.easytier.cn".to_string()];
|
|
config.set_stun_servers(Some(custom_servers.clone()));
|
|
|
|
let retrieved_servers = config.get_stun_servers();
|
|
assert_eq!(retrieved_servers.unwrap(), custom_servers);
|
|
|
|
let custom_tcp_servers = vec!["tcp-stun.example.com:3478".to_string()];
|
|
config.set_tcp_stun_servers(Some(custom_tcp_servers.clone()));
|
|
|
|
let retrieved_tcp_servers = config.get_tcp_stun_servers();
|
|
assert_eq!(retrieved_tcp_servers.unwrap(), custom_tcp_servers);
|
|
}
|
|
|
|
#[test]
|
|
fn test_stun_servers_toml_parsing() {
|
|
let config_str = r#"
|
|
instance_name = "test"
|
|
stun_servers = [
|
|
"stun.l.google.com:19302",
|
|
"stun1.l.google.com:19302",
|
|
"txt:stun.easytier.cn"
|
|
]
|
|
tcp_stun_servers = [
|
|
"tcp-stun.example.com:3478"
|
|
]"#;
|
|
|
|
let config = TomlConfigLoader::new_from_str(config_str).unwrap();
|
|
let stun_servers = config.get_stun_servers().unwrap();
|
|
let tcp_stun_servers = config.get_tcp_stun_servers().unwrap();
|
|
|
|
assert_eq!(stun_servers.len(), 3);
|
|
assert_eq!(stun_servers[0], "stun.l.google.com:19302");
|
|
assert_eq!(stun_servers[1], "stun1.l.google.com:19302");
|
|
assert_eq!(stun_servers[2], "txt:stun.easytier.cn");
|
|
assert_eq!(tcp_stun_servers, ["tcp-stun.example.com:3478"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_empty_tcp_stun_servers_toml_parsing() {
|
|
let config = TomlConfigLoader::new_from_str(
|
|
r#"
|
|
instance_name = "test"
|
|
tcp_stun_servers = []
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(config.get_tcp_stun_servers(), Some(Vec::new()));
|
|
}
|
|
|
|
#[cfg(feature = "config-write")]
|
|
#[test]
|
|
fn test_network_config_source_toml_roundtrip() {
|
|
let config = TomlConfigLoader::default();
|
|
assert_eq!(config.get_network_config_source(), ConfigSource::User);
|
|
|
|
config.set_network_config_source(Some(ConfigSource::Web));
|
|
let dumped = config.dump();
|
|
|
|
assert!(dumped.contains("[source]"));
|
|
assert!(dumped.contains("source = \"web\""));
|
|
|
|
let loaded = TomlConfigLoader::new_from_str(&dumped).unwrap();
|
|
assert_eq!(loaded.get_network_config_source(), ConfigSource::Web);
|
|
}
|
|
|
|
#[cfg(feature = "config-write")]
|
|
#[test]
|
|
fn test_toml_credential_mode_omits_network_secret() {
|
|
for network_secret in ["", r#"network_secret = """#] {
|
|
let config = TomlConfigLoader::new_from_str(&format!(
|
|
r#"
|
|
[network_identity]
|
|
network_name = "credential-network"
|
|
{network_secret}
|
|
|
|
[secure_mode]
|
|
enabled = true
|
|
"#
|
|
))
|
|
.unwrap();
|
|
|
|
let identity = config.get_network_identity();
|
|
assert_eq!(identity.network_name, "credential-network");
|
|
assert_eq!(identity.network_secret, None);
|
|
assert_eq!(identity.network_secret_digest, None);
|
|
assert!(!config.dump().contains("network_secret"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_toml_secure_mode_without_network_identity_uses_default_secret() {
|
|
let config = TomlConfigLoader::new_from_str(
|
|
r#"
|
|
[secure_mode]
|
|
enabled = true
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let identity = config.get_network_identity();
|
|
assert_eq!(identity.network_name, "default");
|
|
assert_eq!(identity.network_secret.as_deref(), Some(""));
|
|
assert!(identity.network_secret_digest.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_toml_secure_mode_generates_keypair_when_keys_missing() {
|
|
let config = TomlConfigLoader::new_from_str(
|
|
r#"
|
|
[secure_mode]
|
|
enabled = true
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let secure_mode = config.get_secure_mode().unwrap();
|
|
let private_key = secure_mode.private_key().unwrap();
|
|
let public_key = secure_mode.public_key().unwrap();
|
|
assert_eq!(
|
|
x25519_dalek::PublicKey::from(&private_key).as_bytes(),
|
|
public_key.as_bytes()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_toml_secure_mode_derives_public_key_from_private_key() {
|
|
let private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng);
|
|
let config = TomlConfigLoader::new_from_str(&format!(
|
|
r#"
|
|
[secure_mode]
|
|
enabled = true
|
|
local_private_key = "{}"
|
|
"#,
|
|
BASE64_STANDARD.encode(private.as_bytes())
|
|
))
|
|
.unwrap();
|
|
|
|
let secure_mode = config.get_secure_mode().unwrap();
|
|
let private_key = secure_mode.private_key().unwrap();
|
|
assert_eq!(private_key.as_bytes(), private.as_bytes());
|
|
assert_eq!(
|
|
secure_mode.public_key().unwrap().as_bytes(),
|
|
x25519_dalek::PublicKey::from(&private).as_bytes()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_toml_secure_mode_rejects_mismatched_keypair() {
|
|
let private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng);
|
|
let other_public = x25519_dalek::PublicKey::from(
|
|
&x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng),
|
|
);
|
|
let error = TomlConfigLoader::new_from_str(&format!(
|
|
r#"
|
|
[secure_mode]
|
|
enabled = true
|
|
local_private_key = "{}"
|
|
local_public_key = "{}"
|
|
"#,
|
|
BASE64_STANDARD.encode(private.as_bytes()),
|
|
BASE64_STANDARD.encode(other_public.as_bytes())
|
|
))
|
|
.unwrap_err();
|
|
let error = format!("{error:#}");
|
|
|
|
assert!(
|
|
error.contains("failed to normalize [secure_mode] config"),
|
|
"{error}"
|
|
);
|
|
assert!(
|
|
error.contains("does not match generated public key"),
|
|
"{error}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_toml_secure_mode_disabled_keeps_keys_unset() {
|
|
let config = TomlConfigLoader::new_from_str(
|
|
r#"
|
|
[secure_mode]
|
|
enabled = false
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let secure_mode = config.get_secure_mode().unwrap();
|
|
assert!(!secure_mode.enabled);
|
|
assert_eq!(secure_mode.local_private_key, None);
|
|
assert_eq!(secure_mode.local_public_key, None);
|
|
}
|
|
|
|
#[cfg(feature = "config-write")]
|
|
#[test]
|
|
fn test_toml_secure_mode_keypair_survives_roundtrip() {
|
|
let config = TomlConfigLoader::new_from_str(
|
|
r#"
|
|
[secure_mode]
|
|
enabled = true
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let dumped = config.dump();
|
|
let restored = TomlConfigLoader::new_from_str(&dumped).unwrap();
|
|
|
|
assert_eq!(
|
|
config.get_secure_mode().unwrap(),
|
|
restored.get_secure_mode().unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_acl_toml_rule_uses_defaults_for_omitted_fields() {
|
|
use crate::proto::acl::{Action, ChainType, Protocol};
|
|
|
|
let config_str = r#"
|
|
[[acl.acl_v1.chains]]
|
|
name = "subnet_proxy_protect"
|
|
chain_type = 3
|
|
enabled = true
|
|
default_action = 2
|
|
|
|
[[acl.acl_v1.chains.rules]]
|
|
name = "allow_my_devices"
|
|
priority = 1000
|
|
action = 1
|
|
source_ips = ["10.172.192.2/32"]
|
|
protocol = 5
|
|
enabled = true
|
|
"#;
|
|
|
|
let config = TomlConfigLoader::new_from_str(config_str).unwrap();
|
|
let acl = config.get_acl().unwrap();
|
|
let acl_v1 = acl.acl_v1.unwrap();
|
|
let chain = &acl_v1.chains[0];
|
|
let rule = &chain.rules[0];
|
|
|
|
assert_eq!(chain.chain_type, ChainType::Forward as i32);
|
|
assert_eq!(chain.default_action, Action::Drop as i32);
|
|
assert_eq!(rule.action, Action::Allow as i32);
|
|
assert_eq!(rule.protocol, Protocol::Any as i32);
|
|
assert_eq!(rule.source_ips, vec!["10.172.192.2/32"]);
|
|
assert!(rule.ports.is_empty());
|
|
assert!(rule.source_ports.is_empty());
|
|
assert!(rule.destination_ips.is_empty());
|
|
assert!(rule.source_groups.is_empty());
|
|
assert!(rule.destination_groups.is_empty());
|
|
assert_eq!(rule.rate_limit, 0);
|
|
assert_eq!(rule.burst_limit, 0);
|
|
assert!(!rule.stateful);
|
|
}
|
|
|
|
#[test]
|
|
fn test_acl_toml_group_can_omit_declares_or_members() {
|
|
let declares_only = r#"
|
|
[acl.acl_v1.group]
|
|
|
|
[[acl.acl_v1.group.declares]]
|
|
group_name = "admin"
|
|
group_secret = "admin-pw"
|
|
"#;
|
|
let config = TomlConfigLoader::new_from_str(declares_only).unwrap();
|
|
let group = config.get_acl().unwrap().acl_v1.unwrap().group.unwrap();
|
|
assert_eq!(group.declares.len(), 1);
|
|
assert!(group.members.is_empty());
|
|
|
|
let members_only = r#"
|
|
[acl.acl_v1.group]
|
|
members = ["admin"]
|
|
"#;
|
|
let config = TomlConfigLoader::new_from_str(members_only).unwrap();
|
|
let group = config.get_acl().unwrap().acl_v1.unwrap().group.unwrap();
|
|
assert!(group.declares.is_empty());
|
|
assert_eq!(group.members, vec!["admin"]);
|
|
}
|
|
|
|
#[cfg(feature = "config-write")]
|
|
#[test]
|
|
fn test_network_config_source_user_is_implicit() {
|
|
let config = TomlConfigLoader::default();
|
|
config.set_network_config_source(Some(ConfigSource::User));
|
|
let dumped = config.dump();
|
|
|
|
assert!(!dumped.contains("[source]"));
|
|
|
|
let loaded = TomlConfigLoader::new_from_str(&dumped).unwrap();
|
|
assert_eq!(loaded.get_network_config_source(), ConfigSource::User);
|
|
|
|
let explicit_user = TomlConfigLoader::new_from_str(
|
|
r#"
|
|
[source]
|
|
source = "user"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
explicit_user.get_network_config_source(),
|
|
ConfigSource::User
|
|
);
|
|
assert!(!explicit_user.dump().contains("[source]"));
|
|
}
|
|
|
|
#[cfg(feature = "config-write")]
|
|
#[test]
|
|
fn test_ipv6_public_addr_config_roundtrip() {
|
|
let config = TomlConfigLoader::default();
|
|
let prefix: cidr::Ipv6Cidr = "2001:db8:100::/64".parse().unwrap();
|
|
|
|
config.set_ipv6_public_addr_provider(true);
|
|
config.set_ipv6_public_addr_auto(true);
|
|
config.set_ipv6_public_addr_prefix(Some(prefix));
|
|
|
|
assert!(config.get_ipv6_public_addr_provider());
|
|
assert!(config.get_ipv6_public_addr_auto());
|
|
assert_eq!(config.get_ipv6_public_addr_prefix(), Some(prefix));
|
|
|
|
let dumped = config.dump();
|
|
let loaded = TomlConfigLoader::new_from_str(&dumped).unwrap();
|
|
assert!(loaded.get_ipv6_public_addr_provider());
|
|
assert!(loaded.get_ipv6_public_addr_auto());
|
|
assert_eq!(loaded.get_ipv6_public_addr_prefix(), Some(prefix));
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod full_example_tests {
|
|
use super::*;
|
|
|
|
#[cfg(feature = "config-write")]
|
|
#[test]
|
|
fn full_example_test() {
|
|
let config_str = r#"
|
|
instance_name = "default"
|
|
instance_id = "87ede5a2-9c3d-492d-9bbe-989b9d07e742"
|
|
ipv4 = "10.144.144.10"
|
|
listeners = [ "tcp://0.0.0.0:11010", "udp://0.0.0.0:11010" ]
|
|
routes = [ "192.168.0.0/16" ]
|
|
|
|
[network_identity]
|
|
network_name = "default"
|
|
network_secret = ""
|
|
|
|
[[peer]]
|
|
uri = "tcp://public.kkrainbow.top:11010"
|
|
|
|
[[peer]]
|
|
uri = "udp://192.168.94.33:11010"
|
|
|
|
[[proxy_network]]
|
|
cidr = "10.147.223.0/24"
|
|
allow = ["tcp", "udp", "icmp"]
|
|
|
|
[[proxy_network]]
|
|
cidr = "10.1.1.0/24"
|
|
allow = ["tcp", "icmp"]
|
|
|
|
[file_logger]
|
|
level = "info"
|
|
file = "easytier"
|
|
dir = "/tmp/easytier"
|
|
|
|
[console_logger]
|
|
level = "warn"
|
|
|
|
[[port_forward]]
|
|
bind_addr = "0.0.0.0:11011"
|
|
dst_addr = "192.168.94.33:11011"
|
|
proto = "tcp"
|
|
"#;
|
|
let ret = TomlConfigLoader::new_from_str(config_str);
|
|
if let Err(e) = &ret {
|
|
println!("{}", e);
|
|
} else {
|
|
println!("{:?}", ret.as_ref().unwrap());
|
|
}
|
|
assert!(ret.is_ok());
|
|
|
|
let ret = ret.unwrap();
|
|
assert_eq!("10.144.144.10/24", ret.get_ipv4().unwrap().to_string());
|
|
|
|
assert_eq!(
|
|
vec!["tcp://0.0.0.0:11010", "udp://0.0.0.0:11010"],
|
|
ret.get_listener_uris()
|
|
.iter()
|
|
.map(|u| u.to_string())
|
|
.collect::<Vec<String>>()
|
|
);
|
|
|
|
assert_eq!(
|
|
vec![PortForwardConfig {
|
|
bind_addr: "0.0.0.0:11011".parse().unwrap(),
|
|
dst_addr: "192.168.94.33:11011".parse().unwrap(),
|
|
proto: "tcp".to_string(),
|
|
}],
|
|
ret.get_port_forwards()
|
|
);
|
|
println!("{}", ret.dump());
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod diagnostic_compatibility_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn stdin_source_name_and_caret_are_preserved() {
|
|
let error = TomlConfig::new_from_str_with_source("stdin", "dhcp = \"yes\"")
|
|
.unwrap_err()
|
|
.to_string();
|
|
|
|
assert!(error.contains("stdin"));
|
|
assert!(error.contains("dhcp = \"yes\""));
|
|
assert!(error.contains('^'));
|
|
assert!(!error.contains("<unknown>"));
|
|
}
|
|
|
|
#[test]
|
|
fn non_ascii_before_typed_error_keeps_byte_location() {
|
|
let error = TomlConfig::new_from_str("hostname = \"节点\"\ndhcp = \"yes\"")
|
|
.unwrap_err()
|
|
.to_string();
|
|
|
|
assert!(error.contains("dhcp = \"yes\""));
|
|
assert!(error.contains('^'));
|
|
assert!(error.contains("invalid type: string"));
|
|
}
|
|
|
|
#[cfg(feature = "rich-config-errors")]
|
|
#[test]
|
|
fn non_ascii_on_syntax_error_line_keeps_source_location() {
|
|
let error = TomlConfig::new_from_str("hostname = \"节点\" dhcp = \"yes\"")
|
|
.unwrap_err()
|
|
.to_string();
|
|
|
|
assert!(error.contains("inline config:1:"));
|
|
assert!(error.contains("hostname = \"节点\" dhcp = \"yes\""));
|
|
assert!(error.contains("expected newline"));
|
|
assert!(!error.contains("<unknown>"));
|
|
}
|
|
|
|
#[test]
|
|
fn flags_conversion_error_keeps_source_and_cause_chain() {
|
|
let error = TomlConfig::new_from_str_with_source(
|
|
"flags-fixture.toml",
|
|
"[flags]\nsocket_mark = \"bad\"",
|
|
)
|
|
.unwrap_err();
|
|
let display = error.to_string();
|
|
|
|
assert!(display.contains("flags-fixture.toml"));
|
|
assert!(display.contains("failed to load config"));
|
|
assert!(display.contains("failed to parse flags"));
|
|
assert!(
|
|
error
|
|
.chain()
|
|
.any(|cause| cause.to_string().contains("failed to parse flags"))
|
|
);
|
|
}
|
|
}
|