diff --git a/Cargo.lock b/Cargo.lock index 70387f4..98862cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,6 +165,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.8" @@ -323,6 +329,16 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "ioctl-sys" version = "0.8.0" @@ -349,12 +365,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "itoa" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" - [[package]] name = "libc" version = "0.2.150" @@ -482,6 +492,12 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -639,9 +655,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.8" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", "ring", @@ -721,12 +737,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "ryu" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" - [[package]] name = "schannel" version = "0.1.22" @@ -784,6 +794,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_default_utils" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b528184e8ea6409c5574f2cd3d28a81d1c559c7c425a22b4ed371f0f2d3f2b6a" +dependencies = [ + "paste", +] + [[package]] name = "serde_derive" version = "1.0.193" @@ -796,13 +815,11 @@ dependencies = [ ] [[package]] -name = "serde_json" -version = "1.0.108" +name = "serde_spanned" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ - "itoa", - "ryu", "serde", ] @@ -942,7 +959,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.8", + "rustls 0.21.10", "tokio", ] @@ -971,6 +988,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tracing" version = "0.1.40" @@ -1214,6 +1265,15 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "winnow" +version = "0.5.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" +dependencies = [ + "memchr", +] + [[package]] name = "wintun" version = "0.3.2" @@ -1267,12 +1327,13 @@ dependencies = [ "lru", "rand", "rumqttc", - "rustls 0.21.8", + "rustls 0.21.10", "rustls-pemfile 2.0.0", "serde", - "serde_json", + "serde_default_utils", "tokio", "tokio-rustls 0.25.0", "tokio-util", + "toml", "tun", ] diff --git a/Cargo.toml b/Cargo.toml index b60902d..e8aa79f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,12 +24,13 @@ ipnetwork = "0.20.0" log = "0.4.20" lru = "0.12.0" rand = "0.8.5" -rumqttc = { version = "0.23.0", features = ["use-rustls"] } -rustls = "0.21.8" +rustls = "0.21.10" rustls-pemfile = "2.0.0" +rumqttc = { version = "0.23.0", features = ["use-rustls"] } +toml = "0.8.8" serde = { version = "1.0.193", features = ["derive"] } -serde_json = "1.0.108" -tokio = { version = "1.34.0", features = ["full"] } +serde_default_utils = "0.2.1" tokio-rustls = "0.25.0" tokio-util = "0.7.10" +tokio = { version = "1.34.0", features = ["full"] } tun = { version = "0.6.1", features = ["async"] } diff --git a/README.md b/README.md index 2edd09b..54bb5fd 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,10 @@ it's possible capture/inject arbitrary IP packets to/from the target device if t ### Configuration -See [zika_config.sample.json](zika_config.sample.json) +See [zika_config.example.toml](zika_config.example.toml) ### Building -Dependencies: -- cargo -- ... - ``` $ cargo build ``` diff --git a/src/client.rs b/src/client.rs index a82528a..f732174 100644 --- a/src/client.rs +++ b/src/client.rs @@ -39,7 +39,7 @@ struct Tunnel { impl Client { pub async fn from_config(config: config::Config) -> Self { - let mqtt_options = config.broker_mqtt_options(); + let mqtt_options = config.mqtt.to_mqtt_options().expect("valid mqtt config"); let client_config = config.client.expect("non-null client config"); Self::new(mqtt_options, client_config).await } diff --git a/src/config.rs b/src/config.rs index bd76713..919d46a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,67 +1,59 @@ use core::time::Duration; +use std::error::Error; +use std::fs::{read_to_string, File}; +use std::io::{self, BufReader, ErrorKind}; +use std::net::Ipv4Addr; + use rand::{thread_rng, Rng, distributions::Alphanumeric}; use rustls::{Certificate, PrivateKey, RootCertStore}; +use rumqttc::TlsConfiguration; +use toml; use serde::Deserialize; -use std::fs::{read_to_string, File}; -use std::io::BufReader; -use std::net::Ipv4Addr; +use serde_default_utils::*; #[derive(Deserialize, Clone, Debug)] -pub struct MqttOptions { - pub keepalive_interval: Option, - - pub username: Option, - pub password: Option, - - pub ca_file: Option, - pub key_file: Option, - pub cert_file: Option, - - pub topic_alias_max: Option, -} - -#[derive(Deserialize, Debug)] pub struct MqttBroker { - pub options: Option, pub host: String, pub port: u16, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Clone, Debug)] pub struct MqttConfig { - pub options: Option, + pub ca_file: Option, + pub key_file: Option, + pub cert_file: Option, + + #[serde(default = "default_u64::<60>")] + pub keepalive_interval: u64, + #[serde(default = "default_u16::<255>")] + pub topic_alias_max: u16, + pub brokers: Vec, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Clone, Debug)] pub struct ServerConfig { - #[serde(default = "default_id_length")] + #[serde(default = "default_usize::<4>")] pub id_length: usize, - pub topic: String, pub bind_cidr: String, } -fn default_id_length() -> usize { - return 4; -} - -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Clone, Debug)] pub struct ClientTunnel { - #[serde(default = "default_id_length")] + #[serde(default = "default_usize::<4>")] pub id_length: usize, - pub topic: String, pub bind_addr: Ipv4Addr, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Clone, Debug)] pub struct ClientConfig { pub bind_cidr: String, pub tunnels: Vec, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Clone, Debug)] pub struct Config { pub mqtt: MqttConfig, pub server: Option, @@ -71,113 +63,63 @@ pub struct Config { #[derive(Debug)] pub enum ConfigError { IOError(std::io::Error), - DecodeError(serde_json::Error), + DecodeError(toml::de::Error), } pub fn read_from_default_location() -> Result { - let text = read_to_string("zika_config.json").map_err(ConfigError::IOError)?; - let deserialized: Config = serde_json::from_str(&text).map_err(ConfigError::DecodeError)?; + let text = read_to_string("zika_config.toml").map_err(ConfigError::IOError)?; + let deserialized: Config = toml::from_str(&text).map_err(ConfigError::DecodeError)?; return Ok(deserialized); } -impl MqttOptions { - pub fn merge_with_option(&self, another_option: MqttOptions) -> MqttOptions { - MqttOptions { - keepalive_interval: another_option - .keepalive_interval - .or(self.keepalive_interval), - - username: another_option.username.or(self.username.clone()), - password: another_option.password.or(self.password.clone()), - ca_file: another_option.ca_file.or(self.ca_file.clone()), - key_file: another_option.key_file.or(self.key_file.clone()), - cert_file: another_option.cert_file.or(self.cert_file.clone()), - - topic_alias_max: another_option.topic_alias_max.or(self.topic_alias_max), - } - } -} -impl MqttBroker { - pub fn to_mqtt_options( - &self, - base_mqtt_options: &Option, - ) -> rumqttc::v5::MqttOptions { +impl MqttConfig { + pub fn to_mqtt_options(&self) -> Result, Box> { let mut rng = thread_rng(); - let random_id: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); - let mut options = rumqttc::v5::MqttOptions::new(random_id, &self.host, self.port); - let mqtt_options = match (base_mqtt_options, self.options.clone()) { - (Some(b), Some(opts)) => Some(b.merge_with_option(opts)), - (None, o) => o, - (o, None) => o.clone(), - }; - - if let Some(opts) = mqtt_options { - options.set_keep_alive(Duration::new(opts.keepalive_interval.unwrap_or(60), 0)); - options.set_topic_alias_max(opts.topic_alias_max); - - if let (Some(u), Some(p)) = (&opts.username, &opts.password) { - options.set_credentials(u, p); + let mut mqtt_options: Vec = Vec::new(); + for broker_cfg in self.brokers.clone().into_iter() { + let random_id: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); + let mut opts = rumqttc::v5::MqttOptions::new(random_id, &broker_cfg.host, broker_cfg.port); + opts.set_keep_alive(Duration::new(self.keepalive_interval, 0)); + if self.topic_alias_max > 0 { + opts.set_topic_alias_max(Some(self.topic_alias_max)); + } + let tls_builder = if let Some(ca_file) = self.ca_file.clone() { + let mut ca_store = RootCertStore::empty(); + let mut ca_rd = BufReader::new(File::open(ca_file)?); + for ca in rustls_pemfile::certs(&mut ca_rd) { + ca_store.add(&Certificate(ca?.to_vec()))?; + } + let builder = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(ca_store); + Some(builder) + } else { + None }; - - if opts.ca_file.is_some() || (opts.cert_file.is_some() && opts.key_file.is_some()) { - let mut root_cert_store = RootCertStore::empty(); - if let Some(ca_file_path) = opts.ca_file { - let ca_file = File::open(ca_file_path).unwrap(); - let mut ca_buf_read = BufReader::new(ca_file); - let cas = rustls_pemfile::certs(&mut ca_buf_read); - for ca in cas.into_iter() { - root_cert_store - .add(&Certificate(ca.unwrap().to_vec())) - .unwrap(); + match (tls_builder, self.cert_file.clone()) { + (Some(builder), Some(cert_file)) => { + let mut cert_rd = BufReader::new(File::open(cert_file)?); + let mut certs: Vec = Vec::new(); + for der in rustls_pemfile::certs(&mut cert_rd) { + certs.push(Certificate(der?.to_vec())); } + let mut key_rd = BufReader::new(File::open(self.key_file.clone().expect("non-null client key"))?); + let mut key_result = rustls_pemfile::ec_private_keys(&mut key_rd); + let sec1_key = key_result.next().unwrap_or_else(|| Err(io::Error::new(ErrorKind::NotFound, "No EC key")))?; + let key = PrivateKey(sec1_key.secret_sec1_der().to_vec()); + let tls_cfg: TlsConfiguration = builder.with_client_auth_cert(certs, key)?.into(); + opts.set_transport(rumqttc::Transport::tls_with_config(tls_cfg.into())); } - - let tls_config = match (opts.cert_file, opts.key_file) { - (Some(cert_file_path), Some(key_file_path)) => { - let cert_file = File::open(cert_file_path).unwrap(); - let key_file = File::open(key_file_path).unwrap(); - let mut cert_buf_read = BufReader::new(cert_file); - let mut key_buf_read = BufReader::new(key_file); - - let certs = rustls_pemfile::certs(&mut cert_buf_read); - let certs_vec = certs - .into_iter() - .map(|c| Certificate(c.unwrap().to_vec())) - .collect(); - - let mut keys = rustls_pemfile::ec_private_keys(&mut key_buf_read); - let first_key = keys.next().unwrap(); - let key = PrivateKey(first_key.unwrap().secret_sec1_der().to_vec()); - - rustls::ClientConfig::builder() - .with_safe_defaults() - .with_root_certificates(root_cert_store) - .with_client_auth_cert(certs_vec, key) - .unwrap() - } - _ => rustls::ClientConfig::builder() - .with_safe_defaults() - .with_root_certificates(root_cert_store) - .with_no_client_auth(), - }; - - options.set_transport(rumqttc::Transport::tls_with_config(tls_config.into())); + (Some(builder), None) => { + let tls_cfg: TlsConfiguration = builder.with_no_client_auth().into(); + opts.set_transport(rumqttc::Transport::tls_with_config(tls_cfg)); + } + _ => () } - }; - return options; - } -} - -impl Config { - pub fn broker_mqtt_options(&self) -> Vec { - let base_options = &self.mqtt.options; - return self - .mqtt - .brokers - .iter() - .map(|s| s.to_mqtt_options(base_options)) - .collect::>(); + mqtt_options.push(opts); + } + Ok(mqtt_options) } -} +} \ No newline at end of file diff --git a/src/server.rs b/src/server.rs index d05e835..8e17f6e 100644 --- a/src/server.rs +++ b/src/server.rs @@ -31,7 +31,7 @@ pub struct Server { impl Server { pub fn from_config(config: config::Config) -> Self { - let mqtt_options = config.broker_mqtt_options(); + let mqtt_options = config.mqtt.to_mqtt_options().expect("valid mqtt config"); let server_config = config.server.expect("non-null server config"); Self::new(mqtt_options, server_config) } diff --git a/zika_config.example.toml b/zika_config.example.toml new file mode 100644 index 0000000..a878ae3 --- /dev/null +++ b/zika_config.example.toml @@ -0,0 +1,26 @@ +[mqtt] +#ca_file = "ca.pem" +#key_file = "key.pem" +#cert_file = "cert.pem" +#keepalive_interval = 60 +#topic_alias_max = 255 + +[[mqtt.brokers]] +host = "localhost" +port = 1883 + +## NOTE: Tunnel ID and MQTT topic are prepended to all IP packets +## Long IDs / topics will add significant overhead to traffic with many small packets + +[server] +#id_length = 4 +topic = "zika/OjFcZWEAGy2E3Vkh" +bind_cidr = "172.20.0.0/24" + +[client] +bind_cidr = "172.20.0.0/24" + +[[client.tunnels]] +#id_length = 4 +topic = "zika/OjFcZWEAGy2E3Vkh" +bind_addr = "172.20.0.2" diff --git a/zika_config.sample.json b/zika_config.sample.json deleted file mode 100644 index 5ec0966..0000000 --- a/zika_config.sample.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "mqtt": { - "options": { - "// Optional MQTT Auth": "", - "username": "", - "password": "", - - "// Optional TLS": "", - "ca_file": "ca.pem", - "key_file": "key.pem", - "cert_file": "cert.pem", - - "// Unit: seconds (int), Optonal: defaults shown": "", - "keepalive_interval": 60, - - "// Setting to 0 disable topic alias": "", - "topic_alias_max": 5 - }, - - "// Config can be overwritten on broker level": "", - "brokers": [ - { "host": "localhost", "port": 1883 }, - { "host": "localhost", "port": 1883, "options": { "keepalive_interval": 30 } } - ] - }, - - "// NOTE: Tunnel ID and MQTT topic are prepended to all IP packets": "", - "// Long IDs / topics will add significant overhead to traffic with many small packets": "", - - "server": { - "// In bytes (Optional, default shown, must match client)": "", - "id_length": 4, - "topic": "zika/OjFcZWEAGy2E3Vkh", - "bind_cidr": "172.20.0.0/24" - }, - - "client": { - "bind_cidr": "172.20.0.0/24", - "tunnels": [ - { - "topic": "zika/OjFcZWEAGy2E3Vkh", - "bind_addr": "172.20.0.2", - "// In bytes (Optional, default shown, must match server)": "", - "id_length": 4 - } - ] - } -}