diff --git a/zerotier-network-hypervisor/src/util/marshalable.rs b/zerotier-network-hypervisor/src/util/marshalable.rs index 6080d2998..aa5f59141 100644 --- a/zerotier-network-hypervisor/src/util/marshalable.rs +++ b/zerotier-network-hypervisor/src/util/marshalable.rs @@ -8,6 +8,10 @@ use crate::util::buffer::Buffer; +/// Must be larger than any object we want to use with to_bytes() or from_bytes(). +/// This hack can go away once Rust allows us to reference trait consts as generics. +const TEMP_BUF_SIZE: usize = 131072; + /// A super-lightweight zero-allocation serialization interface. pub trait Marshalable: Sized { const MAX_MARSHAL_SIZE: usize; @@ -26,7 +30,6 @@ pub trait Marshalable: Sized { /// /// This will return an Err if the buffer is too small or some other error occurs. It's just /// a shortcut to creating a buffer and marshaling into it. - #[inline(always)] fn to_buffer(&self) -> std::io::Result> { assert!(BL >= Self::MAX_MARSHAL_SIZE); let mut tmp = Buffer::new(); @@ -37,9 +40,28 @@ pub trait Marshalable: Sized { /// Unmarshal this object from a buffer. /// /// This is just a shortcut to calling unmarshal() with a zero cursor and then discarding the cursor. - #[inline(always)] fn from_buffer(buf: &Buffer) -> std::io::Result { let mut tmp = 0; Self::unmarshal(buf, &mut tmp) } + + /// Marshal and convert to a Rust vector. + fn to_bytes(&self) -> Vec { + assert!(Self::MAX_MARSHAL_SIZE <= TEMP_BUF_SIZE); + let mut tmp = Buffer::::new_boxed(); + assert!(self.marshal(&mut tmp).is_ok()); + tmp.as_bytes().to_vec() + } + + /// Unmarshal from a raw slice. + fn from_bytes(b: &[u8]) -> std::io::Result { + if b.len() <= TEMP_BUF_SIZE { + let mut tmp = Buffer::::new_boxed(); + assert!(tmp.append_bytes(b).is_ok()); + let mut cursor = 0; + Self::unmarshal(&tmp, &mut cursor) + } else { + Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "object too large")) + } + } } diff --git a/zerotier-system-service/src/cli/mod.rs b/zerotier-system-service/src/cli/mod.rs new file mode 100644 index 000000000..124ef2266 --- /dev/null +++ b/zerotier-system-service/src/cli/mod.rs @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c)2021 ZeroTier, Inc. + * https://www.zerotier.com/ + */ + +pub mod rootset; diff --git a/zerotier-system-service/src/cli/rootset.rs b/zerotier-system-service/src/cli/rootset.rs new file mode 100644 index 000000000..4be20b6a1 --- /dev/null +++ b/zerotier-system-service/src/cli/rootset.rs @@ -0,0 +1,118 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c)2021 ZeroTier, Inc. + * https://www.zerotier.com/ + */ + +use std::io::Write; + +use clap::ArgMatches; + +use crate::{exitcode, Flags}; + +use zerotier_network_hypervisor::util::marshalable::Marshalable; +use zerotier_network_hypervisor::vl1::RootSet; + +pub async fn cmd(flags: Flags, cmd_args: &ArgMatches) -> i32 { + match cmd_args.subcommand() { + Some(("trust", sc_args)) => todo!(), + + Some(("untrust", sc_args)) => todo!(), + + Some(("list", _)) => todo!(), + + Some(("sign", sc_args)) => { + let path = sc_args.value_of("path"); + let secret_arg = sc_args.value_of("secret"); + if path.is_some() && secret_arg.is_some() { + let path = path.unwrap(); + let secret_arg = secret_arg.unwrap(); + let secret = crate::utils::parse_cli_identity(secret_arg, true).await; + let json_data = crate::utils::read_limit(path, 1048576).await; + if secret.is_err() { + eprintln!("ERROR: unable to parse '{}' or read as a file.", secret_arg); + return exitcode::ERR_IOERR; + } + let secret = secret.unwrap(); + if !secret.secret.is_some() { + eprintln!("ERROR: identity does not include secret key, which is required for signing."); + return exitcode::ERR_IOERR; + } + if json_data.is_err() { + eprintln!("ERROR: unable to read '{}'.", path); + return exitcode::ERR_IOERR; + } + let json_data = json_data.unwrap(); + let root_set = serde_json::from_slice::(json_data.as_slice()); + if root_set.is_err() { + eprintln!("ERROR: root set JSON parsing failed: {}", root_set.err().unwrap().to_string()); + return exitcode::ERR_IOERR; + } + let mut root_set = root_set.unwrap(); + if !root_set.sign(&secret) { + eprintln!("ERROR: root set signing failed, invalid identity?"); + return exitcode::ERR_INTERNAL; + } + println!("{}", crate::utils::to_json_pretty(&root_set)); + } else { + eprintln!("ERROR: 'rootset sign' requires a path to a root set in JSON format and a secret identity."); + return exitcode::ERR_IOERR; + } + } + + Some(("verify", sc_args)) => { + let path = sc_args.value_of("path"); + if path.is_some() { + let path = path.unwrap(); + let json_data = crate::utils::read_limit(path, 1048576).await; + if json_data.is_err() { + eprintln!("ERROR: unable to read '{}'.", path); + return exitcode::ERR_IOERR; + } + let json_data = json_data.unwrap(); + let root_set = serde_json::from_slice::(json_data.as_slice()); + if root_set.is_err() { + eprintln!("ERROR: root set JSON parsing failed: {}", root_set.err().unwrap().to_string()); + return exitcode::ERR_IOERR; + } + let root_set = root_set.unwrap(); + if root_set.verify() { + println!("OK"); + } else { + println!("FAILED"); + return exitcode::ERR_DATA_FORMAT; + } + } else { + eprintln!("ERROR: 'rootset marshal' requires a path to a root set in JSON format."); + return exitcode::ERR_IOERR; + } + } + + Some(("marshal", sc_args)) => { + let path = sc_args.value_of("path"); + if path.is_some() { + let path = path.unwrap(); + let json_data = crate::utils::read_limit(path, 1048576).await; + if json_data.is_err() { + eprintln!("ERROR: unable to read '{}'.", path); + return exitcode::ERR_IOERR; + } + let json_data = json_data.unwrap(); + let root_set = serde_json::from_slice::(json_data.as_slice()); + if root_set.is_err() { + eprintln!("ERROR: root set JSON parsing failed: {}", root_set.err().unwrap().to_string()); + return exitcode::ERR_IOERR; + } + let _ = std::io::stdout().write_all(root_set.unwrap().to_bytes().as_slice()); + } else { + eprintln!("ERROR: 'rootset marshal' requires a path to a root set in JSON format."); + return exitcode::ERR_IOERR; + } + } + + _ => panic!(), + } + return exitcode::OK; +} diff --git a/zerotier-system-service/src/jsonformatter.rs b/zerotier-system-service/src/jsonformatter.rs new file mode 100644 index 000000000..667986664 --- /dev/null +++ b/zerotier-system-service/src/jsonformatter.rs @@ -0,0 +1,134 @@ +/* This is a forked and hacked version of PrettyFormatter from: + * + * https://github.com/serde-rs/json/blob/master/src/ser.rs + * + * It is therefore under the same Apache license. + */ + +use serde_json::ser::Formatter; + +#[derive(Clone, Debug)] +pub struct JsonFormatter<'a> { + current_indent: usize, + has_value: bool, + indent: &'a [u8], +} + +fn indent(wr: &mut W, n: usize, s: &[u8]) -> std::io::Result<()> +where + W: ?Sized + std::io::Write, +{ + for _ in 0..n { + wr.write_all(s)?; + } + Ok(()) +} + +impl<'a> JsonFormatter<'a> { + pub fn new() -> Self { + JsonFormatter::with_indent(b" ") + } + + pub fn with_indent(indent: &'a [u8]) -> Self { + JsonFormatter { current_indent: 0, has_value: false, indent } + } +} + +impl<'a> Default for JsonFormatter<'a> { + fn default() -> Self { + JsonFormatter::new() + } +} + +impl<'a> Formatter for JsonFormatter<'a> { + fn begin_array(&mut self, writer: &mut W) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + self.current_indent += 1; + self.has_value = false; + writer.write_all(b"[") + } + + fn end_array(&mut self, writer: &mut W) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + self.current_indent -= 1; + if self.has_value { + writer.write_all(b" ]") + } else { + writer.write_all(b"]") + } + } + + fn begin_array_value(&mut self, writer: &mut W, first: bool) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + if first { + writer.write_all(b" ")?; + } else { + writer.write_all(b", ")?; + } + Ok(()) + } + + fn end_array_value(&mut self, _writer: &mut W) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + self.has_value = true; + Ok(()) + } + + fn begin_object(&mut self, writer: &mut W) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + self.current_indent += 1; + self.has_value = false; + writer.write_all(b"{") + } + + fn end_object(&mut self, writer: &mut W) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + self.current_indent -= 1; + + if self.has_value { + writer.write_all(b"\n")?; + indent(writer, self.current_indent, self.indent)?; + } + + writer.write_all(b"}") + } + + fn begin_object_key(&mut self, writer: &mut W, first: bool) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + if first { + writer.write_all(b"\n")?; + } else { + writer.write_all(b",\n")?; + } + indent(writer, self.current_indent, self.indent) + } + + fn begin_object_value(&mut self, writer: &mut W) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + writer.write_all(b": ") + } + + fn end_object_value(&mut self, _writer: &mut W) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + self.has_value = true; + Ok(()) + } +} diff --git a/zerotier-system-service/src/main.rs b/zerotier-system-service/src/main.rs index b9fbc93e1..27bb02814 100644 --- a/zerotier-system-service/src/main.rs +++ b/zerotier-system-service/src/main.rs @@ -13,8 +13,10 @@ use clap::{Arg, ArgMatches, Command}; use zerotier_network_hypervisor::{VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION}; +pub mod cli; pub mod exitcode; pub mod getifaddrs; +pub mod jsonformatter; pub mod localconfig; pub mod utils; pub mod vnic; @@ -83,6 +85,8 @@ Advanced Operations: · untrust Stop using a root set · list List root sets in use sign Sign a root set with an identity + verify Load and verify a root set + marshal Dump root set as binary to stdout service Start local service (usually not invoked manually) @@ -101,13 +105,6 @@ pub fn print_help() { let _ = std::io::stdout().write_all(h.as_bytes()); } -pub struct GlobalCommandLineFlags { - pub json_output: bool, - pub base_path: String, - pub auth_token_path_override: Option, - pub auth_token_override: Option, -} - #[cfg(any(target_os = "macos"))] pub fn platform_default_home_path() -> String { "/Library/Application Support/ZeroTier".into() @@ -118,16 +115,16 @@ pub fn platform_default_home_path() -> String { "/var/lib/zerotier".into() } -async fn async_main(cli_args: Box) -> i32 { - let global_cli_flags = GlobalCommandLineFlags { - json_output: cli_args.is_present("json"), - base_path: cli_args.value_of("path").map_or_else(platform_default_home_path, |p| p.to_string()), - auth_token_path_override: cli_args.value_of("token_path").map(|p| p.to_string()), - auth_token_override: cli_args.value_of("token").map(|t| t.to_string()), - }; +pub struct Flags { + pub json_output: bool, + pub base_path: String, + pub auth_token_path_override: Option, + pub auth_token_override: Option, +} +async fn async_main(flags: Flags, global_args: ArgMatches) -> i32 { #[allow(unused)] - return match cli_args.subcommand() { + return match global_args.subcommand() { Some(("help", _)) => { print_help(); exitcode::OK @@ -137,29 +134,30 @@ async fn async_main(cli_args: Box) -> i32 { exitcode::OK } Some(("status", _)) => todo!(), - Some(("set", args)) => todo!(), - Some(("peer", args)) => todo!(), - Some(("network", args)) => todo!(), - Some(("join", args)) => todo!(), - Some(("leave", args)) => todo!(), + Some(("set", cmd_args)) => todo!(), + Some(("peer", cmd_args)) => todo!(), + Some(("network", cmd_args)) => todo!(), + Some(("join", cmd_args)) => todo!(), + Some(("leave", cmd_args)) => todo!(), Some(("service", _)) => todo!(), - Some(("identity", args)) => todo!(), - Some(("rootset", args)) => todo!(), + Some(("identity", cmd_args)) => todo!(), + Some(("rootset", cmd_args)) => cli::rootset::cmd(flags, cmd_args).await, _ => { - print_help(); + eprintln!("Invalid command line. Use 'help' for help."); exitcode::ERR_USAGE } }; } fn main() { - let cli_args = Box::new({ + let global_args = { let help = make_help(); Command::new("zerotier") .arg(Arg::new("json").short('j')) .arg(Arg::new("path").short('p').takes_value(true)) .arg(Arg::new("token_path").short('t').takes_value(true)) .arg(Arg::new("token").short('T').takes_value(true)) + .subcommand_required(true) .subcommand(Command::new("help")) .subcommand(Command::new("version")) .subcommand(Command::new("status")) @@ -198,7 +196,9 @@ fn main() { .subcommand(Command::new("trust").arg(Arg::new("path").index(1).required(true))) .subcommand(Command::new("untrust").arg(Arg::new("name").index(1).required(true))) .subcommand(Command::new("list")) - .subcommand(Command::new("sign").arg(Arg::new("path").index(1).required(true)).arg(Arg::new("secret").index(2).required(true))), + .subcommand(Command::new("sign").arg(Arg::new("path").index(1).required(true)).arg(Arg::new("secret").index(2).required(true))) + .subcommand(Command::new("verify").arg(Arg::new("path").index(1).required(true))) + .subcommand(Command::new("marshal").arg(Arg::new("path").index(1).required(true))), ) .override_help(help.as_str()) .override_usage("") @@ -207,7 +207,7 @@ fn main() { .disable_help_flag(true) .try_get_matches_from(std::env::args()) .unwrap_or_else(|e| { - if e.kind() == clap::ErrorKind::DisplayHelp { + if e.kind() == clap::ErrorKind::DisplayHelp || e.kind() == clap::ErrorKind::MissingSubcommand { print_help(); std::process::exit(exitcode::OK); } else { @@ -225,18 +225,25 @@ fn main() { } } if invalid.is_empty() { - println!("Invalid command line. Use 'help' for help."); + eprintln!("Invalid command line. Use 'help' for help."); } else { if suggested.is_empty() { - println!("Unrecognized option '{}'. Use 'help' for help.", invalid); + eprintln!("Unrecognized option '{}'. Use 'help' for help.", invalid); } else { - println!("Unrecognized option '{}', did you mean {}? Use 'help' for help.", invalid, suggested); + eprintln!("Unrecognized option '{}', did you mean {}? Use 'help' for help.", invalid, suggested); } } std::process::exit(exitcode::ERR_USAGE); } }) - }); + }; - std::process::exit(tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on(async_main(cli_args))); + let flags = Flags { + json_output: global_args.is_present("json"), + base_path: global_args.value_of("path").map_or_else(platform_default_home_path, |p| p.to_string()), + auth_token_path_override: global_args.value_of("token_path").map(|p| p.to_string()), + auth_token_override: global_args.value_of("token").map(|t| t.to_string()), + }; + + std::process::exit(tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on(async_main(flags, global_args))); } diff --git a/zerotier-system-service/src/utils.rs b/zerotier-system-service/src/utils.rs index 44d224706..5fd1cce3b 100644 --- a/zerotier-system-service/src/utils.rs +++ b/zerotier-system-service/src/utils.rs @@ -6,17 +6,20 @@ * https://www.zerotier.com/ */ +use std::error::Error; use std::str::FromStr; use std::time::{Instant, SystemTime, UNIX_EPOCH}; use serde::de::DeserializeOwned; -use serde::Serialize; +use serde::{Serialize, Serializer}; use lazy_static::lazy_static; use tokio::fs::File; use tokio::io::{AsyncRead, AsyncReadExt}; +use crate::jsonformatter::JsonFormatter; + use zerotier_network_hypervisor::vl1::Identity; lazy_static! { @@ -63,16 +66,6 @@ pub fn is_valid_port(v: &str) -> Result<(), String> { Err(format!("invalid TCP/IP port number: {}", v)) } -/// Shortcut to use serde_json to serialize an object, returns "null" on error. -pub fn to_json(o: &O) -> String { - serde_json::to_string(o).unwrap_or("null".into()) -} - -/// Shortcut to use serde_json to serialize an object, returns "null" on error. -pub fn to_json_pretty(o: &O) -> String { - serde_json::to_string_pretty(o).unwrap_or("null".into()) -} - /// Recursively patch a JSON object. /// /// This is slightly different from a usual JSON merge. For objects in the target their fields @@ -131,6 +124,22 @@ pub fn json_patch_object(obj: O, patch: &s ) } +/// Shortcut to use serde_json to serialize an object, returns "null" on error. +pub fn to_json(o: &O) -> String { + serde_json::to_string(o).unwrap_or("null".into()) +} + +/// Shortcut to use serde_json to serialize an object, returns "null" on error. +pub fn to_json_pretty(o: &O) -> String { + let mut buf = Vec::new(); + let mut ser = serde_json::Serializer::with_formatter(&mut buf, JsonFormatter::new()); + if o.serialize(&mut ser).is_ok() { + String::from_utf8(buf).unwrap_or_else(|_| "null".into()) + } else { + "null".into() + } +} + /// Convenience function to read up to limit bytes from a file. /// /// If the file is larger than limit, the excess is not read. diff --git a/zerotier-system-service/zerotier-rootset.json b/zerotier-system-service/zerotier-rootset.json new file mode 100644 index 000000000..5a7598e12 --- /dev/null +++ b/zerotier-system-service/zerotier-rootset.json @@ -0,0 +1,25 @@ +{ + "name": "root.zerotier.com", + "revision": 1, + "members": [ { + "identity": "62f865ae71:0:e2076c57de870e6288d7d5e7404408b1545efca37d67f77b87e9e54168c25d3ef1a9abf2905ea5e785c01dff23887ad4232d95c7a8fd2c27111a72bd159322dc", + "endpoints": [ "udp:50.7.252.138/9993", "udp:2001:49f0:d0db:2::2/9993" ], + "signature": [ 1, 45, 14, 211, 108, 240, 151, 85, 43, 241, 113, 18, 24, 45, 198, 197, 67, 254, 96, 138, 194, 77, 170, 156, 168, 31, 240, 55, 168, 108, 69, 135, 253, 198, 153, 36, 166, 200, 222, 157, 122, 50, 149, 149, 40, 35, 125, 93, 78, 228, 51, 245, 53, 238, 133, 84, 188, 190, 98, 145, 177, 19, 54, 154, 0 ], + "flags": 0 + }, { + "identity": "778cde7190:0:3f6681a99e5ad1895e9fba33e6212d4454e168bcec7112101bf000956ed8e92e42892cb6f2ec410881a84ab19da50e1287ba3d926c3a1f755cccf299a1207055", + "endpoints": [ "udp:103.195.103.66/9993", "udp:2605:9880:400:c3:254:f2bc:a1f7:19/9993" ], + "signature": [ 1, 202, 181, 145, 69, 58, 169, 42, 149, 210, 160, 77, 220, 56, 246, 54, 210, 161, 144, 158, 103, 70, 104, 236, 58, 66, 127, 100, 117, 242, 208, 70, 68, 87, 142, 163, 222, 231, 146, 60, 205, 180, 202, 18, 181, 137, 216, 204, 109, 118, 224, 86, 220, 26, 142, 61, 18, 50, 174, 173, 44, 167, 231, 249, 0 ], + "flags": 0 + }, { + "identity": "cafe04eba9:0:6c6a9d1dea55c1616bfe2a2b8f0ff9a8cacaf70374fb1f39e3bef81cbfebef17b7228268a0a2a29d3488c752565c6c965cbd6506ec24397cc8a5d9d15285a87f", + "endpoints": [ "udp:84.17.53.155/9993", "udp:2a02:6ea0:d405::9993/9993" ], + "signature": [ 1, 129, 31, 37, 249, 242, 179, 153, 184, 117, 15, 192, 41, 69, 112, 196, 189, 18, 57, 96, 33, 82, 31, 142, 57, 251, 151, 118, 86, 71, 11, 170, 197, 11, 20, 55, 74, 66, 10, 248, 133, 216, 88, 212, 34, 139, 128, 179, 246, 241, 8, 126, 105, 195, 126, 235, 140, 219, 66, 92, 166, 203, 111, 132, 0 ], + "flags": 0 + }, { + "identity": "cafe9efeb9:0:ccdef76bc7b97ded904eabc5df09886d9c1514a610036cb9139cc214001a2958978efcec15712dd3948c6e6b3a8e893df01ff493d1f8d9806a860c5420571bf0", + "endpoints": [ "udp:104.194.8.134/9993", "udp:2605:9880:200:1200:30:571:e34:51/9993" ], + "signature": [ 1, 254, 236, 249, 244, 29, 229, 55, 85, 171, 15, 42, 222, 51, 237, 237, 47, 54, 158, 123, 96, 24, 101, 207, 63, 82, 113, 254, 154, 225, 188, 147, 75, 115, 243, 200, 253, 221, 198, 234, 74, 168, 126, 13, 137, 143, 13, 56, 73, 206, 242, 29, 97, 8, 221, 31, 236, 187, 86, 190, 15, 65, 184, 253, 13 ], + "flags": 0 + } ] +}