// (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md. use std::collections::{HashMap, HashSet}; use std::io::Write; use std::str::FromStr; use serde::{Deserialize, Serialize}; use crate::vl1::{Address, Identity, InetAddress}; use crate::vl2::rule::Rule; use crate::vl2::v1::{CertificateOfMembership, CertificateOfOwnership, Tag}; use crate::vl2::NetworkId; use zerotier_utils::buffer::Buffer; use zerotier_utils::dictionary::Dictionary; use zerotier_utils::error::InvalidParameterError; use zerotier_utils::marshalable::Marshalable; /// Network configuration object sent to nodes by network controllers. #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct NetworkConfig { /// Network ID pub network_id: NetworkId, /// Short address of node to which this config was issued pub issued_to: Address, /// Human-readable network name #[serde(skip_serializing_if = "String::is_empty")] #[serde(default)] pub name: String, /// A human-readable message for members of this network (V2 only) #[serde(skip_serializing_if = "String::is_empty")] #[serde(default)] pub motd: String, /// True if network has access control (the default) pub private: bool, /// Network configuration timestamp pub timestamp: i64, /// TTL for credentials on this network (or window size for V1 nodes) pub credential_ttl: i64, /// Network configuration revision number (V1) #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] pub revision: Option, /// L2 Ethernet MTU for this network. pub mtu: u16, /// Suggested horizon limit for multicast (not a hard limit, but 0 disables multicast) pub multicast_limit: u32, /// Multicast "like" expire time in milliseconds (default if omitted). #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] pub multicast_like_expire: Option, /// ZeroTier-assigned L3 routes for this node. #[serde(skip_serializing_if = "HashSet::is_empty")] #[serde(default)] pub routes: HashSet, /// ZeroTier-assigned static IP addresses for this node. #[serde(skip_serializing_if = "HashSet::is_empty")] #[serde(default)] pub static_ips: HashSet, /// Network flow rules (low level). #[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default)] pub rules: Vec, /// DNS resolvers available to be auto-configured on the host. #[serde(skip_serializing_if = "HashMap::is_empty")] #[serde(default)] pub dns: HashMap>, /// V1 certificate of membership and other exchange-able credentials, may be absent on V2-only networks. #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] pub v1_credentials: Option, /// URL to ZeroTier Central instance that is controlling the controller that issued this (if any). #[serde(skip_serializing_if = "String::is_empty")] #[serde(default)] pub central_url: String, /// SSO / third party auth information (if enabled). #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] pub sso: Option, } impl NetworkConfig { pub fn new(network_id: NetworkId, issued_to: Address) -> Self { Self { network_id, issued_to, name: String::new(), motd: String::new(), private: true, timestamp: 0, credential_ttl: 0, revision: None, mtu: 0, multicast_limit: 0, multicast_like_expire: None, routes: HashSet::new(), static_ips: HashSet::new(), rules: Vec::new(), dns: HashMap::new(), v1_credentials: None, central_url: String::new(), sso: None, } } /// Encode a network configuration for sending to V1 nodes. pub fn v1_proto_to_dictionary(&self, controller_identity: &Identity) -> Option { let mut d = Dictionary::new(); d.set_u64(proto_v1_field_name::network_config::VERSION, 6); d.set_str( proto_v1_field_name::network_config::NETWORK_ID, self.network_id.to_string().as_str(), ); if !self.name.is_empty() { d.set_str(proto_v1_field_name::network_config::NAME, self.name.as_str()); } d.set_str(proto_v1_field_name::network_config::ISSUED_TO, self.issued_to.to_string().as_str()); d.set_str( proto_v1_field_name::network_config::TYPE, if self.private { "0" } else { "1" }, ); d.set_u64(proto_v1_field_name::network_config::TIMESTAMP, self.timestamp as u64); d.set_u64(proto_v1_field_name::network_config::MAX_DELTA, self.credential_ttl as u64); d.set_u64(proto_v1_field_name::network_config::REVISION, self.revision.unwrap_or(0)); d.set_u64(proto_v1_field_name::network_config::MTU, self.mtu as u64); d.set_u64(proto_v1_field_name::network_config::MULTICAST_LIMIT, self.multicast_limit as u64); if !self.routes.is_empty() { let r: Vec = self.routes.iter().cloned().collect(); d.set_bytes( proto_v1_field_name::network_config::ROUTES, IpRoute::marshal_multiple_to_bytes(r.as_slice()).unwrap(), ); } if !self.static_ips.is_empty() { let ips: Vec = self.static_ips.iter().cloned().collect(); d.set_bytes( proto_v1_field_name::network_config::STATIC_IPS, InetAddress::marshal_multiple_to_bytes(ips.as_slice()).unwrap(), ); } if !self.rules.is_empty() { d.set_bytes( proto_v1_field_name::network_config::RULES, Rule::marshal_multiple_to_bytes(self.rules.as_slice()).unwrap(), ); } if !self.dns.is_empty() { // NOTE: v1 nodes only support one DNS server per network! If there is more than // one the first will be picked, whichever that is (it's a set). The UI should not // allow a user to add more than one unless this is a v2-only network. let mut dns_bin: Vec = Vec::with_capacity(256); if let Some((name, servers)) = self.dns.iter().next() { let mut name_bytes = name.as_bytes(); name_bytes = &name_bytes[..name_bytes.len().min(127)]; let _ = dns_bin.write_all(name_bytes); for _ in 0..(128 - name_bytes.len()) { dns_bin.push(0); } for s in servers.iter() { if let Ok(s) = s.to_buffer::<64>() { let _ = dns_bin.write_all(s.as_bytes()); } } } d.set_bytes(proto_v1_field_name::network_config::DNS, dns_bin); } if let Some(v1cred) = self.v1_credentials.as_ref() { d.set_bytes( proto_v1_field_name::network_config::CERTIFICATE_OF_MEMBERSHIP, v1cred .certificate_of_membership .to_bytes(self.network_id.network_controller()) .as_bytes() .to_vec(), ); if !v1cred.certificates_of_ownership.is_empty() { let mut certs = Vec::with_capacity(v1cred.certificates_of_ownership.len() * 256); for c in v1cred.certificates_of_ownership.iter() { 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); } if !v1cred.tags.is_empty() { let mut tags = Vec::with_capacity(v1cred.tags.len() * 256); for (_, t) in v1cred.tags.iter() { let _ = tags.write_all(t.to_bytes(controller_identity.address).as_ref()); } d.set_bytes(proto_v1_field_name::network_config::TAGS, tags); } } // node_info is not supported by V1 nodes if !self.central_url.is_empty() { d.set_str(proto_v1_field_name::network_config::CENTRAL_URL, self.central_url.as_str()); } if let Some(sso) = self.sso.as_ref() { d.set_bool(proto_v1_field_name::network_config::SSO_ENABLED, true); d.set_u64(proto_v1_field_name::network_config::SSO_VERSION, sso.version as u64); d.set_str( proto_v1_field_name::network_config::SSO_AUTHENTICATION_URL, sso.authentication_url.as_str(), ); d.set_u64( proto_v1_field_name::network_config::SSO_AUTHENTICATION_EXPIRY_TIME, sso.authentication_expiry_time as u64, ); d.set_str(proto_v1_field_name::network_config::SSO_ISSUER_URL, sso.issuer_url.as_str()); d.set_str(proto_v1_field_name::network_config::SSO_NONCE, sso.nonce.as_str()); d.set_str(proto_v1_field_name::network_config::SSO_STATE, sso.state.as_str()); d.set_str(proto_v1_field_name::network_config::SSO_CLIENT_ID, sso.client_id.as_str()); } else { d.set_bool(proto_v1_field_name::network_config::SSO_ENABLED, false); } Some(d) } /// Decode a V1 format network configuration. pub fn v1_proto_from_dictionary(d: &Dictionary) -> Result { let nwid = NetworkId::from_str( d.get_str(proto_v1_field_name::network_config::NETWORK_ID) .ok_or(InvalidParameterError("missing network ID"))?, ) .map_err(|_| InvalidParameterError("invalid network ID"))?; let issued_to_address = Address::from_str( d.get_str(proto_v1_field_name::network_config::ISSUED_TO) .ok_or(InvalidParameterError("missing address"))?, ) .map_err(|_| InvalidParameterError("invalid address"))?; let mut nc = Self::new(nwid, issued_to_address); d.get_str(proto_v1_field_name::network_config::NAME) .map(|x| nc.name = x.to_string()); nc.private = d.get_str(proto_v1_field_name::network_config::TYPE).map_or(true, |x| x == "1"); nc.timestamp = d .get_i64(proto_v1_field_name::network_config::TIMESTAMP) .ok_or(InvalidParameterError("missing timestamp"))?; nc.credential_ttl = d.get_i64(proto_v1_field_name::network_config::MAX_DELTA).unwrap_or(0); nc.revision = Some(d.get_u64(proto_v1_field_name::network_config::REVISION).unwrap_or(0)); nc.mtu = d .get_u64(proto_v1_field_name::network_config::MTU) .unwrap_or(crate::protocol::ZEROTIER_VIRTUAL_NETWORK_DEFAULT_MTU as u64) as u16; nc.multicast_limit = d.get_u64(proto_v1_field_name::network_config::MULTICAST_LIMIT).unwrap_or(0) as u32; if let Some(routes_bin) = d.get_bytes(proto_v1_field_name::network_config::ROUTES) { for r in IpRoute::unmarshal_multiple_from_bytes(routes_bin) .map_err(|_| InvalidParameterError("invalid route object(s)"))? .drain(..) { let _ = nc.routes.insert(r); } } if let Some(static_ips_bin) = d.get_bytes(proto_v1_field_name::network_config::STATIC_IPS) { for ip in InetAddress::unmarshal_multiple_from_bytes(static_ips_bin) .map_err(|_| InvalidParameterError("invalid route object(s)"))? .drain(..) { let _ = nc.static_ips.insert(ip); } } if let Some(rules_bin) = d.get_bytes(proto_v1_field_name::network_config::RULES) { nc.rules = Rule::unmarshal_multiple_from_bytes(rules_bin).map_err(|_| InvalidParameterError("invalid route object(s)"))?; } if let Some(dns_bin) = d.get_bytes(proto_v1_field_name::network_config::DNS) { if dns_bin.len() > 128 && dns_bin.len() < 1024 { let mut name = String::with_capacity(64); for i in 0..128 { if dns_bin[i] == 0 { break; } else { name.push(dns_bin[i] as char); } } if !name.is_empty() { let mut tmp: Buffer<1024> = Buffer::new(); let _ = tmp.append_bytes(&dns_bin[128..]); let mut servers = HashSet::new(); let mut cursor = 0; while cursor < tmp.len() { if let Ok(s) = InetAddress::unmarshal(&tmp, &mut cursor) { let _ = servers.insert(s); } else { break; } } if !servers.is_empty() { let _ = nc.dns.insert(name, servers); } } } } let mut v1cred = V1Credentials { certificate_of_membership: CertificateOfMembership::from_bytes( 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) { while !coo_bin.is_empty() { let c = CertificateOfOwnership::from_bytes(coo_bin)?; v1cred.certificates_of_ownership.push(c.0); coo_bin = c.1; } } if let Some(mut tag_bin) = d.get_bytes(proto_v1_field_name::network_config::TAGS) { while !tag_bin.is_empty() { let t = Tag::from_bytes(tag_bin)?; let _ = v1cred.tags.insert(t.0.id, t.0); 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) { nc.central_url = central_url.to_string(); } if d.get_bool(proto_v1_field_name::network_config::SSO_ENABLED).unwrap_or(false) { nc.sso = Some(SSOAuthConfiguration { version: d.get_u64(proto_v1_field_name::network_config::SSO_VERSION).unwrap_or(0) as u32, authentication_url: d .get_str(proto_v1_field_name::network_config::SSO_AUTHENTICATION_URL) .unwrap_or("") .to_string(), authentication_expiry_time: d .get_i64(proto_v1_field_name::network_config::SSO_AUTHENTICATION_EXPIRY_TIME) .unwrap_or(0), issuer_url: d .get_str(proto_v1_field_name::network_config::SSO_ISSUER_URL) .unwrap_or("") .to_string(), nonce: d.get_str(proto_v1_field_name::network_config::SSO_NONCE).unwrap_or("").to_string(), state: d.get_str(proto_v1_field_name::network_config::SSO_STATE).unwrap_or("").to_string(), client_id: d .get_str(proto_v1_field_name::network_config::SSO_CLIENT_ID) .unwrap_or("") .to_string(), }) } Ok(nc) } } #[allow(unused)] mod proto_v1_field_name { pub mod network_config { pub const VERSION: &'static str = "v"; pub const NETWORK_ID: &'static str = "nwid"; pub const TIMESTAMP: &'static str = "ts"; pub const REVISION: &'static str = "r"; pub const ISSUED_TO: &'static str = "id"; pub const FLAGS: &'static str = "f"; pub const MULTICAST_LIMIT: &'static str = "ml"; pub const TYPE: &'static str = "t"; pub const NAME: &'static str = "n"; pub const MTU: &'static str = "mtu"; pub const MAX_DELTA: &'static str = "ctmd"; pub const CERTIFICATE_OF_MEMBERSHIP: &'static str = "C"; pub const ROUTES: &'static str = "RT"; pub const STATIC_IPS: &'static str = "I"; pub const RULES: &'static str = "R"; pub const TAGS: &'static str = "TAG"; pub const CERTIFICATES_OF_OWNERSHIP: &'static str = "COO"; pub const DNS: &'static str = "DNS"; pub const NODE_INFO: &'static str = "NI"; pub const CENTRAL_URL: &'static str = "ssoce"; pub const SSO_ENABLED: &'static str = "ssoe"; pub const SSO_VERSION: &'static str = "ssov"; pub const SSO_AUTHENTICATION_URL: &'static str = "aurl"; pub const SSO_AUTHENTICATION_EXPIRY_TIME: &'static str = "aexpt"; pub const SSO_ISSUER_URL: &'static str = "iurl"; pub const SSO_NONCE: &'static str = "sson"; pub const SSO_STATE: &'static str = "ssos"; pub const SSO_CLIENT_ID: &'static str = "ssocid"; } pub mod sso_auth_info { pub const VERSION: &'static str = "aV"; pub const AUTHENTICATION_URL: &'static str = "aU"; pub const ISSUER_URL: &'static str = "iU"; pub const CENTRAL_URL: &'static str = "aCU"; pub const NONCE: &'static str = "aN"; pub const STATE: &'static str = "aS"; pub const CLIENT_ID: &'static str = "aCID"; } } /// SSO authentication configuration object. #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct SSOAuthConfiguration { pub version: u32, pub authentication_url: String, pub authentication_expiry_time: i64, pub issuer_url: String, pub nonce: String, pub state: String, pub client_id: String, } /// 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, #[serde(skip_serializing_if = "HashMap::is_empty")] #[serde(default)] pub tags: HashMap, } /// Statically pushed L3 IP routes included with a network configuration. #[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct IpRoute { pub target: InetAddress, #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] pub via: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] pub flags: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] pub metric: Option, } impl Marshalable for IpRoute { const MAX_MARSHAL_SIZE: usize = (InetAddress::MAX_MARSHAL_SIZE * 2) + 2 + 2; fn marshal( &self, buf: &mut zerotier_utils::buffer::Buffer, ) -> Result<(), zerotier_utils::marshalable::UnmarshalError> { self.target.marshal(buf)?; if let Some(via) = self.via.as_ref() { via.marshal(buf)?; } else { buf.append_u8(0)?; // "nil" InetAddress } buf.append_u16(self.flags.unwrap_or(0))?; buf.append_u16(self.metric.unwrap_or(0))?; Ok(()) } fn unmarshal( buf: &zerotier_utils::buffer::Buffer, cursor: &mut usize, ) -> Result { Ok(IpRoute { target: InetAddress::unmarshal(buf, cursor)?, via: { let via = InetAddress::unmarshal(buf, cursor)?; if via.is_nil() { None } else { Some(via) } }, flags: buf.read_u16(cursor).map(|f| { if f == 0 { None } else { Some(f) } })?, metric: buf.read_u16(cursor).map(|f| { if f == 0 { None } else { Some(f) } })?, }) } }