diff --git a/zerotier-network-hypervisor/src/vl1/hybridkey.rs b/attic/hybridkey.rs similarity index 100% rename from zerotier-network-hypervisor/src/vl1/hybridkey.rs rename to attic/hybridkey.rs diff --git a/zerotier-network-hypervisor/Cargo.toml b/zerotier-network-hypervisor/Cargo.toml index fb2308def..752e269a5 100644 --- a/zerotier-network-hypervisor/Cargo.toml +++ b/zerotier-network-hypervisor/Cargo.toml @@ -19,7 +19,7 @@ metrohash = "^1" dashmap = "^5" parking_lot = "^0" lazy_static = "^1" -serde = { version = "^1", features = [], default-features = false } +serde = { version = "^1", features = ["derive"], default-features = false } [target."cfg(not(windows))".dependencies] libc = "^0" diff --git a/zerotier-network-hypervisor/src/util/buffer.rs b/zerotier-network-hypervisor/src/util/buffer.rs index e14545444..d170233ce 100644 --- a/zerotier-network-hypervisor/src/util/buffer.rs +++ b/zerotier-network-hypervisor/src/util/buffer.rs @@ -112,6 +112,12 @@ impl Buffer { &mut self.1[0..self.0] } + /// Get a mutable reference to the entire buffer regardless of the current 'size'. + #[inline(always)] + pub unsafe fn entire_buffer_mut(&mut self) -> &mut [u8; L] { + &mut self.1 + } + #[inline(always)] pub fn as_ptr(&self) -> *const u8 { self.1.as_ptr() diff --git a/zerotier-network-hypervisor/src/vl1/address.rs b/zerotier-network-hypervisor/src/vl1/address.rs index 2e586264c..ea9799e05 100644 --- a/zerotier-network-hypervisor/src/vl1/address.rs +++ b/zerotier-network-hypervisor/src/vl1/address.rs @@ -127,7 +127,7 @@ impl<'de> serde::de::Visitor<'de> for AddressVisitor { if v.len() == ADDRESS_SIZE { Address::from_bytes(v).map_or_else(|| Err(E::custom("object too large")), |a| Ok(a)) } else { - Err(E::custom("object too large")) + Err(E::custom("object size incorrect")) } } diff --git a/zerotier-network-hypervisor/src/vl1/endpoint.rs b/zerotier-network-hypervisor/src/vl1/endpoint.rs index 202f372ec..8bcd7946e 100644 --- a/zerotier-network-hypervisor/src/vl1/endpoint.rs +++ b/zerotier-network-hypervisor/src/vl1/endpoint.rs @@ -220,6 +220,7 @@ impl Marshalable for Endpoint { } } } + impl Hash for Endpoint { fn hash(&self, state: &mut H) { match self { @@ -244,7 +245,7 @@ impl Hash for Endpoint { } Endpoint::Ip(ip) => { state.write_u8(TYPE_IP); - ip.hash(state); + ip.ip_bytes().hash(state); } Endpoint::IpUdp(ip) => { state.write_u8(TYPE_IPUDP); diff --git a/zerotier-network-hypervisor/src/vl1/mod.rs b/zerotier-network-hypervisor/src/vl1/mod.rs index a8fe50293..03a1f0abc 100644 --- a/zerotier-network-hypervisor/src/vl1/mod.rs +++ b/zerotier-network-hypervisor/src/vl1/mod.rs @@ -15,10 +15,9 @@ mod dictionary; mod mac; mod path; mod peer; -mod rootcluster; +mod rootset; pub(crate) mod fragmentedpacket; -pub(crate) mod hybridkey; pub(crate) mod node; #[allow(unused)] pub(crate) mod protocol; @@ -34,4 +33,4 @@ pub use mac::MAC; pub use node::{Node, SystemInterface}; pub use path::Path; pub use peer::Peer; -pub use rootcluster::{Root, RootCluster}; +pub use rootset::{Root, RootSet}; diff --git a/zerotier-network-hypervisor/src/vl1/node.rs b/zerotier-network-hypervisor/src/vl1/node.rs index e7e10947c..f8ea74614 100644 --- a/zerotier-network-hypervisor/src/vl1/node.rs +++ b/zerotier-network-hypervisor/src/vl1/node.rs @@ -24,7 +24,7 @@ use crate::vl1::path::Path; use crate::vl1::peer::Peer; use crate::vl1::protocol::*; use crate::vl1::whoisqueue::{QueuedPacket, WhoisQueue}; -use crate::vl1::{Address, Endpoint, Identity, RootCluster}; +use crate::vl1::{Address, Endpoint, Identity, RootSet}; use crate::{PacketBuffer, PacketBufferFactory, PacketBufferPool}; /// Trait implemented by external code to handle events and provide an interface to the system or application. @@ -132,6 +132,12 @@ struct BackgroundTaskIntervals { root_hello: IntervalGate, } +struct RootInfo { + roots: HashMap, Vec>, + sets: HashMap, + sets_modified: bool, +} + /// A VL1 global P2P network node. pub struct Node { /// A random ID generated to identify this particular running instance. @@ -144,13 +150,13 @@ pub struct Node { intervals: Mutex, /// Canonicalized network paths, held as Weak<> to be automatically cleaned when no longer in use. - paths: DashMap>, + paths: DashMap<(u64, u64), Weak>, /// Peers with which we are currently communicating. peers: DashMap>, /// This node's trusted roots, sorted in ascending order of quality/preference, and cluster definitions. - roots: Mutex<(Vec>, Vec)>, + roots: Mutex, /// Current best root. best_root: RwLock>>, @@ -197,7 +203,11 @@ impl Node { intervals: Mutex::new(BackgroundTaskIntervals::default()), paths: DashMap::new(), peers: DashMap::new(), - roots: Mutex::new((Vec::new(), Vec::new())), + roots: Mutex::new(RootInfo { + roots: HashMap::new(), + sets: HashMap::new(), + sets_modified: false, + }), best_root: RwLock::new(None), whois: WhoisQueue::new(), buffer_pool: PacketBufferPool::new(64, PacketBufferFactory::new()), @@ -235,89 +245,83 @@ impl Node { let tt = si.time_ticks(); if intervals.root_sync.gate(tt) { - let mut roots_lock = self.roots.lock(); - let (roots, root_clusters) = &mut *roots_lock; + match &mut (*self.roots.lock()) { + RootInfo { roots, sets, sets_modified } => { + // Sychronize root info with root sets info if the latter has changed. + if *sets_modified { + *sets_modified = false; + roots.clear(); + let mut colliding_root_addresses = Vec::new(); // see security note below + for (_, rc) in sets.iter() { + for m in rc.members.iter() { + if m.endpoints.is_some() && !colliding_root_addresses.contains(&m.identity.address) { + /* + * SECURITY NOTE: it should be impossible to get an address/identity collision here unless + * the user adds a maliciously crafted root set with an identity that collides another. Under + * normal circumstances the root backplane combined with the address PoW should rule this + * out. However since we trust roots as identity lookup authorities it's important to take + * extra care to check for this case. If it's detected, all roots with the offending + * address are ignored/disabled. + * + * The apparently over-thought functional chain here on peers.entry() is to make access to + * the peer map atomic since we use a "lock-free" data structure here (DashMap). + */ - // Look at root cluster definitions and make sure all have corresponding root peers. - let mut root_endpoints = HashMap::with_capacity(roots.len() * 2); - for rc in root_clusters.iter() { - for m in rc.members.iter() { - if m.endpoints.is_some() { - let endpoints = m.endpoints.as_ref().unwrap(); - - /* - * SECURITY NOTE: we take extra care to handle the case where we have a peer whose identity - * differs from that of a root but whose address is the same. It should be impossible to - * make this happen, but we check anyway. It would require a colliding identity to be - * approved by one of your existing roots and somehow retrieved via WHOIS, but this would - * be hard because this background task loop populates the peer list with specified root - * identities before this happens. It could also happen if you have two root cluster - * definitions with a colliding address, which would itself be hard to produce and would - * probably mean someone is doing something nasty. - * - * In this case the response is to ignore this root entirely and generate a warning. - */ - - // This functional stuff on entry() is to do all this atomically while holding the map's entry - // object, since this is a "lock-free" structure. - let _ = self - .peers - .entry(m.identity.address) - .or_try_insert_with(|| { - Peer::new(&self.identity, m.identity.clone(), tt).map_or(Err(crate::error::UnexpectedError), |new_root| { - let new_root = Arc::new(new_root); - roots.retain(|r| r.identity.address != m.identity.address); // sanity check, should be impossible - roots.push(new_root.clone()); - Ok(new_root) - }) - }) - .and_then(|root_peer_entry| { - let rp = root_peer_entry.value(); - if rp.identity.eq(&m.identity) { - Ok(root_peer_entry) - } else { - roots.retain(|r| r.identity.address != m.identity.address); - si.event_security_warning(format!("address/identity collision between root {} (from root cluster definition '{}') and known peer {}", m.identity.address.to_string(), rc.name, rp.identity.to_string()).as_str()); - Err(crate::error::UnexpectedError) + let _ = self + .peers + .entry(m.identity.address) + .or_try_insert_with(|| Peer::new(&self.identity, m.identity.clone(), tt).map_or(Err(crate::error::UnexpectedError), |new_root| Ok(Arc::new(new_root)))) + .and_then(|root_peer_entry| { + let rp = root_peer_entry.value(); + if rp.identity.eq(&m.identity) { + Ok(root_peer_entry) + } else { + colliding_root_addresses.push(m.identity.address); + si.event_security_warning( + format!("address/identity collision between root {} (from root cluster definition '{}') and known peer {}", m.identity.address.to_string(), rc.name, rp.identity.to_string()).as_str(), + ); + Err(crate::error::UnexpectedError) + } + }) + .map(|r| roots.insert(r.value().clone(), m.endpoints.as_ref().unwrap().iter().map(|e| e.clone()).collect())); } - }) - .map(|_| { - let _ = root_endpoints.insert(m.identity.address, endpoints); - }); + } + } } - } - } - // Remove all roots not in any current root cluster definition. - roots.retain(|r| root_endpoints.contains_key(&r.identity.address)); - - // Say HELLO to all roots periodically. For roots we send HELLO to every single endpoint - // they have, which is a behavior that differs from normal peers. This allows roots to - // e.g. see our IPv4 and our IPv6 address which can be important for us to learn our - // external addresses from them. - assert!(ROOT_SYNC_INTERVAL_MS <= (ROOT_HELLO_INTERVAL / 2)); - if intervals.root_hello.gate(tt) { - for r in roots.iter() { - for ep in root_endpoints.get(&r.identity.address).unwrap().iter() { - r.send_hello(si, self, Some(ep)); + // Say HELLO to all roots periodically. For roots we send HELLO to every single endpoint + // they have, which is a behavior that differs from normal peers. This allows roots to + // e.g. see our IPv4 and our IPv6 address which can be important for us to learn our + // external addresses from them. + assert!(ROOT_SYNC_INTERVAL_MS <= (ROOT_HELLO_INTERVAL / 2)); + if intervals.root_hello.gate(tt) { + for (root, endpoints) in roots.iter() { + for ep in endpoints.iter() { + root.send_hello(si, self, Some(ep)); + } + } } - } - } - // The best root is the one that has replied to a HELLO most recently. Since we send HELLOs in unison - // this is a proxy for latency and also causes roots that fail to reply to drop out quickly. - if !roots.is_empty() { - roots.sort_unstable_by(|a, b| a.last_hello_reply_time_ticks.load(Ordering::Relaxed).cmp(&b.last_hello_reply_time_ticks.load(Ordering::Relaxed))); - let _ = self.best_root.write().insert(roots.last().unwrap().clone()); - } else { - let _ = self.best_root.write().take(); + // The best root is the one that has replied to a HELLO most recently. Since we send HELLOs in unison + // this is a proxy for latency and also causes roots that fail to reply to drop out quickly. + let mut latest_hello_reply = 0; + let mut best: Option<&Arc> = None; + for (r, _) in roots.iter() { + let t = r.last_hello_reply_time_ticks.load(Ordering::Relaxed); + if t >= latest_hello_reply { + latest_hello_reply = t; + let _ = best.insert(r); + } + } + *(self.best_root.write()) = best.cloned(); + } } } if intervals.peers.gate(tt) { // Service all peers, removing any whose service() method returns false AND that are not // roots. Roots on the other hand remain in the peer list as long as they are roots. - self.peers.retain(|_, peer| if peer.service(si, self, tt) { true } else { !self.roots.lock().0.iter().any(|r| Arc::ptr_eq(peer, r)) }); + self.peers.retain(|_, peer| if peer.service(si, self, tt) { true } else { !self.roots.lock().roots.contains_key(peer) }); } if intervals.paths.gate(tt) { @@ -339,7 +343,7 @@ impl Node { if dest == self.identity.address { // Handle packets (seemingly) addressed to this node. - let path = self.path_to_endpoint(source_endpoint, source_local_socket, source_local_interface); + let path = self.canonical_path(source_endpoint, source_local_socket, source_local_interface); path.log_receive_anything(time_ticks); if fragment_header.is_fragment() { @@ -407,16 +411,35 @@ impl Node { /// Return true if a peer is a root. pub fn is_peer_root(&self, peer: &Peer) -> bool { - self.roots.lock().0.iter().any(|p| Arc::as_ptr(p) == (peer as *const Peer)) + self.roots.lock().roots.contains_key(peer) + } + + pub fn add_update_root_set(&self, rs: RootSet) -> bool { + let mut roots = self.roots.lock(); + let entry = roots.sets.get_mut(&rs.name); + if entry.is_some() { + let old_rs = entry.unwrap(); + if rs.should_replace(old_rs) { + *old_rs = rs; + roots.sets_modified = true; + return true; + } + } else { + if rs.verify() { + roots.sets.insert(rs.name.clone(), rs); + roots.sets_modified = true; + return true; + } + } + return false; } /// Get the canonical Path object for a given endpoint and local socket information. /// /// This is a canonicalizing function that returns a unique path object for every tuple /// of endpoint, local socket, and local interface. - pub fn path_to_endpoint(&self, ep: &Endpoint, local_socket: Option, local_interface: Option) -> Arc { - let key = Path::local_lookup_key(ep, local_socket, local_interface); - let mut path_entry = self.paths.entry(key).or_insert_with(|| Weak::new()); + pub fn canonical_path(&self, ep: &Endpoint, local_socket: Option, local_interface: Option) -> Arc { + let mut path_entry = self.paths.entry(Path::local_lookup_key(ep, local_socket, local_interface)).or_default(); if let Some(path) = path_entry.value().upgrade() { path } else { @@ -426,7 +449,3 @@ impl Node { } } } - -unsafe impl Send for Node {} - -unsafe impl Sync for Node {} diff --git a/zerotier-network-hypervisor/src/vl1/path.rs b/zerotier-network-hypervisor/src/vl1/path.rs index 44d4ae5f0..31ad7af36 100644 --- a/zerotier-network-hypervisor/src/vl1/path.rs +++ b/zerotier-network-hypervisor/src/vl1/path.rs @@ -42,7 +42,7 @@ pub struct Path { impl Path { /// Get a 128-bit key to look up this endpoint in the local node path map. - pub(crate) fn local_lookup_key(endpoint: &Endpoint, local_socket: Option, local_interface: Option) -> u128 { + pub(crate) fn local_lookup_key(endpoint: &Endpoint, local_socket: Option, local_interface: Option) -> (u64, u64) { let mut h = MetroHash128::with_seed(*METROHASH_SEED); h.write_u64(local_socket.map_or(0, |s| s.get() as u64)); h.write_u64(local_interface.map_or(0, |s| s.get() as u64)); @@ -89,8 +89,7 @@ impl Path { h.write(fingerprint); } } - assert_eq!(std::mem::size_of::<(u64, u64)>(), std::mem::size_of::()); - unsafe { std::mem::transmute(h.finish128()) } + h.finish128() } pub fn new(endpoint: Endpoint, local_socket: Option, local_interface: Option) -> Self { diff --git a/zerotier-network-hypervisor/src/vl1/peer.rs b/zerotier-network-hypervisor/src/vl1/peer.rs index d435bcad2..0b4c057e4 100644 --- a/zerotier-network-hypervisor/src/vl1/peer.rs +++ b/zerotier-network-hypervisor/src/vl1/peer.rs @@ -7,6 +7,7 @@ */ use std::convert::TryInto; +use std::hash::{Hash, Hasher}; use std::mem::MaybeUninit; use std::num::NonZeroI64; use std::sync::atomic::{AtomicI64, AtomicU64, AtomicU8, Ordering}; @@ -32,8 +33,11 @@ use crate::vl1::{Dictionary, Endpoint, Identity, Path}; use crate::{PacketBuffer, VERSION_MAJOR, VERSION_MINOR, VERSION_PROTO, VERSION_REVISION}; /// A remote peer known to this node. -/// Sending-related and receiving-related fields are locked separately since concurrent -/// send/receive is not uncommon. +/// +/// NOTE: this implements PartialEq/Eq and Hash in terms of the pointer identity of +/// the structure. This means two peers are equal only if they are the same instance in +/// memory. This is done because they are only stored in an Arc<> internally and we want +/// to use these as efficient hash map keys. pub struct Peer { // This peer's identity. pub(crate) identity: Identity, @@ -583,6 +587,22 @@ impl Peer { } } +impl PartialEq for Peer { + #[inline(always)] + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self as *const Peer, other as *const Peer) + } +} + +impl Eq for Peer {} + +impl Hash for Peer { + #[inline(always)] + fn hash(&self, state: &mut H) { + state.write_usize((self as *const Peer) as usize) + } +} + impl BackgroundServicable for Peer { const SERVICE_INTERVAL_MS: i64 = EPHEMERAL_SECRET_REKEY_AFTER_TIME / 10; diff --git a/zerotier-network-hypervisor/src/vl1/protocol.rs b/zerotier-network-hypervisor/src/vl1/protocol.rs index bd716ab48..1e6e79a5d 100644 --- a/zerotier-network-hypervisor/src/vl1/protocol.rs +++ b/zerotier-network-hypervisor/src/vl1/protocol.rs @@ -189,23 +189,23 @@ pub const ROOT_HELLO_INTERVAL: i64 = PATH_KEEPALIVE_INTERVAL * 2; /// Proof of work difficulty (threshold) for identity generation. pub const IDENTITY_POW_THRESHOLD: u8 = 17; -/// Compress a packet and return true if compressed. -/// The 'dest' buffer must be empty (will panic otherwise). A return value of false indicates an error or -/// that the data was not compressible. The state of the destination buffer is undefined on a return -/// value of false. -pub fn compress_packet(src: &[u8], dest: &mut Buffer<{ PACKET_SIZE_MAX }>) -> bool { - if src.len() > PACKET_VERB_INDEX { - debug_assert!(dest.is_empty()); - let cs = { - let d = dest.as_bytes_mut(); +/// Attempt to compress a packet's payload with LZ4 +/// +/// If this returns true the destination buffer will contain a compressed packet. If false is +/// returned the contents of 'dest' are entirely undefined. This indicates that the data was not +/// compressable or some other error occurred. +pub fn compress_packet(src: &[u8], dest: &mut Buffer) -> bool { + if src.len() > (PACKET_VERB_INDEX + 16) { + let compressed_data_size = { + let d = unsafe { dest.entire_buffer_mut() }; d[0..PACKET_VERB_INDEX].copy_from_slice(&src[0..PACKET_VERB_INDEX]); d[PACKET_VERB_INDEX] = src[PACKET_VERB_INDEX] | VERB_FLAG_COMPRESSED; lz4_flex::block::compress_into(&src[PACKET_VERB_INDEX + 1..], &mut d[PACKET_VERB_INDEX + 1..]) }; - if cs.is_ok() { - let cs = cs.unwrap(); - if cs > 0 && cs < (src.len() - PACKET_VERB_INDEX) { - unsafe { dest.set_size_unchecked(PACKET_VERB_INDEX + 1 + cs) }; + if compressed_data_size.is_ok() { + let compressed_data_size = compressed_data_size.unwrap(); + if compressed_data_size > 0 && compressed_data_size < (src.len() - PACKET_VERB_INDEX) { + unsafe { dest.set_size_unchecked(PACKET_VERB_INDEX + 1 + compressed_data_size) }; return true; } } diff --git a/zerotier-network-hypervisor/src/vl1/rootcluster.rs b/zerotier-network-hypervisor/src/vl1/rootset.rs similarity index 92% rename from zerotier-network-hypervisor/src/vl1/rootcluster.rs rename to zerotier-network-hypervisor/src/vl1/rootset.rs index cabc90be3..9d977cb36 100644 --- a/zerotier-network-hypervisor/src/vl1/rootcluster.rs +++ b/zerotier-network-hypervisor/src/vl1/rootset.rs @@ -15,8 +15,10 @@ use crate::vl1::identity::*; use crate::vl1::protocol::PACKET_SIZE_MAX; use crate::vl1::Endpoint; +use serde::{Deserialize, Serialize}; + /// Description of a member of a root cluster. -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Root { /// Full identity of this node. pub identity: Identity, @@ -24,10 +26,12 @@ pub struct Root { /// Endpoints for this root or None if this is a former member attesting to an update that removes it. pub endpoints: Option>, - /// Signature of entire cluster by this identity. - pub cluster_signature: Vec, + /// Signature of entire root set by this identity. + #[serde(default)] + pub signature: Vec, /// Flags field (currently unused). + #[serde(default)] pub flags: u64, } @@ -54,8 +58,8 @@ impl Ord for Root { /// To build a cluster definition first use new(), then use add() to add all members, then have each member /// use sign() to sign its entry. All members must sign after all calls to add() have been made since everyone /// must sign the same definition. -#[derive(Clone, PartialEq, Eq)] -pub struct RootCluster { +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RootSet { /// An arbitrary name, which could be something like a domain. pub name: String, @@ -67,7 +71,7 @@ pub struct RootCluster { pub members: Vec, } -impl RootCluster { +impl RootSet { pub fn new(name: String, revision: u64) -> Self { Self { name, revision, members: Vec::new() } } @@ -90,8 +94,8 @@ impl RootCluster { buf.append_varint(0)?; } if include_signatures { - buf.append_varint(m.cluster_signature.len() as u64)?; - buf.append_bytes(m.cluster_signature.as_slice())?; + buf.append_varint(m.signature.len() as u64)?; + buf.append_bytes(m.signature.as_slice())?; } buf.append_varint(m.flags)?; buf.append_varint(0)?; // size of additional fields for future use @@ -115,7 +119,7 @@ impl RootCluster { let tmp = self.marshal_for_signing(); for m in self.members.iter() { - if m.cluster_signature.is_empty() || !m.identity.verify(tmp.as_bytes(), m.cluster_signature.as_slice()) { + if m.signature.is_empty() || !m.identity.verify(tmp.as_bytes(), m.signature.as_slice()) { return false; } } @@ -135,7 +139,7 @@ impl RootCluster { } tmp }), - cluster_signature: Vec::new(), + signature: Vec::new(), flags: 0, }); self.members.sort(); @@ -157,7 +161,7 @@ impl RootCluster { let _ = self.members.push(Root { identity: unsigned_entry.identity, endpoints: unsigned_entry.endpoints, - cluster_signature: signature.unwrap(), + signature: signature.unwrap(), flags: unsigned_entry.flags, }); self.members.sort(); @@ -208,7 +212,7 @@ impl RootCluster { } } -impl Marshalable for RootCluster { +impl Marshalable for RootSet { const MAX_MARSHAL_SIZE: usize = PACKET_SIZE_MAX; #[inline(always)] @@ -232,7 +236,7 @@ impl Marshalable for RootCluster { let mut m = Root { identity: Identity::unmarshal(buf, cursor)?, endpoints: None, - cluster_signature: Vec::new(), + signature: Vec::new(), flags: 0, }; @@ -246,7 +250,7 @@ impl Marshalable for RootCluster { } let signature_size = buf.read_varint(cursor)?; - let _ = m.cluster_signature.write_all(buf.read_bytes(signature_size as usize, cursor)?); + let _ = m.signature.write_all(buf.read_bytes(signature_size as usize, cursor)?); m.flags = buf.read_varint(cursor)?; diff --git a/zerotier-system-service/Cargo.lock b/zerotier-system-service/Cargo.lock index b780f156d..59597d2c6 100644 --- a/zerotier-system-service/Cargo.lock +++ b/zerotier-system-service/Cargo.lock @@ -27,17 +27,6 @@ dependencies = [ "critical-section", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -122,12 +111,10 @@ version = "3.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85a35a599b11c089a7f49105658d089b8f2cf0882993c17daf6de15285c2c35d" dependencies = [ - "atty", "bitflags", "clap_lex", "indexmap", "strsim", - "termcolor", "textwrap", ] @@ -821,15 +808,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - [[package]] name = "textwrap" version = "0.15.0" @@ -960,15 +938,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/zerotier-system-service/Cargo.toml b/zerotier-system-service/Cargo.toml index cbdf03018..7120a0b90 100644 --- a/zerotier-system-service/Cargo.toml +++ b/zerotier-system-service/Cargo.toml @@ -20,8 +20,7 @@ serde = { version = "^1", features = ["derive"], default-features = false } serde_json = { version = "^1", features = ["std"], default-features = false } parking_lot = "^0" lazy_static = "^1" -clap = "^3" -#async-trait = "^0" +clap = { version = "^3", features = ["std", "suggestions"], default-features = false } [target."cfg(windows)".dependencies] winapi = { version = "^0", features = ["handleapi", "ws2ipdef", "ws2tcpip"] } diff --git a/zerotier-system-service/src/exitcode.rs b/zerotier-system-service/src/exitcode.rs new file mode 100644 index 000000000..71627f6b7 --- /dev/null +++ b/zerotier-system-service/src/exitcode.rs @@ -0,0 +1,21 @@ +/* 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/ + */ + +// These were taken from BSD sysexits.h to provide some standard. + +pub const OK: i32 = 0; +pub const ERR_USAGE: i32 = 64; +pub const ERR_DATA_FORMAT: i32 = 65; +pub const ERR_NO_INPUT: i32 = 66; +pub const ERR_SERVICE_UNAVAILABLE: i32 = 69; +pub const ERR_INTERNAL: i32 = 70; +pub const ERR_OSERR: i32 = 71; +pub const ERR_OSFILE: i32 = 72; +pub const ERR_IOERR: i32 = 74; +pub const ERR_NOPERM: i32 = 77; +pub const ERR_CONFIG: i32 = 78; diff --git a/zerotier-system-service/src/main.rs b/zerotier-system-service/src/main.rs index 43088f0cb..b9fbc93e1 100644 --- a/zerotier-system-service/src/main.rs +++ b/zerotier-system-service/src/main.rs @@ -8,20 +8,22 @@ use std::io::Write; +use clap::error::{ContextKind, ContextValue}; use clap::{Arg, ArgMatches, Command}; use zerotier_network_hypervisor::{VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION}; +pub mod exitcode; pub mod getifaddrs; pub mod localconfig; pub mod utils; pub mod vnic; -fn make_help(long_help: bool) -> String { +fn make_help() -> String { format!( r###"ZeroTier Network Hypervisor Service Version {}.{}.{} (c)2013-2022 ZeroTier, Inc. -Licensed under the Mozilla Public License (MPL) 2.0 (see LICENSE.txt) +Licensed under the Mozilla Public License (MPL) 2.0 Usage: zerotier [-...] [command args] @@ -35,7 +37,6 @@ Global Options: Common Operations: help Show this help - oldhelp Show v1.x legacy commands version Print version (of this binary) · status Show node status and configuration @@ -66,37 +67,37 @@ Common Operations: · join Join a virtual network · leave Leave a virtual network -{}"###, - VERSION_MAJOR, - VERSION_MINOR, - VERSION_REVISION, - if long_help { - r###" + Advanced Operations: - service Start local service - (usually not invoked manually) - identity [args] - new [c25519 | p384] Create identity (default: c25519) + new Create new identity getpublic Extract public part of identity fingerprint Get an identity's fingerprint validate Locally validate an identity sign <@file> Sign a file with an identity's key verify <@file> Verify a signature - · Command (or command with argument type) requires a running node. + rootset [args] +· trust <@root set> Add or update a root set +· untrust Stop using a root set +· list List root sets in use + sign Sign a root set with an identity + + service Start local service + (usually not invoked manually) + + · Command requires a running node to control. @ Argument is the path to a file containing the object. ? Argument can be either the object or a path to it (auto-detected). -"### - } else { - "" - } + +"###, + VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION, ) } -pub fn print_help(long_help: bool) { - let h = make_help(long_help); +pub fn print_help() { + let h = make_help(); let _ = std::io::stdout().write_all(h.as_bytes()); } @@ -112,10 +113,15 @@ pub fn platform_default_home_path() -> String { "/Library/Application Support/ZeroTier".into() } +#[cfg(any(target_os = "linux"))] +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()), + 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()), }; @@ -123,32 +129,32 @@ async fn async_main(cli_args: Box) -> i32 { #[allow(unused)] return match cli_args.subcommand() { Some(("help", _)) => { - print_help(false); - 0 + print_help(); + exitcode::OK } - Some(("oldhelp", _)) => todo!(), Some(("version", _)) => { println!("{}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION); - 0 + exitcode::OK } Some(("status", _)) => todo!(), - Some(("set", sub_cli_args)) => todo!(), - Some(("peer", sub_cli_args)) => todo!(), - Some(("network", sub_cli_args)) => todo!(), - Some(("join", sub_cli_args)) => todo!(), - Some(("leave", sub_cli_args)) => todo!(), + Some(("set", args)) => todo!(), + Some(("peer", args)) => todo!(), + Some(("network", args)) => todo!(), + Some(("join", args)) => todo!(), + Some(("leave", args)) => todo!(), Some(("service", _)) => todo!(), - Some(("identity", sub_cli_args)) => todo!(), + Some(("identity", args)) => todo!(), + Some(("rootset", args)) => todo!(), _ => { - print_help(false); - 1 + print_help(); + exitcode::ERR_USAGE } }; } fn main() { let cli_args = Box::new({ - let help = make_help(false); + let help = make_help(); Command::new("zerotier") .arg(Arg::new("json").short('j')) .arg(Arg::new("path").short('p').takes_value(true)) @@ -180,17 +186,56 @@ fn main() { .subcommand(Command::new("service")) .subcommand( Command::new("identity") - .subcommand(Command::new("new").arg(Arg::new("type").possible_value("p384").possible_value("c25519").default_value("c25519").index(1))) + .subcommand(Command::new("new")) .subcommand(Command::new("getpublic").arg(Arg::new("identity").index(1).required(true))) .subcommand(Command::new("fingerprint").arg(Arg::new("identity").index(1).required(true))) .subcommand(Command::new("validate").arg(Arg::new("identity").index(1).required(true))) .subcommand(Command::new("sign").arg(Arg::new("identity").index(1).required(true)).arg(Arg::new("path").index(2).required(true))) .subcommand(Command::new("verify").arg(Arg::new("identity").index(1).required(true)).arg(Arg::new("path").index(2).required(true)).arg(Arg::new("signature").index(3).required(true))), ) + .subcommand( + Command::new("rootset") + .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))), + ) .override_help(help.as_str()) - .override_usage(help.as_str()) + .override_usage("") + .disable_version_flag(true) + .disable_help_subcommand(false) .disable_help_flag(true) - .get_matches_from(std::env::args()) + .try_get_matches_from(std::env::args()) + .unwrap_or_else(|e| { + if e.kind() == clap::ErrorKind::DisplayHelp { + print_help(); + std::process::exit(exitcode::OK); + } else { + let mut invalid = String::default(); + let mut suggested = String::default(); + for c in e.context() { + match c { + (ContextKind::SuggestedSubcommand | ContextKind::SuggestedArg, ContextValue::String(name)) => { + suggested = name.clone(); + } + (ContextKind::InvalidArg | ContextKind::InvalidSubcommand, ContextValue::String(name)) => { + invalid = name.clone(); + } + _ => {} + } + } + if invalid.is_empty() { + println!("Invalid command line. Use 'help' for help."); + } else { + if suggested.is_empty() { + println!("Unrecognized option '{}'. Use 'help' for help.", invalid); + } else { + println!("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)));