A bunch of tightening up code in vl1/node, more CLI work, sketch out CLI for creating root sets.

This commit is contained in:
Adam Ierymenko 2022-05-06 11:44:41 -04:00
parent 3f6ce29f22
commit 083e2bc666
No known key found for this signature in database
GPG key ID: C8877CF2D7A5D7F3
15 changed files with 274 additions and 192 deletions

View file

@ -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"

View file

@ -112,6 +112,12 @@ impl<const L: usize> Buffer<L> {
&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()

View file

@ -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"))
}
}

View file

@ -220,6 +220,7 @@ impl Marshalable for Endpoint {
}
}
}
impl Hash for Endpoint {
fn hash<H: Hasher>(&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);

View file

@ -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};

View file

@ -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<ROOT_HELLO_INTERVAL>,
}
struct RootInfo {
roots: HashMap<Arc<Peer>, Vec<Endpoint>>,
sets: HashMap<String, RootSet>,
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<BackgroundTaskIntervals>,
/// Canonicalized network paths, held as Weak<> to be automatically cleaned when no longer in use.
paths: DashMap<u128, Weak<Path>>,
paths: DashMap<(u64, u64), Weak<Path>>,
/// Peers with which we are currently communicating.
peers: DashMap<Address, Arc<Peer>>,
/// This node's trusted roots, sorted in ascending order of quality/preference, and cluster definitions.
roots: Mutex<(Vec<Arc<Peer>>, Vec<RootCluster>)>,
roots: Mutex<RootInfo>,
/// Current best root.
best_root: RwLock<Option<Arc<Peer>>>,
@ -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<Peer>> = 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<NonZeroI64>, local_interface: Option<NonZeroI64>) -> Arc<Path> {
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<NonZeroI64>, local_interface: Option<NonZeroI64>) -> Arc<Path> {
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 {}

View file

@ -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<NonZeroI64>, local_interface: Option<NonZeroI64>) -> u128 {
pub(crate) fn local_lookup_key(endpoint: &Endpoint, local_socket: Option<NonZeroI64>, local_interface: Option<NonZeroI64>) -> (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::<u128>());
unsafe { std::mem::transmute(h.finish128()) }
h.finish128()
}
pub fn new(endpoint: Endpoint, local_socket: Option<NonZeroI64>, local_interface: Option<NonZeroI64>) -> Self {

View file

@ -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<H: Hasher>(&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;

View file

@ -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<PACKET_SIZE_MAX>) -> 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;
}
}

View file

@ -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<BTreeSet<Endpoint>>,
/// Signature of entire cluster by this identity.
pub cluster_signature: Vec<u8>,
/// Signature of entire root set by this identity.
#[serde(default)]
pub signature: Vec<u8>,
/// 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<Root>,
}
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)?;

View file

@ -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"

View file

@ -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"] }

View file

@ -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;

View file

@ -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> [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 <network> Join a virtual network
· leave <network> 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 <command> [args]
new [c25519 | p384] Create identity (default: c25519)
new Create new identity
getpublic <?identity> Extract public part of identity
fingerprint <?identity> Get an identity's fingerprint
validate <?identity> Locally validate an identity
sign <?identity> <@file> Sign a file with an identity's key
verify <?identity> <@file> <sig> Verify a signature
· Command (or command with argument type) requires a running node.
rootset <command> [args]
· trust <@root set> Add or update a root set
· untrust <root set name> Stop using a root set
· list List root sets in use
sign <path> <?identity secret> 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<ArgMatches>) -> 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<ArgMatches>) -> 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)));