use std::{
fs,
io::Read,
net::{IpAddr, SocketAddr},
path::PathBuf,
};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::transport::{quic, tls};
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
pub struct ForwardConfig {
pub address: SocketAddr,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[serde(tag = "transport_type", content = "args")]
#[serde(rename_all = "snake_case")]
pub enum TransportServerConfig {
QuicPlain(quic::ServerConfig),
TlsPlain(tls::ServerConfig),
}
impl From<quic::ServerConfig> for TransportServerConfig {
fn from(value: quic::ServerConfig) -> Self {
TransportServerConfig::QuicPlain(value)
}
}
impl From<tls::ServerConfig> for TransportServerConfig {
fn from(value: tls::ServerConfig) -> Self {
TransportServerConfig::TlsPlain(value)
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
pub struct PersistedServerConfig {
pub forward: ForwardConfig,
pub client_params_path: Option<std::path::PathBuf>,
pub public_ips: Vec<String>,
pub transports: Vec<TransportServerConfig>,
}
#[allow(unused)]
impl PersistedServerConfig {
pub fn parse(config_str: impl AsRef<str>) -> Result<Self> {
toml::from_str(config_str.as_ref()).context("failed to parse config")
}
pub fn parse_file(config_path: &PathBuf) -> Result<Self> {
let mut config_file = fs::File::open(config_path)?;
let mut config = vec![];
config_file.read_to_end(&mut config)?;
toml::from_slice(&config).context("failed to parse config")
}
}
pub use crate::types::{ClientConfig, PersistedClientConfig};
pub fn parse_persisted_config_json(config_str: impl AsRef<str>) -> Result<PersistedClientConfig> {
serde_json::from_str(config_str.as_ref()).context("failed to parse config")
}
pub fn parse_persisted_config(config_str: impl AsRef<str>) -> Result<PersistedClientConfig> {
toml::from_str(config_str.as_ref()).context("failed to parse config")
}
#[allow(unused)]
fn parse_json_file(config_path: PathBuf) -> Result<PersistedClientConfig> {
let mut config_file = fs::File::open(config_path)?;
let mut config_str = String::new();
config_file.read_to_string(&mut config_str)?;
parse_persisted_config_json(config_str)
}
#[allow(unused)]
fn parse_file(config_path: PathBuf) -> Result<PersistedClientConfig> {
let mut config_file = fs::File::open(config_path)?;
let mut config_str = String::new();
config_file.read_to_string(&mut config_str)?;
parse_persisted_config(config_str)
}
impl TryFrom<&PersistedServerConfig> for PersistedClientConfig {
type Error = anyhow::Error;
fn try_from(value: &PersistedServerConfig) -> Result<Self> {
let ips: Vec<IpAddr> = value
.public_ips
.iter()
.map(|ip_str| {
ip_str
.parse()
.expect("failed to parse public IP '{ip_str}' as IpAddr")
})
.collect();
let mut transports = vec![];
for transport in &value.transports {
match transport {
TransportServerConfig::QuicPlain(cfg) => {
let port = cfg.listen.port();
let addresses = ips.iter().map(|ip| SocketAddr::new(*ip, port)).collect();
let id_pubkey = cfg.get_id_pubkey()?.to_string();
transports.push(ClientConfig::QuicPlain(quic::ClientOptions {
addresses,
host: Some("netdna.bootstrapcdn.com".to_string()),
id_pubkey,
}));
}
TransportServerConfig::TlsPlain(cfg) => {
let port = cfg.listen.port();
let addresses = ips.iter().map(|ip| SocketAddr::new(*ip, port)).collect();
let id_pubkey = cfg.get_id_pubkey()?.to_string();
transports.push(ClientConfig::TlsPlain(tls::ClientOptions {
addresses,
host: Some("netdna.bootstrapcdn.com".to_string()),
id_pubkey,
}));
}
}
}
Ok(Self {
version: "0".into(),
transports,
})
}
}
#[cfg(test)]
mod test {
use super::*;
use anyhow::Result;
#[test]
fn serialize_and_deserialize_server_config() -> Result<()> {
let quic_cfg1 = quic::ServerConfig {
listen: "[::1]:4433".parse().unwrap(),
stateless_retry: Default::default(),
block: Default::default(),
connection_limit: Default::default(),
identity_key: Some("fditK5JfNM/88mLWd3ccbLasSrHA5dw1wj+/+1bfGWk=".into()),
private_ed25519_identity_key_file: None,
};
let tls_cfg1 = tls::ServerConfig {
listen: "[::1]:4443".parse().unwrap(),
connection_limit: Default::default(),
identity_key: Some("fditK5JfNM/88mLWd3ccbLasSrHA5dw1wj+/+1bfGWk=".into()),
private_ed25519_identity_key_file: None,
};
let cfg = PersistedServerConfig {
forward: ForwardConfig {
address: "[::1]:50001".parse().unwrap(),
},
client_params_path: None,
public_ips: vec!["192.168.0.1".into(), "fe80::1".into()],
transports: vec![quic_cfg1.into(), tls_cfg1.into()],
};
let example_server_config = r#"
public_ips = ["192.168.0.1", "fe80::1"]
[forward]
address = "[::1]:50001"
[[transports]]
transport_type = "quic_plain"
[transports.args]
stateless_retry = false
listen = "[::1]:4433"
identity_key = "fditK5JfNM/88mLWd3ccbLasSrHA5dw1wj+/+1bfGWk="
[[transports]]
transport_type = "tls_plain"
[transports.args]
listen = "[::1]:4443"
identity_key = "fditK5JfNM/88mLWd3ccbLasSrHA5dw1wj+/+1bfGWk="
"#;
let parsed_cfg = PersistedServerConfig::parse(example_server_config)?;
assert_eq!(cfg, parsed_cfg);
Ok(())
}
#[test]
fn serialize_and_deserialize_client_config() -> Result<()> {
let quic_cfg1 = quic::ClientOptions {
addresses: vec!["192.168.100.3:443".parse().unwrap()],
host: None,
id_pubkey: "gyKl6DN9hgdPGhEzdf9gY4Ha2GzrOwSzLCguxeTVTJU=".into(),
};
let tls_cfg1 = tls::ClientOptions {
addresses: vec!["123.45.67.89:443".parse().unwrap()],
host: Some("alt.domain.com".into()),
id_pubkey: "gyKl6DN9hgdPGhEzdf9gY4Ha2GzrOwSzLCguxeTVTJU=".into(),
};
let cfg = PersistedClientConfig {
version: "v0.0.0".into(),
transports: vec![quic_cfg1.into(), tls_cfg1.into()],
};
let serialized_cfg = toml::to_string(&cfg)?;
let parsed_cfg = parse_persisted_config(serialized_cfg)?;
assert_eq!(cfg, parsed_cfg);
Ok(())
}
#[test]
fn conversion() -> Result<()> {
let quic_cfg1 = quic::ServerConfig {
listen: "[::1]:4433".parse().unwrap(),
stateless_retry: Default::default(),
block: Default::default(),
connection_limit: Default::default(),
identity_key: Some("fditK5JfNM/88mLWd3ccbLasSrHA5dw1wj+/+1bfGWk=".into()),
private_ed25519_identity_key_file: None,
};
let tls_cfg1 = tls::ServerConfig {
listen: "[::1]:4443".parse().unwrap(),
connection_limit: Default::default(),
identity_key: Some("fditK5JfNM/88mLWd3ccbLasSrHA5dw1wj+/+1bfGWk=".into()),
private_ed25519_identity_key_file: None,
};
let cfg = PersistedServerConfig {
forward: ForwardConfig {
address: "[::1]:50001".parse().unwrap(),
},
client_params_path: None,
public_ips: vec!["192.168.0.1".into(), "fe80::1".into()],
transports: vec![quic_cfg1.into(), tls_cfg1.into()],
};
let client_config = PersistedClientConfig::try_from(&cfg)?;
let expected_quic = quic::ClientOptions {
addresses: vec![
"192.168.0.1:4433".parse().unwrap(),
"[fe80::1]:4433".parse().unwrap(),
],
host: Some("netdna.bootstrapcdn.com".to_string()),
id_pubkey: "gyKl6DN9hgdPGhEzdf9gY4Ha2GzrOwSzLCguxeTVTJU=".into(),
};
let expected_tls = tls::ClientOptions {
addresses: vec![
"192.168.0.1:4443".parse().unwrap(),
"[fe80::1]:4443".parse().unwrap(),
],
host: Some("netdna.bootstrapcdn.com".to_string()),
id_pubkey: "gyKl6DN9hgdPGhEzdf9gY4Ha2GzrOwSzLCguxeTVTJU=".into(),
};
for transport in client_config.transports {
match transport {
ClientConfig::QuicPlain(cc) => assert_eq!(cc, expected_quic),
ClientConfig::TlsPlain(cc) => assert_eq!(cc, expected_tls),
}
}
Ok(())
}
}