diff --git a/.gitignore b/.gitignore index dc886ed..76d0818 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ target/ .DS_STORE .idea/ + +burrow.sock +burrow.db +tmp/ diff --git a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9378372..f0f02af 100644 --- a/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Apple/Burrow.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/SourceKitten.git", "state" : { - "revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56", - "version" : "0.34.1" + "revision" : "fd4df99170f5e9d7cf9aa8312aa8506e0e7a44e7", + "version" : "0.35.0" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", - "version" : "1.2.3" + "revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a", + "version" : "1.2.2" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" + "revision" : "303e5c5c36d6a558407d364878df131c3546fad8", + "version" : "510.0.2" } }, { @@ -51,7 +51,7 @@ "location" : "https://github.com/realm/SwiftLint.git", "state" : { "branch" : "main", - "revision" : "7595ad3fafc1a31086dc40ba01fd898bf6b42d5f" + "revision" : "b42f6ffe77159aed1060bf607212a0410c7623b8" } }, { diff --git a/Cargo.lock b/Cargo.lock index 03794fb..3d443f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -353,6 +353,7 @@ dependencies = [ "rand_core", "ring", "rusqlite", + "rust-ini", "schemars", "serde", "serde_json", @@ -586,6 +587,26 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -641,6 +662,12 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -699,6 +726,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "dyn-clone" version = "1.0.16" @@ -1601,6 +1637,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.3", +] + [[package]] name = "overload" version = "0.1.1" @@ -1947,6 +1993,17 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-ini" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d625ed57d8f49af6cfa514c42e1a71fadcff60eb0b1c517ff82fe41aa025b41" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2347,6 +2404,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2627,6 +2693,12 @@ dependencies = [ "tracing-log 0.2.0", ] +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + [[package]] name = "try-lock" version = "0.2.5" diff --git a/burrow/Cargo.toml b/burrow/Cargo.toml index d47b184..b2367fc 100644 --- a/burrow/Cargo.toml +++ b/burrow/Cargo.toml @@ -24,7 +24,7 @@ clap = { version = "4.4", features = ["derive"] } tracing = "0.1" tracing-log = "0.1" tracing-oslog = { git = "https://github.com/Stormshield-robinc/tracing-oslog" } -tracing-subscriber = { version = "0.3" , features = ["std", "env-filter"] } +tracing-subscriber = { version = "0.3", features = ["std", "env-filter"] } log = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1.0" @@ -51,6 +51,7 @@ once_cell = "1.19" console-subscriber = { version = "0.2.0", optional = true } console = "0.15.8" toml = "0.8.12" +rust-ini = "0.21.0" [dependencies.rusqlite] version = "0.31.0" diff --git a/burrow/src/daemon/instance.rs b/burrow/src/daemon/instance.rs index 018ec9f..6a5cfb0 100644 --- a/burrow/src/daemon/instance.rs +++ b/burrow/src/daemon/instance.rs @@ -17,7 +17,7 @@ use crate::{ ServerConfig, ServerInfo, }, - database::{get_connection, load_interface, dump_interface}, + database::{dump_interface, get_connection, load_interface}, wireguard::{Config, Interface}, }; @@ -116,10 +116,10 @@ impl DaemonInstance { .await?; Ok(DaemonResponseData::None) } - DaemonCommand::AddConfigToml(config_toml) => { + DaemonCommand::AddConfig(cfig_raw) => { let conn = get_connection(self.db_path.as_deref())?; - let cfig = Config::from_toml(&config_toml)?; - let _if_id = dump_interface(&conn, &cfig)?; + let cfig = Config::from_content_fmt(&cfig_raw.content, &cfig_raw.fmt)?; + let _if_id = dump_interface(&conn, &cfig, cfig_raw.interface_id)?; Ok(DaemonResponseData::None) } } diff --git a/burrow/src/daemon/rpc/request.rs b/burrow/src/daemon/rpc/request.rs index 0779abb..9e1e2e9 100644 --- a/burrow/src/daemon/rpc/request.rs +++ b/burrow/src/daemon/rpc/request.rs @@ -3,14 +3,14 @@ use serde::{Deserialize, Serialize}; use tun::TunOptions; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(tag="method", content="params")] +#[serde(tag = "method", content = "params")] pub enum DaemonCommand { Start(DaemonStartOptions), ServerInfo, ServerConfig, Stop, ReloadConfig(String), - AddConfigToml(String), + AddConfig(AddConfigOptions), } #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] @@ -18,6 +18,13 @@ pub struct DaemonStartOptions { pub tun: TunOptions, } +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct AddConfigOptions { + pub content: String, + pub fmt: String, + pub interface_id: Option, +} + #[derive(Clone, Serialize, Deserialize)] pub struct DaemonRequest { pub id: u64, diff --git a/burrow/src/database.rs b/burrow/src/database.rs index 0dbf397..4d1d38e 100644 --- a/burrow/src/database.rs +++ b/burrow/src/database.rs @@ -92,17 +92,36 @@ pub fn load_interface(conn: &Connection, interface_id: &str) -> Result { Ok(Config { interface: iface, peers }) } -pub fn dump_interface(conn: &Connection, config: &Config) -> Result { - let mut stmt = conn.prepare("INSERT INTO wg_interface (private_key, dns, address, listen_port, mtu) VALUES (?, ?, ?, ?, ?)")?; - let cif = &config.interface; - stmt.execute(params![ - cif.private_key, - to_lst(&cif.dns), - to_lst(&cif.address), - cif.listen_port, - cif.mtu - ])?; - let interface_id = conn.last_insert_rowid(); +pub fn dump_interface( + conn: &Connection, + config: &Config, + interface_id: Option, +) -> Result { + let interface_id = if let Some(id) = interface_id { + let mut stmt = conn.prepare("INSERT INTO wg_interface (private_key, dns, address, listen_port, mtu, id) VALUES (?, ?, ?, ?, ?, ?)")?; + let cif = &config.interface; + stmt.execute(params![ + cif.private_key, + to_lst(&cif.dns), + to_lst(&cif.address), + cif.listen_port, + cif.mtu, + id + ])?; + id + } else { + let mut stmt = conn.prepare("INSERT INTO wg_interface (private_key, dns, address, listen_port, mtu) VALUES (?, ?, ?, ?, ?)")?; + let cif = &config.interface; + stmt.execute(params![ + cif.private_key, + to_lst(&cif.dns), + to_lst(&cif.address), + cif.listen_port, + cif.mtu + ])?; + conn.last_insert_rowid() + }; + let mut stmt = conn.prepare("INSERT INTO wg_peer (interface_id, public_key, preshared_key, allowed_ips, endpoint) VALUES (?, ?, ?, ?, ?)")?; for peer in &config.peers { stmt.execute(params![ @@ -121,7 +140,7 @@ pub fn get_connection(path: Option<&Path>) -> Result { if !p.exists() { let conn = Connection::open(p)?; initialize_tables(&conn)?; - dump_interface(&conn, &Config::default())?; + dump_interface(&conn, &Config::default(), None)?; return Ok(conn); } Ok(Connection::open(p)?) @@ -129,8 +148,6 @@ pub fn get_connection(path: Option<&Path>) -> Result { #[cfg(test)] mod tests { - use std::path::Path; - use super::*; #[test] @@ -138,7 +155,7 @@ mod tests { let conn = Connection::open_in_memory().unwrap(); initialize_tables(&conn).unwrap(); let config = Config::default(); - dump_interface(&conn, &config).unwrap(); + dump_interface(&conn, &config, None).unwrap(); let loaded = load_interface(&conn, "1").unwrap(); assert_eq!(config, loaded); } diff --git a/burrow/src/main.rs b/burrow/src/main.rs index 295373a..93ede99 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -1,6 +1,10 @@ +use std::{borrow::Cow, path::PathBuf}; + use anyhow::Result; use clap::{Args, Parser, Subcommand}; +use crate::daemon::rpc::request::AddConfigOptions; + #[cfg(any(target_os = "linux", target_vendor = "apple"))] mod daemon; pub(crate) mod tracing; @@ -47,6 +51,16 @@ enum Commands { ServerConfig, /// Reload Config ReloadConfig(ReloadConfigArgs), + /// Add Server Config + AddConfig(AddServerConfigArgs), +} + +#[derive(Args)] +struct AddServerConfigArgs { + #[clap(short, long)] + path: PathBuf, + #[clap(short, long)] + interface_id: Option, } #[derive(Args)] @@ -59,7 +73,12 @@ struct ReloadConfigArgs { struct StartArgs {} #[derive(Args)] -struct DaemonArgs {} +struct DaemonArgs { + #[clap(long, short)] + socket_path: Option, + #[clap(long, short)] + db_path: Option, +} #[cfg(any(target_os = "linux", target_vendor = "apple"))] async fn try_start() -> Result<()> { @@ -132,6 +151,24 @@ async fn try_reloadconfig(interface_id: String) -> Result<()> { Ok(()) } +#[cfg(any(target_os = "linux", target_vendor = "apple"))] +async fn try_add_server_config(path: &PathBuf, interface_id: Option) -> Result<()> { + let mut client = DaemonClient::new().await?; + let ext = path + .extension() + .map(|e| e.to_string_lossy()) + .unwrap_or_else(|| Cow::Borrowed("toml")); + let content = std::fs::read_to_string(path)?; + let res = client + .send_command(DaemonCommand::AddConfig(AddConfigOptions { + content, + fmt: ext.to_string(), + interface_id, + })) + .await?; + Ok(()) +} + #[cfg(any(target_os = "linux", target_vendor = "apple"))] #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { @@ -139,12 +176,20 @@ async fn main() -> Result<()> { let cli = Cli::parse(); match &cli.command { - Commands::Start(..) => try_start().await?, + Commands::Start(_) => try_start().await?, Commands::Stop => try_stop().await?, - Commands::Daemon(_) => daemon::daemon_main(None, None, None).await?, + Commands::Daemon(daemon_args) => { + daemon::daemon_main( + daemon_args.socket_path.as_ref().map(|p| p.as_path()), + daemon_args.db_path.as_ref().map(|p| p.as_path()), + None, + ) + .await? + } Commands::ServerInfo => try_serverinfo().await?, Commands::ServerConfig => try_serverconfig().await?, Commands::ReloadConfig(args) => try_reloadconfig(args.interface_id.clone()).await?, + Commands::AddConfig(args) => try_add_server_config(&args.path, args.interface_id).await?, } Ok(()) diff --git a/burrow/src/wireguard/config.rs b/burrow/src/wireguard/config.rs index 0fbbc7a..1d9315a 100644 --- a/burrow/src/wireguard/config.rs +++ b/burrow/src/wireguard/config.rs @@ -3,10 +3,12 @@ use std::{net::ToSocketAddrs, str::FromStr}; use anyhow::{anyhow, Error, Result}; use base64::{engine::general_purpose, Engine}; use fehler::throws; +use ini::{Ini, Properties}; use ip_network::IpNetwork; use serde::{Deserialize, Serialize}; use x25519_dalek::{PublicKey, StaticSecret}; +use super::inifield::IniField; use crate::wireguard::{Interface as WgInterface, Peer as WgPeer}; #[throws] @@ -53,6 +55,7 @@ pub struct Interface { #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct Config { + #[serde(rename = "Peer")] pub peers: Vec, pub interface: Interface, // Support for multiple interfaces? } @@ -115,10 +118,69 @@ impl Default for Config { } } +fn props_get(props: &Properties, key: &str) -> T +where + T: From, +{ + IniField::from(props.get(key)).into() +} + +impl TryFrom<&Properties> for Interface { + type Error = anyhow::Error; + + fn try_from(props: &Properties) -> Result { + Ok(Self { + private_key: props_get(props, "PrivateKey"), + address: props_get(props, "Address"), + listen_port: props_get(props, "ListenPort"), + dns: props_get(props, "DNS"), + mtu: props_get(props, "MTU"), + }) + } +} + +impl TryFrom<&Properties> for Peer { + type Error = anyhow::Error; + + fn try_from(props: &Properties) -> Result { + Ok(Self { + public_key: props_get(props, "PublicKey"), + preshared_key: props_get(props, "PresharedKey"), + allowed_ips: props_get(props, "AllowedIPs"), + endpoint: props_get(props, "Endpoint"), + persistent_keepalive: props_get(props, "PersistentKeepalive"), + name: props_get(props, "Name"), + }) + } +} + impl Config { pub fn from_toml(toml: &str) -> Result { toml::from_str(toml).map_err(Into::into) } + + pub fn from_ini(ini: &str) -> Result { + let ini = Ini::load_from_str(ini)?; + let interface = ini + .section(Some("Interface")) + .ok_or(anyhow!("Interface section not found"))?; + let peers = ini.section_all(Some("Peer")); + Ok(Self { + interface: Interface::try_from(interface)?, + peers: peers + .into_iter() + .map(|v| Peer::try_from(v)) + .collect::>>()?, + }) + } + + pub fn from_content_fmt(content: &str, fmt: &str) -> Result { + match fmt { + "toml" => Self::from_toml(content), + "ini" | "conf" => Self::from_ini(content), + _ => Err(anyhow::anyhow!("Unsupported format: {}", fmt)), + } + } } #[cfg(test)] diff --git a/burrow/src/wireguard/inifield.rs b/burrow/src/wireguard/inifield.rs new file mode 100644 index 0000000..fb65a20 --- /dev/null +++ b/burrow/src/wireguard/inifield.rs @@ -0,0 +1,59 @@ +use std::str::FromStr; + +pub struct IniField(String); + +impl FromStr for IniField { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Ok(Self(s.to_string())) + } +} + +impl From for Vec { + fn from(field: IniField) -> Self { + field.0.split(",").map(|s| s.to_string()).collect() + } +} + +impl From for u32 { + fn from(value: IniField) -> Self { + value.0.parse().unwrap() + } +} + +impl From for Option { + fn from(value: IniField) -> Self { + Some(value.0.parse().unwrap()) + } +} + +impl From for String { + fn from(value: IniField) -> Self { + value.0 + } +} + +impl From for Option { + fn from(value: IniField) -> Self { + Some(value.0) + } +} + +impl From> for IniField +where + T: ToString, +{ + fn from(value: Option) -> Self { + match value { + Some(v) => Self(v.to_string()), + None => Self("".to_string()), + } + } +} + +impl IniField { + fn new(value: &str) -> Self { + Self(value.to_string()) + } +} diff --git a/burrow/src/wireguard/mod.rs b/burrow/src/wireguard/mod.rs index 4c70a7f..cfb4585 100755 --- a/burrow/src/wireguard/mod.rs +++ b/burrow/src/wireguard/mod.rs @@ -1,5 +1,6 @@ pub mod config; mod iface; +mod inifield; mod noise; mod pcb; mod peer;