A bunch of reorg to partition off V1 stuff for future convenience, and more controller work.

This commit is contained in:
Adam Ierymenko 2022-10-26 14:00:07 -04:00
parent 0239991c0d
commit 8a50427833
No known key found for this signature in database
GPG key ID: C8877CF2D7A5D7F3
18 changed files with 273 additions and 220 deletions

View file

@ -18,7 +18,3 @@ serde_json = { version = "^1", features = ["std"], default-features = false }
serde_yaml = "^0" serde_yaml = "^0"
clap = { version = "^3", features = ["std", "suggestions"], default-features = false } clap = { version = "^3", features = ["std", "suggestions"], default-features = false }
notify = { version = "^5", features = ["macos_fsevent"], default-features = false } notify = { version = "^5", features = ["macos_fsevent"], default-features = false }
[target."cfg(not(windows))".dependencies]
signal-hook = "^0"
libc = "^0"

View file

@ -1,5 +1,6 @@
// (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md. // (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md.
use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use std::sync::{Arc, Mutex, RwLock, Weak}; use std::sync::{Arc, Mutex, RwLock, Weak};
@ -8,7 +9,9 @@ use tokio::time::{Duration, Instant};
use zerotier_network_hypervisor::protocol; use zerotier_network_hypervisor::protocol;
use zerotier_network_hypervisor::protocol::{PacketBuffer, DEFAULT_MULTICAST_LIMIT, ZEROTIER_VIRTUAL_NETWORK_DEFAULT_MTU}; use zerotier_network_hypervisor::protocol::{PacketBuffer, DEFAULT_MULTICAST_LIMIT, ZEROTIER_VIRTUAL_NETWORK_DEFAULT_MTU};
use zerotier_network_hypervisor::vl1::{HostSystem, Identity, InnerProtocol, Node, PacketHandlerResult, Path, PathFilter, Peer}; use zerotier_network_hypervisor::vl1::{HostSystem, Identity, InnerProtocol, Node, PacketHandlerResult, Path, PathFilter, Peer};
use zerotier_network_hypervisor::vl2::{CertificateOfMembership, CertificateOfOwnership, NetworkConfig, NetworkId, Tag}; use zerotier_network_hypervisor::vl2;
use zerotier_network_hypervisor::vl2::networkconfig::*;
use zerotier_network_hypervisor::vl2::NetworkId;
use zerotier_utils::blob::Blob; use zerotier_utils::blob::Blob;
use zerotier_utils::buffer::OutOfBoundsError; use zerotier_utils::buffer::OutOfBoundsError;
use zerotier_utils::dictionary::Dictionary; use zerotier_utils::dictionary::Dictionary;
@ -37,6 +40,8 @@ pub struct Controller {
impl Controller { impl Controller {
/// Start an inner protocol handler answer ZeroTier VL2 network controller queries. /// Start an inner protocol handler answer ZeroTier VL2 network controller queries.
///
/// The start() method must be called once the service this will run within is also created.
pub async fn new(database: Arc<dyn Database>, runtime: tokio::runtime::Handle) -> Result<Arc<Self>, Box<dyn Error>> { pub async fn new(database: Arc<dyn Database>, runtime: tokio::runtime::Handle) -> Result<Arc<Self>, Box<dyn Error>> {
if let Some(local_identity) = database.load_node_identity() { if let Some(local_identity) = database.load_node_identity() {
assert!(local_identity.secret.is_some()); assert!(local_identity.secret.is_some());
@ -56,13 +61,13 @@ impl Controller {
} }
} }
/// Set the service and HostSystem implementation for this controller. /// Set the service and HostSystem implementation for this controller and start daemons.
/// ///
/// This must be called once the service that uses this handler is up or the controller /// This must be called once the service that uses this handler is up or the controller
/// won't actually do anything. The reference the handler holds is weak to prevent /// won't actually do anything. The reference the handler holds is weak to prevent
/// a circular reference, so if the VL1Service is dropped this must be called again to /// a circular reference, so if the VL1Service is dropped this must be called again to
/// tell the controller handler about a new instance. /// tell the controller handler about a new instance.
pub async fn set_service(&self, service: &Arc<VL1Service<dyn Database, Self, Self>>) { pub async fn start(&self, service: &Arc<VL1Service<dyn Database, Self, Self>>) {
*self.service.write().unwrap() = Arc::downgrade(service); *self.service.write().unwrap() = Arc::downgrade(service);
if let Some(cw) = self.database.changes().await.map(|mut ch| { if let Some(cw) = self.database.changes().await.map(|mut ch| {
@ -137,10 +142,24 @@ impl Controller {
} }
/// Called when the DB informs us of a change. /// Called when the DB informs us of a change.
async fn handle_change_notification(self: Arc<Self>, _change: Change) {} async fn handle_change_notification(self: Arc<Self>, change: Change) {
match change {
Change::MemberAuthorized(_, _) => {}
Change::MemberDeauthorized(network_id, node_id) => {
if let Ok(Some(member)) = self.database.get_member(network_id, node_id).await {
if !member.authorized() {
// TODO
}
}
}
}
}
/// Attempt to create a network configuration and return the result. /// Attempt to create a network configuration and return the result.
/// ///
/// This is the central function of the controller that looks up members, checks their
/// permissions, and generates a network config and other credentials (or not).
///
/// An error is only returned if a database or other unusual error occurs. Otherwise a rejection /// An error is only returned if a database or other unusual error occurs. Otherwise a rejection
/// reason is returned with None or an acceptance reason with a network configuration is returned. /// reason is returned with None or an acceptance reason with a network configuration is returned.
async fn get_network_config( async fn get_network_config(
@ -151,27 +170,37 @@ impl Controller {
) -> Result<(AuthorizationResult, Option<NetworkConfig>), Box<dyn Error + Send + Sync>> { ) -> Result<(AuthorizationResult, Option<NetworkConfig>), Box<dyn Error + Send + Sync>> {
let network = self.database.get_network(network_id).await?; let network = self.database.get_network(network_id).await?;
if network.is_none() { if network.is_none() {
// TODO: send error
return Ok((AuthorizationResult::Rejected, None)); return Ok((AuthorizationResult::Rejected, None));
} }
let network = network.unwrap(); let network = network.unwrap();
let mut member = self.database.get_member(network_id, source_identity.address).await?; let mut member = self.database.get_member(network_id, source_identity.address).await?;
let mut member_changed = false; let mut member_changed = false;
let legacy_v1 = source_identity.p384.is_none();
// If we have a member object and a pinned identity, check to make sure it matches. // If we have a member object and a pinned identity, check to make sure it matches. Also accept
if let Some(member) = member.as_ref() { // upgraded identities to replace old versions if they are properly formed and inherit.
if let Some(member) = member.as_mut() {
if let Some(pinned_identity) = member.identity.as_ref() { if let Some(pinned_identity) = member.identity.as_ref() {
if !pinned_identity.eq(&source_identity) { if !pinned_identity.eq(&source_identity) {
return Ok((AuthorizationResult::RejectedIdentityMismatch, None)); return Ok((AuthorizationResult::RejectedIdentityMismatch, None));
} else if source_identity.is_upgraded_from(pinned_identity) {
let _ = member.identity.replace(source_identity.clone_without_secret());
member_changed = true;
} }
} }
} }
// This is the final verdict after everything has been checked.
let mut authorization_result = AuthorizationResult::Rejected; let mut authorization_result = AuthorizationResult::Rejected;
let mut authorized = member.as_ref().map_or(false, |m| m.authorized());
if !authorized { // This is the main "authorized" flag on the member record. If it is true then
// the member is allowed, but with the caveat that SSO must be checked if it's
// enabled on the network. If this is false then the member is rejected unless
// authorized by a token or unless it's a public network.
let mut member_authorized = member.as_ref().map_or(false, |m| m.authorized());
// If the member isn't authorized, check to see if it should be auto-authorized.
if !member_authorized {
if member.is_none() { if member.is_none() {
if network.learn_members.unwrap_or(true) { if network.learn_members.unwrap_or(true) {
let _ = member.insert(Member::new_with_identity(source_identity.clone(), network_id)); let _ = member.insert(Member::new_with_identity(source_identity.clone(), network_id));
@ -181,33 +210,38 @@ impl Controller {
} }
} }
if !network.private { if network.private {
// TODO: check token authorization
} else {
authorization_result = AuthorizationResult::ApprovedOnPublicNetwork; authorization_result = AuthorizationResult::ApprovedOnPublicNetwork;
authorized = true;
member.as_mut().unwrap().last_authorized_time = Some(now); member.as_mut().unwrap().last_authorized_time = Some(now);
member_authorized = true;
member_changed = true; member_changed = true;
} }
} }
let mut member = member.unwrap(); let mut member = member.unwrap();
let nc: Option<NetworkConfig> = if authorized { // If the member is authorized set the final verdict to reflect this unless SSO (third party auth)
// ==================================================================================== // is enabled on the network and disagrees. Skip if the verdict is already one of the approved
// Authorized requests are handled here // values, which would indicate auth-authorization above.
// ==================================================================================== if member_authorized {
if !authorization_result.approved() {
// TODO: check SSO if enabled on network!
authorization_result = AuthorizationResult::Approved;
}
} else {
// This should not be able to be in approved state if member_authorized is still false.
assert!(!authorization_result.approved());
}
// TODO: check SSO let nc: Option<NetworkConfig> = if authorization_result.approved() {
// We should not be able to make it here if this is still false.
assert!(member_authorized);
// Figure out time bounds for the certificate to generate. // Figure out TTL for credentials (time window in V1).
let credential_ttl = network.credential_ttl.unwrap_or(CREDENTIAL_WINDOW_SIZE_DEFAULT); let credential_ttl = network.credential_ttl.unwrap_or(CREDENTIAL_WINDOW_SIZE_DEFAULT);
// Get a list of all network members that were deauthorized but are still within the time window.
// These will be issued revocations to remind the node not to speak to them until they fall off.
let deauthed_members_still_in_window = self
.database
.list_members_deauthorized_after(network.id, now - credential_ttl)
.await;
// Check and if necessary auto-assign static IPs for this member. // Check and if necessary auto-assign static IPs for this member.
member_changed |= network.check_zt_ip_assignments(self.database.as_ref(), &mut member).await; member_changed |= network.check_zt_ip_assignments(self.database.as_ref(), &mut member).await;
@ -225,35 +259,52 @@ impl Controller {
nc.rules = network.rules; nc.rules = network.rules;
nc.dns = network.dns; nc.dns = network.dns;
nc.certificate_of_membership = if network.min_supported_version.unwrap_or(0) < (protocol::PROTOCOL_VERSION_V2 as u32) {
CertificateOfMembership::new(&self.local_identity, network_id, &source_identity, now, credential_ttl, legacy_v1); // Get a list of all network members that were deauthorized but are still within the time window.
if nc.certificate_of_membership.is_none() { // These will be issued revocations to remind the node not to speak to them until they fall off.
return Ok((AuthorizationResult::RejectedDueToError, None)); let deauthed_members_still_in_window = self
} .database
.list_members_deauthorized_after(network.id, now - credential_ttl)
.await;
let mut coo = CertificateOfOwnership::new(network_id, now, source_identity.address, legacy_v1); if let Some(com) =
for ip in nc.static_ips.iter() { vl2::v1::CertificateOfMembership::new(&self.local_identity, network_id, &source_identity, now, credential_ttl)
coo.add_ip(ip); {
} let mut v1cred = V1Credentials {
if !coo.sign(&self.local_identity, &source_identity) { certificate_of_membership: com,
return Ok((AuthorizationResult::RejectedDueToError, None)); certificates_of_ownership: Vec::new(),
} tags: HashMap::new(),
nc.certificates_of_ownership.push(coo); };
for (id, value) in member.tags.iter() { let mut coo = vl2::v1::CertificateOfOwnership::new(network_id, now, source_identity.address);
let tag = Tag::new(*id, *value, &self.local_identity, network_id, &source_identity, now, legacy_v1); for ip in nc.static_ips.iter() {
if tag.is_none() { coo.add_ip(ip);
}
if !coo.sign(&self.local_identity, &source_identity) {
return Ok((AuthorizationResult::RejectedDueToError, None));
}
v1cred.certificates_of_ownership.push(coo);
for (id, value) in member.tags.iter() {
let tag = vl2::v1::Tag::new(*id, *value, &self.local_identity, network_id, &source_identity, now);
if tag.is_none() {
return Ok((AuthorizationResult::RejectedDueToError, None));
}
let _ = v1cred.tags.insert(*id, tag.unwrap());
}
nc.v1_credentials = Some(v1cred);
} else {
return Ok((AuthorizationResult::RejectedDueToError, None)); return Ok((AuthorizationResult::RejectedDueToError, None));
} }
let _ = nc.tags.insert(*id, tag.unwrap()); } else {
// TODO: create V2 type credential for V2-only networks
// TODO: populate node info for V2 networks
} }
// TODO: node info, which isn't supported in v1 so not needed yet
// TODO: revocations! // TODO: revocations!
Some(nc) Some(nc)
// ====================================================================================
} else { } else {
None None
}; };
@ -409,6 +460,7 @@ impl InnerProtocol for Controller {
} }
fn should_respond_to(&self, _: &Identity) -> bool { fn should_respond_to(&self, _: &Identity) -> bool {
// Controllers respond to anyone.
true true
} }
} }

View file

@ -11,7 +11,6 @@ use crate::model::*;
/// Database change relevant to the controller and that was NOT initiated by the controller. /// Database change relevant to the controller and that was NOT initiated by the controller.
#[derive(Clone)] #[derive(Clone)]
pub enum Change { pub enum Change {
NetworkDeleted(NetworkId),
MemberAuthorized(NetworkId, Address), MemberAuthorized(NetworkId, Address),
MemberDeauthorized(NetworkId, Address), MemberDeauthorized(NetworkId, Address),
} }

View file

@ -86,13 +86,13 @@ impl FileDatabase {
} }
fn network_path(&self, network_id: NetworkId) -> PathBuf { fn network_path(&self, network_id: NetworkId) -> PathBuf {
self.base_path.join(format!("n{:06x}", network_id.network_no())).join("config.yaml") self.base_path.join(format!("N{:06x}", network_id.network_no())).join("config.yaml")
} }
fn member_path(&self, network_id: NetworkId, member_id: Address) -> PathBuf { fn member_path(&self, network_id: NetworkId, member_id: Address) -> PathBuf {
self.base_path self.base_path
.join(format!("n{:06x}", network_id.network_no())) .join(format!("N{:06x}", network_id.network_no()))
.join(format!("m{}.yaml", member_id.to_string())) .join(format!("M{}.yaml", member_id.to_string()))
} }
} }
@ -154,7 +154,7 @@ impl Database for FileDatabase {
let osname = ent.file_name(); let osname = ent.file_name();
let name = osname.to_string_lossy(); let name = osname.to_string_lossy();
if name.len() == (zerotier_network_hypervisor::protocol::ADDRESS_SIZE_STRING + 6) if name.len() == (zerotier_network_hypervisor::protocol::ADDRESS_SIZE_STRING + 6)
&& name.starts_with("m") && name.starts_with("M")
&& name.ends_with(".yaml") && name.ends_with(".yaml")
{ {
if let Ok(member_address) = u64::from_str_radix(&name[1..11], 16) { if let Ok(member_address) = u64::from_str_radix(&name[1..11], 16) {

View file

@ -1,8 +1,6 @@
// (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md. // (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md.
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use clap::{Arg, Command}; use clap::{Arg, Command};
@ -34,27 +32,9 @@ async fn run(database: Arc<dyn Database>, runtime: &Runtime) -> i32 {
let svc = svc.unwrap(); let svc = svc.unwrap();
svc.node().init_default_roots(); svc.node().init_default_roots();
handler.set_service(&svc).await; handler.start(&svc).await;
// Wait for kill signal on Unix-like platforms.
#[cfg(unix)]
{
let term = Arc::new(AtomicBool::new(false));
let _ = signal_hook::flag::register(libc::SIGINT, term.clone());
let _ = signal_hook::flag::register(libc::SIGTERM, term.clone());
let _ = signal_hook::flag::register(libc::SIGQUIT, term.clone());
while !term.load(Ordering::Relaxed) {
std::thread::sleep(Duration::from_secs(1));
}
}
#[cfg(windows)]
{
// TODO: if anyone wants to use this on Windows you'll need to make it a service or wait
// for a stop signal or soemthing here.
todo!();
}
zerotier_utils::wait_for_process_abort();
println!("Terminate signal received, shutting down..."); println!("Terminate signal received, shutting down...");
exitcode::OK exitcode::OK
} else { } else {
@ -84,7 +64,7 @@ fn main() {
.takes_value(true) .takes_value(true)
.forbid_empty_values(true) .forbid_empty_values(true)
.value_name("path") .value_name("path")
.help(Some("Connect to postgres with parameters in JSON file")) .help(Some("Connect to postgres with parameters in YAML file"))
.required_unless_present_any(&REQUIRE_ONE_OF_ARGS), .required_unless_present_any(&REQUIRE_ONE_OF_ARGS),
) )
.version(format!("{}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION).as_str()) .version(format!("{}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION).as_str())

View file

@ -11,7 +11,8 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use zerotier_network_hypervisor::vl1::{Address, Endpoint}; use zerotier_network_hypervisor::vl1::{Address, Endpoint};
use zerotier_network_hypervisor::vl2::{NetworkConfig, NetworkId}; use zerotier_network_hypervisor::vl2::networkconfig::NetworkConfig;
use zerotier_network_hypervisor::vl2::NetworkId;
use zerotier_utils::blob::Blob; use zerotier_utils::blob::Blob;
/// A complete network with all member configuration information for import/export or blob storage. /// A complete network with all member configuration information for import/export or blob storage.
@ -80,6 +81,14 @@ impl AuthorizationResult {
Self::ApprovedOnPublicNetwork => "ap", Self::ApprovedOnPublicNetwork => "ap",
} }
} }
/// Returns true if this result is one of the 'approved' result types.
pub fn approved(&self) -> bool {
match self {
Self::Approved | Self::ApprovedViaSSO | Self::ApprovedViaToken | Self::ApprovedOnPublicNetwork => true,
_ => false,
}
}
} }
impl ToString for AuthorizationResult { impl ToString for AuthorizationResult {

View file

@ -7,7 +7,8 @@ use serde::{Deserialize, Serialize};
use zerotier_network_hypervisor::vl1::InetAddress; use zerotier_network_hypervisor::vl1::InetAddress;
use zerotier_network_hypervisor::vl2::networkconfig::IpRoute; use zerotier_network_hypervisor::vl2::networkconfig::IpRoute;
use zerotier_network_hypervisor::vl2::{NetworkId, Rule}; use zerotier_network_hypervisor::vl2::rule::Rule;
use zerotier_network_hypervisor::vl2::NetworkId;
use crate::database::Database; use crate::database::Database;
use crate::model::{Member, ObjectType}; use crate::model::{Member, ObjectType};
@ -102,6 +103,11 @@ pub struct Network {
#[serde(rename = "credentialTtl")] #[serde(rename = "credentialTtl")]
pub credential_ttl: Option<i64>, pub credential_ttl: Option<i64>,
/// Minimum supported ZeroTier protocol version for this network (default: undefined, up to members)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "minSupportedVersion")]
pub min_supported_version: Option<u32>,
/// MTU inside the virtual network, default of 2800 is used if not set. /// MTU inside the virtual network, default of 2800 is used if not set.
pub mtu: Option<u16>, pub mtu: Option<u16>,

View file

@ -50,6 +50,9 @@ pub const PROTOCOL_VERSION: u8 = 20;
/// We could probably push it back to 8 or 9 with some added support for sending Salsa/Poly packets. /// We could probably push it back to 8 or 9 with some added support for sending Salsa/Poly packets.
pub const PROTOCOL_VERSION_MIN: u8 = 11; pub const PROTOCOL_VERSION_MIN: u8 = 11;
/// V2 is this protocol version or higher.
pub const PROTOCOL_VERSION_V2: u8 = 20;
/// Size of a pooled packet buffer. /// Size of a pooled packet buffer.
pub const PACKET_BUFFER_SIZE: usize = 16384; pub const PACKET_BUFFER_SIZE: usize = 16384;
@ -565,6 +568,10 @@ pub(crate) const PEER_EXPIRATION_TIME: i64 = (PEER_HELLO_INTERVAL_MAX * 2) + 100
/// Proof of work difficulty (threshold) for identity generation. /// Proof of work difficulty (threshold) for identity generation.
pub(crate) const IDENTITY_POW_THRESHOLD: u8 = 17; pub(crate) const IDENTITY_POW_THRESHOLD: u8 = 17;
/// Maximum number of key/value pairs in a single Tag credential.
/// (This is for V2 only. In V1 tag credentials can have only one pair.)
pub(crate) const MAX_TAG_KEY_VALUE_PAIRS: usize = 128;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::mem::size_of; use std::mem::size_of;

View file

@ -157,6 +157,7 @@ impl Identity {
+ P384_ECDSA_SIGNATURE_SIZE; + P384_ECDSA_SIGNATURE_SIZE;
pub const FINGERPRINT_SIZE: usize = IDENTITY_FINGERPRINT_SIZE; pub const FINGERPRINT_SIZE: usize = IDENTITY_FINGERPRINT_SIZE;
pub const MAX_SIGNATURE_SIZE: usize = IDENTITY_MAX_SIGNATURE_SIZE;
const ALGORITHM_X25519: u8 = 0x01; const ALGORITHM_X25519: u8 = 0x01;
const ALGORITHM_EC_NIST_P384: u8 = 0x02; const ALGORITHM_EC_NIST_P384: u8 = 0x02;
@ -320,6 +321,15 @@ impl Identity {
return digest[0] < IDENTITY_POW_THRESHOLD && Address::from_bytes(&digest[59..64]).map_or(false, |a| a == self.address); return digest[0] < IDENTITY_POW_THRESHOLD && Address::from_bytes(&digest[59..64]).map_or(false, |a| a == self.address);
} }
/// Returns true if this identity was upgraded from another older version.
pub fn is_upgraded_from(&self, other: &Identity) -> bool {
self.address == other.address
&& self.x25519 == other.x25519
&& self.ed25519 == other.ed25519
&& self.p384.is_some()
&& other.p384.is_none()
}
/// Perform ECDH key agreement, returning a shared secret or None on error. /// Perform ECDH key agreement, returning a shared secret or None on error.
/// ///
/// An error can occur if this identity does not hold its secret portion or if either key is invalid. /// An error can occur if this identity does not hold its secret portion or if either key is invalid.

View file

@ -1,20 +1,13 @@
// (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md. // (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md.
mod certificateofmembership;
mod certificateofownership;
mod multicastgroup; mod multicastgroup;
mod networkid; mod networkid;
mod rule;
mod switch; mod switch;
mod tag;
pub mod networkconfig; pub mod networkconfig;
pub mod rule;
pub mod v1;
pub use certificateofmembership::CertificateOfMembership;
pub use certificateofownership::CertificateOfOwnership;
pub use multicastgroup::MulticastGroup; pub use multicastgroup::MulticastGroup;
pub use networkconfig::NetworkConfig;
pub use networkid::NetworkId; pub use networkid::NetworkId;
pub use rule::Rule;
pub use switch::{Switch, SwitchInterface}; pub use switch::{Switch, SwitchInterface};
pub use tag::Tag;

View file

@ -7,10 +7,8 @@ use std::str::FromStr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::vl1::{Address, Identity, InetAddress}; use crate::vl1::{Address, Identity, InetAddress};
use crate::vl2::certificateofmembership::CertificateOfMembership;
use crate::vl2::certificateofownership::CertificateOfOwnership;
use crate::vl2::rule::Rule; use crate::vl2::rule::Rule;
use crate::vl2::tag::Tag; use crate::vl2::v1::{CertificateOfMembership, CertificateOfOwnership, Tag};
use crate::vl2::NetworkId; use crate::vl2::NetworkId;
use zerotier_utils::buffer::Buffer; use zerotier_utils::buffer::Buffer;
@ -18,6 +16,19 @@ use zerotier_utils::dictionary::Dictionary;
use zerotier_utils::error::InvalidParameterError; use zerotier_utils::error::InvalidParameterError;
use zerotier_utils::marshalable::Marshalable; use zerotier_utils::marshalable::Marshalable;
/// Credentials that must be sent to V1 nodes to allow access.
///
/// These are also handed out to V2 nodes to use when communicating with V1 nodes on
/// networks that support older protocol versions.
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct V1Credentials {
pub certificate_of_membership: CertificateOfMembership,
pub certificates_of_ownership: Vec<CertificateOfOwnership>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
#[serde(default)]
pub tags: HashMap<u32, Tag>,
}
/// Network configuration object sent to nodes by network controllers. /// Network configuration object sent to nodes by network controllers.
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NetworkConfig { pub struct NetworkConfig {
@ -51,13 +62,9 @@ pub struct NetworkConfig {
#[serde(default)] #[serde(default)]
pub dns: HashMap<String, HashSet<InetAddress>>, pub dns: HashMap<String, HashSet<InetAddress>>,
pub certificate_of_membership: Option<CertificateOfMembership>, // considered invalid if None #[serde(skip_serializing_if = "Option::is_none")]
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)] #[serde(default)]
pub certificates_of_ownership: Vec<CertificateOfOwnership>, pub v1_credentials: Option<V1Credentials>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
#[serde(default)]
pub tags: HashMap<u32, Tag>,
#[serde(skip_serializing_if = "HashSet::is_empty")] #[serde(skip_serializing_if = "HashSet::is_empty")]
#[serde(default)] #[serde(default)]
@ -91,9 +98,7 @@ impl NetworkConfig {
static_ips: HashSet::new(), static_ips: HashSet::new(),
rules: Vec::new(), rules: Vec::new(),
dns: HashMap::new(), dns: HashMap::new(),
certificate_of_membership: None, v1_credentials: None,
certificates_of_ownership: Vec::new(),
tags: HashMap::new(),
banned: HashSet::new(), banned: HashSet::new(),
node_info: HashMap::new(), node_info: HashMap::new(),
central_url: String::new(), central_url: String::new(),
@ -171,25 +176,27 @@ impl NetworkConfig {
d.set_bytes(proto_v1_field_name::network_config::DNS, dns_bin); d.set_bytes(proto_v1_field_name::network_config::DNS, dns_bin);
} }
d.set_bytes( if let Some(v1cred) = self.v1_credentials.as_ref() {
proto_v1_field_name::network_config::CERTIFICATE_OF_MEMBERSHIP, d.set_bytes(
self.certificate_of_membership.as_ref()?.to_bytes()?, proto_v1_field_name::network_config::CERTIFICATE_OF_MEMBERSHIP,
); v1cred.certificate_of_membership.to_bytes()?,
);
if !self.certificates_of_ownership.is_empty() { if !v1cred.certificates_of_ownership.is_empty() {
let mut certs = Vec::with_capacity(self.certificates_of_ownership.len() * 256); let mut certs = Vec::with_capacity(v1cred.certificates_of_ownership.len() * 256);
for c in self.certificates_of_ownership.iter() { for c in v1cred.certificates_of_ownership.iter() {
let _ = certs.write_all(c.v1_proto_to_bytes(controller_identity.address)?.as_slice()); let _ = certs.write_all(c.to_bytes(controller_identity.address)?.as_slice());
}
d.set_bytes(proto_v1_field_name::network_config::CERTIFICATES_OF_OWNERSHIP, certs);
} }
d.set_bytes(proto_v1_field_name::network_config::CERTIFICATES_OF_OWNERSHIP, certs);
}
if !self.tags.is_empty() { if !v1cred.tags.is_empty() {
let mut certs = Vec::with_capacity(self.tags.len() * 256); let mut certs = Vec::with_capacity(v1cred.tags.len() * 256);
for (_, t) in self.tags.iter() { for (_, t) in v1cred.tags.iter() {
let _ = certs.write_all(t.v1_proto_to_bytes(controller_identity.address)?.as_slice()); let _ = certs.write_all(t.to_bytes(controller_identity.address)?.as_slice());
}
d.set_bytes(proto_v1_field_name::network_config::TAGS, certs);
} }
d.set_bytes(proto_v1_field_name::network_config::TAGS, certs);
} }
// node_info is not supported by V1 nodes // node_info is not supported by V1 nodes
@ -299,27 +306,33 @@ impl NetworkConfig {
} }
} }
nc.certificate_of_membership = Some(CertificateOfMembership::v1_proto_from_bytes( let mut v1cred = V1Credentials {
d.get_bytes(proto_v1_field_name::network_config::CERTIFICATE_OF_MEMBERSHIP) certificate_of_membership: CertificateOfMembership::from_bytes(
.ok_or(InvalidParameterError("missing certificate of membership"))?, d.get_bytes(proto_v1_field_name::network_config::CERTIFICATE_OF_MEMBERSHIP)
)?); .ok_or(InvalidParameterError("missing certificate of membership"))?,
)?,
certificates_of_ownership: Vec::new(),
tags: HashMap::new(),
};
if let Some(mut coo_bin) = d.get_bytes(proto_v1_field_name::network_config::CERTIFICATES_OF_OWNERSHIP) { if let Some(mut coo_bin) = d.get_bytes(proto_v1_field_name::network_config::CERTIFICATES_OF_OWNERSHIP) {
while !coo_bin.is_empty() { while !coo_bin.is_empty() {
let c = CertificateOfOwnership::v1_proto_from_bytes(coo_bin)?; let c = CertificateOfOwnership::from_bytes(coo_bin)?;
nc.certificates_of_ownership.push(c.0); v1cred.certificates_of_ownership.push(c.0);
coo_bin = c.1; coo_bin = c.1;
} }
} }
if let Some(mut tag_bin) = d.get_bytes(proto_v1_field_name::network_config::TAGS) { if let Some(mut tag_bin) = d.get_bytes(proto_v1_field_name::network_config::TAGS) {
while !tag_bin.is_empty() { while !tag_bin.is_empty() {
let t = Tag::v1_proto_from_bytes(tag_bin)?; let t = Tag::from_bytes(tag_bin)?;
let _ = nc.tags.insert(t.0.id, t.0); let _ = v1cred.tags.insert(t.0.id, t.0);
tag_bin = t.1; tag_bin = t.1;
} }
} }
nc.v1_credentials = Some(v1cred);
if let Some(central_url) = d.get_str(proto_v1_field_name::network_config::CENTRAL_URL) { if let Some(central_url) = d.get_str(proto_v1_field_name::network_config::CENTRAL_URL) {
nc.central_url = central_url.to_string(); nc.central_url = central_url.to_string();
} }

View file

@ -1,6 +1,5 @@
use std::io::Write; use std::io::Write;
use crate::vl1::identity;
use crate::vl1::identity::Identity; use crate::vl1::identity::Identity;
use crate::vl1::Address; use crate::vl1::Address;
use crate::vl2::NetworkId; use crate::vl2::NetworkId;
@ -20,22 +19,14 @@ pub struct CertificateOfMembership {
pub timestamp: i64, pub timestamp: i64,
pub max_delta: i64, pub max_delta: i64,
pub issued_to: Address, pub issued_to: Address,
pub issued_to_fingerprint: Blob<{ identity::IDENTITY_FINGERPRINT_SIZE }>, pub issued_to_fingerprint: Blob<{ Identity::FINGERPRINT_SIZE }>,
pub signature: ArrayVec<u8, { identity::IDENTITY_MAX_SIGNATURE_SIZE }>, pub signature: ArrayVec<u8, { Identity::MAX_SIGNATURE_SIZE }>,
pub version: u8,
} }
impl CertificateOfMembership { impl CertificateOfMembership {
/// Create a new signed certificate of membership. /// Create a new signed certificate of membership.
/// None is returned if an error occurs, such as the issuer missing its secrets. /// None is returned if an error occurs, such as the issuer missing its secrets.
pub fn new( pub fn new(issuer: &Identity, network_id: NetworkId, issued_to: &Identity, timestamp: i64, max_delta: i64) -> Option<Self> {
issuer: &Identity,
network_id: NetworkId,
issued_to: &Identity,
timestamp: i64,
max_delta: i64,
legacy_v1: bool,
) -> Option<Self> {
let mut com = CertificateOfMembership { let mut com = CertificateOfMembership {
network_id, network_id,
timestamp, timestamp,
@ -43,26 +34,18 @@ impl CertificateOfMembership {
issued_to: issued_to.address, issued_to: issued_to.address,
issued_to_fingerprint: Blob::default(), issued_to_fingerprint: Blob::default(),
signature: ArrayVec::new(), signature: ArrayVec::new(),
version: 0,
}; };
if legacy_v1 {
com.issued_to_fingerprint = Blob::from(Self::v1_proto_issued_to_fingerprint(issued_to, Some(issuer.address))); com.issued_to_fingerprint = Blob::from(Self::v1_proto_issued_to_fingerprint(issued_to, Some(issuer.address)));
com.version = 1; if let Some(signature) = issuer.sign(&com.v1_proto_get_qualifier_bytes(), true) {
if let Some(signature) = issuer.sign(&com.v1_proto_get_qualifier_bytes(), true) { com.signature = signature;
com.signature = signature; Some(com)
Some(com)
} else {
None
}
} else { } else {
com.issued_to_fingerprint = Blob::from(issued_to.fingerprint); None
com.version = 2;
todo!()
} }
} }
fn v1_proto_get_qualifier_bytes(&self) -> [u8; 168] { fn v1_proto_get_qualifier_bytes(&self) -> [u8; 168] {
assert_eq!(self.version, 1);
let mut q = [0u64; 21]; let mut q = [0u64; 21];
q[0] = 0; q[0] = 0;
@ -109,25 +92,21 @@ impl CertificateOfMembership {
/// Get this certificate of membership in byte encoded format. /// Get this certificate of membership in byte encoded format.
pub fn to_bytes(&self) -> Option<Vec<u8>> { pub fn to_bytes(&self) -> Option<Vec<u8>> {
if self.version == 1 { if self.signature.len() == 96 {
if self.signature.len() == 96 { let mut v: Vec<u8> = Vec::with_capacity(3 + 168 + 5 + 96);
let mut v: Vec<u8> = Vec::with_capacity(3 + 168 + 5 + 96); v.push(1); // version byte from v1 protocol
v.push(1); // version byte from v1 protocol v.push(0);
v.push(0); v.push(7); // 7 qualifiers, big-endian 16-bit
v.push(7); // 7 qualifiers, big-endian 16-bit let _ = v.write_all(&self.v1_proto_get_qualifier_bytes());
let _ = v.write_all(&self.v1_proto_get_qualifier_bytes()); let _ = v.write_all(&self.issued_to_fingerprint.as_bytes()[32..38]); // issuer address
let _ = v.write_all(&self.issued_to_fingerprint.as_bytes()[32..38]); // issuer address let _ = v.write_all(self.signature.as_bytes());
let _ = v.write_all(self.signature.as_bytes()); return Some(v);
return Some(v);
}
} else if self.version == 2 {
todo!()
} }
return None; return None;
} }
/// Decode a V1 legacy format certificate of membership in byte format. /// Decode a V1 legacy format certificate of membership in byte format.
pub fn v1_proto_from_bytes(mut b: &[u8]) -> Result<Self, InvalidParameterError> { pub fn from_bytes(mut b: &[u8]) -> Result<Self, InvalidParameterError> {
if b.len() <= 3 || b[0] != 1 { if b.len() <= 3 || b[0] != 1 {
return Err(InvalidParameterError("version mismatch")); return Err(InvalidParameterError("version mismatch"));
} }
@ -185,20 +164,15 @@ impl CertificateOfMembership {
s.push_slice(&b[..96]); s.push_slice(&b[..96]);
s s
}, },
version: 1,
}) })
} }
/// Verify this certificate of membership. /// Verify this certificate of membership.
pub fn verify(self, issuer: &Identity, expect_issued_to: &Identity) -> Option<Verified<Self>> { pub fn verify(self, issuer: &Identity, expect_issued_to: &Identity) -> Option<Verified<Self>> {
if self.version == 1 { if Self::v1_proto_issued_to_fingerprint(expect_issued_to, None).eq(&self.issued_to_fingerprint.as_bytes()[..32]) {
if Self::v1_proto_issued_to_fingerprint(expect_issued_to, None).eq(&self.issued_to_fingerprint.as_bytes()[..32]) { if issuer.verify(&self.v1_proto_get_qualifier_bytes(), self.signature.as_bytes()) {
if issuer.verify(&self.v1_proto_get_qualifier_bytes(), self.signature.as_bytes()) { return Some(Verified(self));
return Some(Verified(self));
}
} }
} else if self.version == 2 {
todo!()
} }
return None; return None;
} }

View file

@ -34,23 +34,17 @@ pub struct CertificateOfOwnership {
pub things: HashSet<Thing>, pub things: HashSet<Thing>,
pub issued_to: Address, pub issued_to: Address,
pub signature: ArrayVec<u8, { crate::vl1::identity::IDENTITY_MAX_SIGNATURE_SIZE }>, pub signature: ArrayVec<u8, { crate::vl1::identity::IDENTITY_MAX_SIGNATURE_SIZE }>,
pub version: u8,
} }
impl CertificateOfOwnership { impl CertificateOfOwnership {
/// Create a new empty and unsigned certificate. /// Create a new empty and unsigned certificate.
pub fn new(network_id: NetworkId, timestamp: i64, issued_to: Address, legacy_v1: bool) -> Self { pub fn new(network_id: NetworkId, timestamp: i64, issued_to: Address) -> Self {
Self { Self {
network_id, network_id,
timestamp, timestamp,
things: HashSet::with_capacity(4), things: HashSet::with_capacity(4),
issued_to, issued_to,
signature: ArrayVec::new(), signature: ArrayVec::new(),
version: if legacy_v1 {
1
} else {
2
},
} }
} }
@ -117,13 +111,13 @@ impl CertificateOfOwnership {
} }
#[inline(always)] #[inline(always)]
pub fn v1_proto_to_bytes(&self, signed_by: Address) -> Option<Vec<u8>> { pub fn to_bytes(&self, signed_by: Address) -> Option<Vec<u8>> {
self.internal_v1_proto_to_bytes(false, signed_by) self.internal_v1_proto_to_bytes(false, signed_by)
} }
/// Decode a V1 legacy format certificate of ownership in byte format. /// Decode a V1 legacy format certificate of ownership in byte format.
/// The certificate and the current position slice are returned so multiple certs can be easily read from a buffer. /// The certificate and the current position slice are returned so multiple certs can be easily read from a buffer.
pub fn v1_proto_from_bytes(mut b: &[u8]) -> Result<(Self, &[u8]), InvalidParameterError> { pub fn from_bytes(mut b: &[u8]) -> Result<(Self, &[u8]), InvalidParameterError> {
if b.len() < 30 { if b.len() < 30 {
return Err(InvalidParameterError("incomplete")); return Err(InvalidParameterError("incomplete"));
} }
@ -167,7 +161,6 @@ impl CertificateOfOwnership {
s.push_slice(&b[13..109]); s.push_slice(&b[13..109]);
s s
}, },
version: 1,
}, },
&b[END_LEN..], &b[END_LEN..],
)) ))
@ -175,16 +168,12 @@ impl CertificateOfOwnership {
/// Sign certificate of ownership for use by V1 nodes. /// Sign certificate of ownership for use by V1 nodes.
pub fn sign(&mut self, issuer: &Identity, issued_to: &Identity) -> bool { pub fn sign(&mut self, issuer: &Identity, issued_to: &Identity) -> bool {
if self.version == 1 { self.issued_to = issued_to.address;
self.issued_to = issued_to.address; if let Some(to_sign) = self.internal_v1_proto_to_bytes(true, issuer.address) {
if let Some(to_sign) = self.internal_v1_proto_to_bytes(true, issuer.address) { if let Some(signature) = issuer.sign(&to_sign.as_slice(), true) {
if let Some(signature) = issuer.sign(&to_sign.as_slice(), true) { self.signature = signature;
self.signature = signature; return true;
return true;
}
} }
} else if self.version == 2 {
todo!()
} }
return false; return false;
} }

View file

@ -0,0 +1,9 @@
mod certificateofmembership;
mod certificateofownership;
mod revocation;
mod tag;
pub use certificateofmembership::CertificateOfMembership;
pub use certificateofownership::{CertificateOfOwnership, Thing};
pub use revocation::Revocation;
pub use tag::Tag;

View file

@ -0,0 +1,15 @@
use zerotier_utils::arrayvec::ArrayVec;
use serde::{Deserialize, Serialize};
use crate::vl1::{Address, Identity};
use crate::vl2::NetworkId;
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Revocation {
pub network_id: NetworkId,
pub threshold: i64,
pub issued_to: Address,
pub signature: ArrayVec<u8, { Identity::MAX_SIGNATURE_SIZE }>,
pub version: u8,
}

View file

@ -1,6 +1,5 @@
use std::io::Write; use std::io::Write;
use crate::vl1::identity;
use crate::vl1::identity::Identity; use crate::vl1::identity::Identity;
use crate::vl1::Address; use crate::vl1::Address;
use crate::vl2::NetworkId; use crate::vl2::NetworkId;
@ -17,20 +16,11 @@ pub struct Tag {
pub issued_to: Address, pub issued_to: Address,
pub id: u32, pub id: u32,
pub value: u32, pub value: u32,
pub signature: ArrayVec<u8, { identity::IDENTITY_MAX_SIGNATURE_SIZE }>, pub signature: ArrayVec<u8, { Identity::MAX_SIGNATURE_SIZE }>,
pub version: u8,
} }
impl Tag { impl Tag {
pub fn new( pub fn new(id: u32, value: u32, issuer: &Identity, network_id: NetworkId, issued_to: &Identity, timestamp: i64) -> Option<Self> {
id: u32,
value: u32,
issuer: &Identity,
network_id: NetworkId,
issued_to: &Identity,
timestamp: i64,
legacy_v1: bool,
) -> Option<Self> {
let mut tag = Self { let mut tag = Self {
network_id, network_id,
timestamp, timestamp,
@ -38,26 +28,17 @@ impl Tag {
id, id,
value, value,
signature: ArrayVec::new(), signature: ArrayVec::new(),
version: if legacy_v1 {
1
} else {
2
},
}; };
if legacy_v1 { let to_sign = tag.internal_v1_proto_to_bytes(true, issuer.address)?;
let to_sign = tag.internal_v1_proto_to_bytes(true, issuer.address)?; if let Some(signature) = issuer.sign(to_sign.as_slice(), true) {
if let Some(signature) = issuer.sign(to_sign.as_slice(), true) { tag.signature = signature;
tag.signature = signature; return Some(tag);
return Some(tag);
}
} else {
todo!()
} }
return None; return None;
} }
fn internal_v1_proto_to_bytes(&self, for_sign: bool, signed_by: Address) -> Option<Vec<u8>> { fn internal_v1_proto_to_bytes(&self, for_sign: bool, signed_by: Address) -> Option<Vec<u8>> {
if self.version == 1 && self.signature.len() == 96 { if self.signature.len() == 96 {
let mut v = Vec::with_capacity(256); let mut v = Vec::with_capacity(256);
if for_sign { if for_sign {
let _ = v.write_all(&[0x7f; 8]); let _ = v.write_all(&[0x7f; 8]);
@ -85,11 +66,11 @@ impl Tag {
} }
#[inline(always)] #[inline(always)]
pub fn v1_proto_to_bytes(&self, signed_by: Address) -> Option<Vec<u8>> { pub fn to_bytes(&self, signed_by: Address) -> Option<Vec<u8>> {
self.internal_v1_proto_to_bytes(false, signed_by) self.internal_v1_proto_to_bytes(false, signed_by)
} }
pub fn v1_proto_sign(&mut self, issuer: &Identity, issued_to: &Identity) -> bool { pub fn sign(&mut self, issuer: &Identity, issued_to: &Identity) -> bool {
self.issued_to = issued_to.address; self.issued_to = issued_to.address;
if let Some(to_sign) = self.internal_v1_proto_to_bytes(true, issuer.address) { if let Some(to_sign) = self.internal_v1_proto_to_bytes(true, issuer.address) {
if let Some(signature) = issuer.sign(&to_sign.as_slice(), true) { if let Some(signature) = issuer.sign(&to_sign.as_slice(), true) {
@ -100,7 +81,7 @@ impl Tag {
return false; return false;
} }
pub fn v1_proto_from_bytes(b: &[u8]) -> Result<(Self, &[u8]), InvalidParameterError> { pub fn from_bytes(b: &[u8]) -> Result<(Self, &[u8]), InvalidParameterError> {
const LEN: usize = 8 + 8 + 4 + 4 + 5 + 5 + 3 + 96 + 2; const LEN: usize = 8 + 8 + 4 + 4 + 5 + 5 + 3 + 96 + 2;
if b.len() < LEN { if b.len() < LEN {
return Err(InvalidParameterError("incomplete")); return Err(InvalidParameterError("incomplete"));
@ -117,7 +98,6 @@ impl Tag {
s.push_slice(&b[37..133]); s.push_slice(&b[37..133]);
s s
}, },
version: 1,
}, },
&b[LEN..], &b[LEN..],
)) ))

View file

@ -19,3 +19,4 @@ winapi = { version = "^0", features = ["handleapi", "ws2ipdef", "ws2tcpip"] }
[target."cfg(not(windows))".dependencies] [target."cfg(not(windows))".dependencies]
libc = "^0" libc = "^0"
signal-hook = "^0"

View file

@ -51,6 +51,26 @@ pub fn ms_monotonic() -> i64 {
std::time::Instant::now().duration_since(instant_zero).as_millis() as i64 std::time::Instant::now().duration_since(instant_zero).as_millis() as i64
} }
/// Wait for a kill signal (e.g. SIGINT or OS-equivalent) and return when received.
#[cfg(unix)]
pub fn wait_for_process_abort() {
if let Ok(mut signals) = signal_hook::iterator::Signals::new(&[libc::SIGINT, libc::SIGTERM, libc::SIGQUIT]) {
'wait_for_exit: loop {
for signal in signals.wait() {
match signal as libc::c_int {
libc::SIGINT | libc::SIGTERM | libc::SIGQUIT => {
break 'wait_for_exit;
}
_ => {}
}
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
} else {
panic!("unable to listen to OS signals");
}
}
#[cold] #[cold]
#[inline(never)] #[inline(never)]
pub extern "C" fn unlikely_branch() {} pub extern "C" fn unlikely_branch() {}