diff --git a/controller/src/controller.rs b/controller/src/controller.rs index c06b8cb93..be7208737 100644 --- a/controller/src/controller.rs +++ b/controller/src/controller.rs @@ -12,8 +12,7 @@ use zerotier_network_hypervisor::vl1::*; use zerotier_network_hypervisor::vl2; use zerotier_network_hypervisor::vl2::multicastauthority::MulticastAuthority; use zerotier_network_hypervisor::vl2::networkconfig::*; -use zerotier_network_hypervisor::vl2::v1::Revocation; -use zerotier_network_hypervisor::vl2::NetworkId; +use zerotier_network_hypervisor::vl2::{NetworkId, Revocation}; use zerotier_utils::blob::Blob; use zerotier_utils::buffer::OutOfBoundsError; use zerotier_utils::error::InvalidParameterError; @@ -199,7 +198,7 @@ impl Controller { } /// Send one or more revocation object(s) to a peer. The provided vector is drained. - fn v1_proto_send_revocations(&self, peer: &Peer, revocations: &mut Vec) { + fn send_revocations(&self, peer: &Peer, revocations: &mut Vec) { if let Some(host_system) = self.service.read().unwrap().upgrade() { let time_ticks = ms_monotonic(); while !revocations.is_empty() { @@ -220,7 +219,7 @@ impl Controller { packet.append_u16(send_count as u16)?; for _ in 0..send_count { let r = revocations.pop().unwrap(); - packet.append_bytes(r.to_bytes(self.local_identity.address).as_bytes())?; + packet.append_bytes(r.v1_proto_to_bytes(self.local_identity.address).as_bytes())?; } packet.append_u16(0)?; @@ -241,14 +240,10 @@ impl Controller { for m in all_network_members.iter() { if member.node_id != *m { if let Some(peer) = self.service.read().unwrap().upgrade().and_then(|s| s.node().peer(*m)) { - if peer.is_v2() { - todo!(); - } else { - revocations.clear(); - Revocation::new(member.network_id, time_clock, member.node_id, *m, &self.local_identity, false) - .map(|r| revocations.push(r)); - self.v1_proto_send_revocations(&peer, &mut revocations); - } + revocations.clear(); + Revocation::new(member.network_id, time_clock, member.node_id, *m, &self.local_identity, false) + .map(|r| revocations.push(r)); + self.send_revocations(&peer, &mut revocations); } } } @@ -260,9 +255,6 @@ impl Controller { /// 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). /// - /// This may also return revocations. If it does these should be sent along with or right after - /// the network config. This is for V1 nodes only, since V2 has another mechanism. - /// /// 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. async fn authorize( @@ -270,10 +262,10 @@ impl Controller { source_identity: &Verified, network_id: NetworkId, time_clock: i64, - ) -> Result<(AuthenticationResult, Option, Option>), Box> { + ) -> Result<(AuthenticationResult, Option), Box> { let network = self.database.get_network(network_id).await?; if network.is_none() { - return Ok((AuthenticationResult::Rejected, None, None)); + return Ok((AuthenticationResult::Rejected, None)); } let network = network.unwrap(); @@ -298,7 +290,7 @@ impl Controller { let _ = member.identity_fingerprint.replace(Blob::from(source_identity.fingerprint)); member_changed = true; } else { - return Ok((AuthenticationResult::RejectedIdentityMismatch, None, None)); + return Ok((AuthenticationResult::RejectedIdentityMismatch, None)); } } } @@ -313,7 +305,7 @@ impl Controller { member_changed = true; } } else { - return Ok((AuthenticationResult::RejectedIdentityMismatch, None, None)); + return Ok((AuthenticationResult::RejectedIdentityMismatch, None)); } } } @@ -332,7 +324,7 @@ impl Controller { let _ = member.insert(Member::new_with_identity(source_identity.as_ref().clone(), network_id)); member_changed = true; } else { - return Ok((AuthenticationResult::Rejected, None, None)); + return Ok((AuthenticationResult::Rejected, None)); } } @@ -361,13 +353,12 @@ impl Controller { assert!(!authentication_result.approved()); } - // drop 'mut' from these + // drop 'mut' from these since they should no longer change let member_authorized = member_authorized; let authentication_result = authentication_result; - let mut network_config = None; - let mut revocations = None; - if authentication_result.approved() { + // Generate network configuration if the member is authorized. + let network_config = if authentication_result.approved() { // We should not be able to make it here if this is still false. assert!(member_authorized); @@ -391,7 +382,7 @@ impl Controller { .eq(member.identity_fingerprint.as_ref().unwrap().as_bytes()) { debug_assert!(false); - return Ok((AuthenticationResult::RejectedDueToError, None, None)); + return Ok((AuthenticationResult::RejectedDueToError, None)); } // Figure out TTL for credentials (time window in V1). @@ -410,7 +401,38 @@ impl Controller { nc.mtu = network.mtu.unwrap_or(ZEROTIER_VIRTUAL_NETWORK_DEFAULT_MTU as u16); nc.routes = network.ip_routes.iter().cloned().collect(); nc.static_ips = member.ip_assignments.iter().cloned().collect(); - nc.rules = network.rules; + + // For any members that have been deauthorized but may still be in the cert agreement window, + // insert rules to drop packets to/from those members. This lets us ban them without + // adjusting the window, which is a simpler approach and has less risk of interrupting + // connectivity between valid members. + if let Ok(mut deauthed_members_still_in_window) = self + .database + .list_members_deauthorized_after(network.id, time_clock - (credential_ttl as i64)) + .await + { + if !deauthed_members_still_in_window.is_empty() { + deauthed_members_still_in_window.sort_unstable(); // may improve packet compression + nc.rules.reserve(deauthed_members_still_in_window.len() + 1); + let mut or = false; + for dead in deauthed_members_still_in_window.iter() { + nc.rules.push(vl2::rule::Rule::match_source_zerotier_address(false, or, *dead)); + or = true; + } + nc.rules.push(vl2::rule::Rule::action_drop()); + } + } + + // Then add the rest of the user-defined rules, or a blanket accept if there are none. + if let Some(rules) = network.rules.as_ref() { + nc.rules.reserve(rules.len()); + for r in rules.iter() { + nc.rules.push(r.clone()); + } + } else { + nc.rules.push(vl2::rule::Rule::action_accept()); + } + nc.dns = network.dns.iter().map(|(k, v)| (k.clone(), v.iter().cloned().collect())).collect(); if network.min_supported_version.unwrap_or(0) < (protocol::PROTOCOL_VERSION_V2 as u32) { @@ -434,7 +456,7 @@ impl Controller { coo.add_ip(ip); } if !coo.sign(&self.local_identity, &source_identity) { - return Ok((AuthenticationResult::RejectedDueToError, None, None)); + return Ok((AuthenticationResult::RejectedDueToError, None)); } v1cred.certificates_of_ownership.push(coo); } @@ -442,33 +464,14 @@ impl Controller { for (id, value) in member.tags.iter() { let tag = vl2::v1::Tag::new(*id, *value, &self.local_identity, network_id, &source_identity, time_clock); if tag.is_none() { - return Ok((AuthenticationResult::RejectedDueToError, None, None)); + return Ok((AuthenticationResult::RejectedDueToError, None)); } let _ = v1cred.tags.insert(*id, tag.unwrap()); } - // For anyone who has been deauthorized but is still in the window, send revocations. - if let Ok(deauthed_members_still_in_window) = self - .database - .list_members_deauthorized_after(network.id, time_clock - (credential_ttl as i64)) - .await - { - if !deauthed_members_still_in_window.is_empty() { - let mut revs = Vec::with_capacity(deauthed_members_still_in_window.len()); - for dm in deauthed_members_still_in_window.iter() { - if let Some(rev) = - Revocation::new(network_id, time_clock, *dm, source_identity.address, &self.local_identity, false) - { - revs.push(rev); - } - } - revocations = Some(revs); - } - } - nc.v1_credentials = Some(v1cred); } else { - return Ok((AuthenticationResult::RejectedDueToError, None, None)); + return Ok((AuthenticationResult::RejectedDueToError, None)); } } @@ -486,14 +489,17 @@ impl Controller { .or_default() .insert(network_id, ms_monotonic() + (credential_ttl as i64)); - network_config = Some(nc); - } + Some(nc) + } else { + None + }; + // Save any changes to member record. if member_changed { self.database.save_member(member, false).await?; } - Ok((authentication_result, network_config, revocations)) + Ok((authentication_result, network_config)) } } @@ -558,15 +564,12 @@ impl InnerProtocol for Controller { let now = ms_since_epoch(); let (result, config) = match self2.authorize(&source.identity, network_id, now).await { - Result::Ok((result, Some(config), revocations)) => { + Result::Ok((result, Some(config))) => { //println!("{}", serde_yaml::to_string(&config).unwrap()); self2.send_network_config(source.as_ref(), &config, Some(message_id)); - if let Some(mut revocations) = revocations { - self2.v1_proto_send_revocations(source.as_ref(), &mut revocations); - } (result, Some(config)) } - Result::Ok((result, None, _)) => (result, None), + Result::Ok((result, None)) => (result, None), Result::Err(e) => { #[cfg(debug_assertions)] let host = self2.service.read().unwrap().clone().upgrade().unwrap(); diff --git a/controller/src/model/network.rs b/controller/src/model/network.rs index 5f01b4327..5e94a3889 100644 --- a/controller/src/model/network.rs +++ b/controller/src/model/network.rs @@ -13,7 +13,7 @@ use zerotier_network_hypervisor::vl2::NetworkId; use crate::database::Database; use crate::model::Member; -pub const CREDENTIAL_WINDOW_SIZE_DEFAULT: u64 = 1000 * 60 * 60; +pub const CREDENTIAL_WINDOW_SIZE_DEFAULT: u64 = 1000 * 60 * 60; // 1 hour #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Default, Debug)] pub struct Ipv4AssignMode { @@ -90,10 +90,10 @@ pub struct Network { #[serde(skip_serializing_if = "BTreeMap::is_empty")] pub dns: BTreeMap>, - /// Network rule set. - #[serde(skip_serializing_if = "Vec::is_empty")] + /// Network rule set. (Default: one 'accept' rule.) + #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] - pub rules: Vec, + pub rules: Option>, /// If set this overrides the default TTL for certificates and credentials. /// @@ -151,7 +151,7 @@ impl Network { ip_assignment_pools: BTreeSet::new(), ip_routes: BTreeSet::new(), dns: BTreeMap::new(), - rules: Vec::new(), + rules: None, credential_ttl: None, min_supported_version: None, mtu: None, diff --git a/network-hypervisor/src/vl2/mod.rs b/network-hypervisor/src/vl2/mod.rs index ed9bf5d64..e692a39fa 100644 --- a/network-hypervisor/src/vl2/mod.rs +++ b/network-hypervisor/src/vl2/mod.rs @@ -2,6 +2,7 @@ mod multicastgroup; mod networkid; +mod revocation; mod switch; pub mod multicastauthority; @@ -11,4 +12,5 @@ pub mod v1; pub use multicastgroup::MulticastGroup; pub use networkid::NetworkId; +pub use revocation::Revocation; pub use switch::{Switch, SwitchInterface}; diff --git a/network-hypervisor/src/vl2/v1/revocation.rs b/network-hypervisor/src/vl2/revocation.rs similarity index 87% rename from network-hypervisor/src/vl2/v1/revocation.rs rename to network-hypervisor/src/vl2/revocation.rs index d1ee14bf7..484de1f6b 100644 --- a/network-hypervisor/src/vl2/v1/revocation.rs +++ b/network-hypervisor/src/vl2/revocation.rs @@ -1,6 +1,5 @@ use std::io::Write; -use zerotier_crypto::random; use zerotier_crypto::verified::Verified; use zerotier_utils::arrayvec::ArrayVec; @@ -10,9 +9,9 @@ use crate::vl1::{Address, Identity}; use crate::vl2::v1::CredentialType; use crate::vl2::NetworkId; +/// "Anti-credential" revoking a network member's permission to communicate on a network. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Revocation { - pub id: u32, pub network_id: NetworkId, pub threshold: i64, pub target: Address, @@ -31,7 +30,6 @@ impl Revocation { fast_propagate: bool, ) -> Option { let mut r = Self { - id: random::xorshift64_random() as u32, // arbitrary network_id, threshold, target, @@ -54,7 +52,7 @@ impl Revocation { } let _ = v.write_all(&[0; 4]); - let _ = v.write_all(&self.id.to_be_bytes()); + let _ = v.write_all(&((self.threshold as u32) ^ (u64::from(self.target) as u32)).to_be_bytes()); // ID only used in V1, arbitrary let _ = v.write_all(&self.network_id.to_bytes()); let _ = v.write_all(&[0; 8]); let _ = v.write_all(&self.threshold.to_be_bytes()); @@ -76,7 +74,7 @@ impl Revocation { } #[inline(always)] - pub fn to_bytes(&self, controller_address: Address) -> ArrayVec { + pub fn v1_proto_to_bytes(&self, controller_address: Address) -> ArrayVec { self.internal_to_bytes(false, controller_address) } } diff --git a/network-hypervisor/src/vl2/rule.rs b/network-hypervisor/src/vl2/rule.rs index 73391080c..31a634e12 100644 --- a/network-hypervisor/src/vl2/rule.rs +++ b/network-hypervisor/src/vl2/rule.rs @@ -135,28 +135,39 @@ mod rule_value { } } +fn t(not: bool, or: bool, action_or_condition: u8) -> u8 { + (not as u8).wrapping_shl(7) | (or as u8).wrapping_shl(4) | action_or_condition +} + #[repr(C, packed)] #[derive(Clone, Copy)] union RuleValue { - pub ipv6: rule_value::Ipv6, - pub ipv4: rule_value::Ipv4, - pub int_range: rule_value::IntRange, - pub characteristics: u64, - pub port_range: [u16; 2], - pub zt: u64, - pub random_probability: u32, - pub mac: [u8; 6], - pub vlan_id: u16, - pub vlan_pcp: u8, - pub vlan_dei: u8, - pub ethertype: u16, - pub ip_protocol: u8, - pub ip_tos: rule_value::IpTos, - pub frame_size_range: [u16; 2], - pub icmp: rule_value::Icmp, - pub tag: rule_value::Tag, - pub forward: rule_value::Forward, - pub qos_bucket: u8, + ipv6: rule_value::Ipv6, + ipv4: rule_value::Ipv4, + int_range: rule_value::IntRange, + characteristics: u64, + port_range: [u16; 2], + zt: u64, + random_probability: u32, + mac: [u8; 6], + vlan_id: u16, + vlan_pcp: u8, + vlan_dei: u8, + ethertype: u16, + ip_protocol: u8, + ip_tos: rule_value::IpTos, + frame_size_range: [u16; 2], + icmp: rule_value::Icmp, + tag: rule_value::Tag, + forward: rule_value::Forward, + qos_bucket: u8, +} + +impl Default for RuleValue { + #[inline(always)] + fn default() -> Self { + unsafe { zeroed() } + } } /// Trait to implement in order to evaluate rules. @@ -216,6 +227,65 @@ impl Default for Rule { } impl Rule { + pub fn action_accept() -> Self { + Self { t: action::ACCEPT, v: RuleValue::default() } + } + + pub fn action_drop() -> Self { + Self { t: action::DROP, v: RuleValue::default() } + } + + pub fn action_tee(address: Address, flags: u32, length: u16) -> Self { + Self { + t: action::TEE, + v: RuleValue { + forward: rule_value::Forward { address: address.into(), flags, length }, + }, + } + } + + pub fn action_watch(address: Address, flags: u32, length: u16) -> Self { + Self { + t: action::TEE, + v: RuleValue { + forward: rule_value::Forward { address: address.into(), flags, length }, + }, + } + } + + pub fn action_redirect(address: Address, flags: u32, length: u16) -> Self { + Self { + t: action::TEE, + v: RuleValue { + forward: rule_value::Forward { address: address.into(), flags, length }, + }, + } + } + + pub fn action_break() -> Self { + Self { t: action::BREAK, v: RuleValue::default() } + } + + pub fn action_priority(qos_bucket: u8) -> Self { + Self { t: action::PRIORITY, v: RuleValue { qos_bucket } } + } + + pub fn match_source_zerotier_address(not: bool, or: bool, address: Address) -> Self { + Self { + t: t(not, or, match_cond::SOURCE_ZEROTIER_ADDRESS), + v: RuleValue { zt: address.into() }, + } + } + + pub fn match_dest_zerotier_address(not: bool, or: bool, address: Address) -> Self { + Self { + t: t(not, or, match_cond::DEST_ZEROTIER_ADDRESS), + v: RuleValue { zt: address.into() }, + } + } + + // TODO: implement the rest of these static constructor methods if/when needed + #[inline(always)] pub fn action_or_condition(&self) -> u8 { self.t & 0x3f @@ -229,6 +299,9 @@ impl Rule { let not = (t & 0x80) != 0; let or = (t & 0x40) != 0; match t & 0x3f { + action::DROP => { + return v.action_drop(); + } action::ACCEPT => { return v.action_accept(); } @@ -760,16 +833,7 @@ impl<'a> HumanReadableRule<'a> { fn to_rule(&self) -> Option { if let Some(t) = HR_NAME_TO_RULE_TYPE.get(self._type.to_uppercase().as_str()) { let mut r = Rule::default(); - r.t = - *t | if self.not.unwrap_or(false) { - 0x80 - } else { - 0 - } | if self.or.unwrap_or(false) { - 0x40 - } else { - 0 - }; + r.t = (self.not.unwrap_or(false) as u8).wrapping_shl(7) | (self.or.unwrap_or(false) as u8).wrapping_shl(6); unsafe { match *t { action::TEE | action::WATCH | action::REDIRECT => { diff --git a/network-hypervisor/src/vl2/v1/mod.rs b/network-hypervisor/src/vl2/v1/mod.rs index 5c92fed0c..b18767fb0 100644 --- a/network-hypervisor/src/vl2/v1/mod.rs +++ b/network-hypervisor/src/vl2/v1/mod.rs @@ -1,6 +1,5 @@ mod certificateofmembership; mod certificateofownership; -mod revocation; mod tag; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -16,5 +15,4 @@ pub enum CredentialType { pub use certificateofmembership::CertificateOfMembership; pub use certificateofownership::{CertificateOfOwnership, Thing}; -pub use revocation::Revocation; pub use tag::Tag;