More controller work: fix to Rule, change strategy for blacklisting lingering deauthed members to something cleaner and lighter, move Revocation into VL2 since it will likely stick around post-V1.

This commit is contained in:
Adam Ierymenko 2022-12-06 17:36:20 -05:00
parent 52770ddaef
commit c94c0cfa7f
6 changed files with 162 additions and 97 deletions

View file

@ -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<Revocation>) {
fn send_revocations(&self, peer: &Peer, revocations: &mut Vec<Revocation>) {
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<Identity>,
network_id: NetworkId,
time_clock: i64,
) -> Result<(AuthenticationResult, Option<NetworkConfig>, Option<Vec<vl2::v1::Revocation>>), Box<dyn Error + Send + Sync>> {
) -> Result<(AuthenticationResult, Option<NetworkConfig>), Box<dyn Error + Send + Sync>> {
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();

View file

@ -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<String, BTreeSet<InetAddress>>,
/// 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<Rule>,
pub rules: Option<Vec<Rule>>,
/// 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,

View file

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

View file

@ -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<Self> {
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<u8, 256> {
pub fn v1_proto_to_bytes(&self, controller_address: Address) -> ArrayVec<u8, 256> {
self.internal_to_bytes(false, controller_address)
}
}

View file

@ -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<Rule> {
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 => {

View file

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