mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-06-03 19:13:43 +02:00
Add a simple AES-CTR wrapper to GMAC-SIV, and many other things.
This commit is contained in:
parent
9ea1cd2d6e
commit
788c310322
16 changed files with 661 additions and 130 deletions
|
@ -11,6 +11,8 @@ AES-GMAC-SIV is a "synthetic IV" (SIV) cipher construction implemented using onl
|
|||
|
||||
AES-GMAC-SIV is almost identical to [AES-GCM-SIV](https://en.wikipedia.org/wiki/AES-GCM-SIV), but that mode uses a non-standard MAC called POLYVAL in place of GMAC. POLYVAL is basically little-endian GMAC but the fact that it is not standard GMAC means it's not found in most cryptographic libraries and is not approved by FIPS140 and many other sets of compliance guidelines.
|
||||
|
||||
This also contains a simple AES-CTR wrapper for convenience.
|
||||
|
||||
## Why SIV? Why not just GCM?
|
||||
|
||||
Stream ciphers like AES-CTR, ChaCha20, and others require a number called an initialization vector (IV) for each use. The IV is sometimes called a nonce, or *number used once*, because using the same value for different messages with the same key is a major no-no.
|
||||
|
|
|
@ -2,7 +2,48 @@
|
|||
|
||||
use std::io::Write;
|
||||
|
||||
/// AES-GMAC-SIV encryptor/decryptor.
|
||||
pub struct AesCtr(gcrypt::cipher::Cipher);
|
||||
|
||||
impl AesCtr {
|
||||
/// Construct a new AES-CTR cipher.
|
||||
/// Key must be 16, 24, or 32 bytes in length or a panic will occur.
|
||||
#[inline(always)]
|
||||
pub fn new(k: &[u8]) -> Self {
|
||||
if k.len() != 32 && k.len() != 24 && k.len() != 16 {
|
||||
panic!("AES supports 128, 192, or 256 bits keys");
|
||||
}
|
||||
AesCtr(gcrypt::cipher::Cipher::new(gcrypt::cipher::Algorithm::Aes, gcrypt::cipher::Mode::Ctr).unwrap())
|
||||
}
|
||||
|
||||
/// Initialize AES-CTR for encryption or decryption with the given IV.
|
||||
/// If it's already been used, this also resets the cipher. There is no separate reset.
|
||||
#[inline(always)]
|
||||
pub fn init(&mut self, iv: &[u8]) {
|
||||
let _ = self.0.reset();
|
||||
if iv.len() == 16 {
|
||||
let _ = self.0.set_iv(iv);
|
||||
} else if iv.len() < 16 {
|
||||
let mut iv2 = [0_u8; 16];
|
||||
iv2[0..iv.len()].copy_from_slice(iv);
|
||||
let _ = self.0.set_iv(iv2);
|
||||
} else {
|
||||
panic!("CTR IV must be less than or equal to 16 bytes in length");
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypt or decrypt (same operation with CTR mode)
|
||||
#[inline(always)]
|
||||
pub fn crypt(&mut self, input: &[u8], output: &mut [u8]) {
|
||||
let _ = self.0.encrypt(input, output);
|
||||
}
|
||||
|
||||
/// Encrypt or decrypt in place (same operation with CTR mode)
|
||||
#[inline(always)]
|
||||
pub fn crypt_in_place(&mut self, data: &mut [u8]) {
|
||||
let _ = self.0.encrypt_inplace(data);
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(align(8))] // allow tag and tmp to be accessed as u64 arrays as well
|
||||
pub struct AesGmacSiv {
|
||||
tag: [u8; 16],
|
||||
|
|
|
@ -29,6 +29,77 @@ extern "C" {
|
|||
fn CCCryptorGCMReset(cryptor_ref: *mut c_void) -> i32;
|
||||
}
|
||||
|
||||
pub struct AesCtr(*mut c_void);
|
||||
|
||||
impl Drop for AesCtr {
|
||||
fn drop(&mut self) {
|
||||
if !self.0.is_null() {
|
||||
unsafe {
|
||||
CCCryptorRelease(self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AesCtr {
|
||||
/// Construct a new AES-CTR cipher.
|
||||
/// Key must be 16, 24, or 32 bytes in length or a panic will occur.
|
||||
#[inline(always)]
|
||||
pub fn new(k: &[u8]) -> Self {
|
||||
if k.len() != 32 && k.len() != 24 && k.len() != 16 {
|
||||
panic!("AES supports 128, 192, or 256 bits keys");
|
||||
}
|
||||
unsafe {
|
||||
let mut ptr: *mut c_void = null_mut();
|
||||
let result = CCCryptorCreateWithMode(kCCEncrypt, kCCModeCTR, kCCAlgorithmAES, 0, crate::ZEROES.as_ptr().cast(), k.as_ptr().cast(), k.len(), null(), 0, 0, 0, &mut ptr);
|
||||
if result != 0 {
|
||||
panic!("CCCryptorCreateWithMode for CTR mode returned {}", result);
|
||||
}
|
||||
AesCtr(ptr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize AES-CTR for encryption or decryption with the given IV.
|
||||
/// If it's already been used, this also resets the cipher. There is no separate reset.
|
||||
#[inline(always)]
|
||||
pub fn init(&mut self, iv: &[u8]) {
|
||||
unsafe {
|
||||
if iv.len() == 16 {
|
||||
if CCCryptorReset(self.0, iv.as_ptr().cast()) != 0 {
|
||||
panic!("CCCryptorReset for CTR mode failed (old MacOS bug)");
|
||||
}
|
||||
} else if iv.len() < 16 {
|
||||
let mut iv2 = [0_u8; 16];
|
||||
iv2[0..iv.len()].copy_from_slice(iv);
|
||||
if CCCryptorReset(self.0, iv2.as_ptr().cast()) != 0 {
|
||||
panic!("CCCryptorReset for CTR mode failed (old MacOS bug)");
|
||||
}
|
||||
} else {
|
||||
panic!("CTR IV must be less than or equal to 16 bytes in length");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypt or decrypt (same operation with CTR mode)
|
||||
#[inline(always)]
|
||||
pub fn crypt(&mut self, input: &[u8], output: &mut [u8]) {
|
||||
unsafe {
|
||||
assert!(output.len() >= input.len());
|
||||
let mut data_out_written: usize = 0;
|
||||
CCCryptorUpdate(self.0, input.as_ptr().cast(), input.len(), output.as_mut_ptr().cast(), output.len(), &mut data_out_written);
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypt or decrypt in place (same operation with CTR mode)
|
||||
#[inline(always)]
|
||||
pub fn crypt_in_place(&mut self, data: &mut [u8]) {
|
||||
unsafe {
|
||||
let mut data_out_written: usize = 0;
|
||||
CCCryptorUpdate(self.0, data.as_ptr().cast(), data.len(), data.as_mut_ptr().cast(), data.len(), &mut data_out_written);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(align(8))]
|
||||
pub struct AesGmacSiv {
|
||||
tag: [u8; 16],
|
||||
|
@ -165,6 +236,7 @@ impl AesGmacSiv {
|
|||
#[inline(always)]
|
||||
pub fn encrypt_second_pass(&mut self, plaintext: &[u8], ciphertext: &mut [u8]) {
|
||||
unsafe {
|
||||
assert!(ciphertext.len() >= plaintext.len());
|
||||
let mut data_out_written: usize = 0;
|
||||
CCCryptorUpdate(self.ctr, plaintext.as_ptr().cast(), plaintext.len(), ciphertext.as_mut_ptr().cast(), ciphertext.len(), &mut data_out_written);
|
||||
}
|
||||
|
|
|
@ -1,15 +1,80 @@
|
|||
// AES-GMAC-SIV implemented using OpenSSL.
|
||||
|
||||
use std::convert::TryInto;
|
||||
use openssl::symm::{Crypter, Cipher, Mode};
|
||||
|
||||
fn aes_ctr_by_key_size(ks: usize) -> Cipher {
|
||||
match ks {
|
||||
16 => Cipher::aes_128_ctr(),
|
||||
24 => Cipher::aes_192_ctr(),
|
||||
32 => Cipher::aes_256_ctr(),
|
||||
_ => {
|
||||
panic!("AES supports 128, 192, or 256 bits keys");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn aes_gcm_by_key_size(ks: usize) -> Cipher {
|
||||
match ks {
|
||||
16 => Cipher::aes_128_gcm(),
|
||||
24 => Cipher::aes_192_gcm(),
|
||||
32 => Cipher::aes_256_gcm(),
|
||||
_ => {
|
||||
panic!("AES supports 128, 192, or 256 bits keys");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn aes_ecb_by_key_size(ks: usize) -> Cipher {
|
||||
match ks {
|
||||
16 => Cipher::aes_128_ecb(),
|
||||
24 => Cipher::aes_192_ecb(),
|
||||
32 => Cipher::aes_256_ecb(),
|
||||
_ => {
|
||||
panic!("AES supports 128, 192, or 256 bits keys");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AesCtr(Vec<u8>, Option<Crypter>);
|
||||
|
||||
impl AesCtr {
|
||||
/// Construct a new AES-CTR cipher.
|
||||
/// Key must be 16, 24, or 32 bytes in length or a panic will occur.
|
||||
#[inline(always)]
|
||||
pub fn new(k: &[u8]) -> Self {
|
||||
if k.len() != 32 && k.len() != 24 && k.len() != 16 {
|
||||
panic!("AES supports 128, 192, or 256 bits keys");
|
||||
}
|
||||
AesCtr(k.to_vec(), None)
|
||||
}
|
||||
|
||||
/// Initialize AES-CTR for encryption or decryption with the given IV.
|
||||
/// If it's already been used, this also resets the cipher. There is no separate reset.
|
||||
#[inline(always)]
|
||||
pub fn init(&mut self, iv: &[u8]) {
|
||||
let _ = self.1.replace(Crypter::new(aes_ctr_by_key_size(self.0.len()), Mode::Encrypt, self.0.as_slice(), Some(iv)).unwrap());
|
||||
}
|
||||
|
||||
/// Encrypt or decrypt (same operation with CTR mode)
|
||||
#[inline(always)]
|
||||
pub fn crypt(&mut self, input: &[u8], output: &mut [u8]) {
|
||||
let _ = self.1.as_mut().unwrap().update(input, output);
|
||||
}
|
||||
|
||||
/// Encrypt or decrypt in place (same operation with CTR mode)
|
||||
#[inline(always)]
|
||||
pub fn crypt_in_place(&mut self, data: &mut [u8]) {
|
||||
let _ = self.1.as_mut().unwrap().update(unsafe { &*std::slice::from_raw_parts(data.as_ptr(), data.len()) }, data);
|
||||
}
|
||||
}
|
||||
|
||||
/// AES-GMAC-SIV encryptor/decryptor.
|
||||
#[repr(align(8))] // allow tag and tmp to be accessed as u64 arrays as well
|
||||
pub struct AesGmacSiv {
|
||||
tag: [u8; 16],
|
||||
tmp: [u8; 16],
|
||||
k0: [u8; 32],
|
||||
k1: [u8; 32],
|
||||
k0: Vec<u8>,
|
||||
k1: Vec<u8>,
|
||||
ctr: Option<Crypter>,
|
||||
gmac: Option<Crypter>,
|
||||
}
|
||||
|
@ -28,8 +93,8 @@ impl AesGmacSiv {
|
|||
AesGmacSiv {
|
||||
tag: [0_u8; 16],
|
||||
tmp: [0_u8; 16],
|
||||
k0: k0.try_into().unwrap(),
|
||||
k1: k1.try_into().unwrap(),
|
||||
k0: k0.to_vec(),
|
||||
k1: k1.to_vec(),
|
||||
ctr: None,
|
||||
gmac: None,
|
||||
}
|
||||
|
@ -47,7 +112,7 @@ impl AesGmacSiv {
|
|||
pub fn encrypt_init(&mut self, iv: &[u8]) {
|
||||
self.tag[0..8].copy_from_slice(iv);
|
||||
self.tag[8..16].fill(0);
|
||||
let _ = self.gmac.replace(Crypter::new(Cipher::aes_256_gcm(), Mode::Encrypt, &self.k0, Some(&self.tag)).unwrap());
|
||||
let _ = self.gmac.replace(Crypter::new(aes_gcm_by_key_size(self.k0.len()), Mode::Encrypt, self.k0.as_slice(), Some(&self.tag)).unwrap());
|
||||
}
|
||||
|
||||
/// Set additional authenticated data (data to be authenticated but not encrypted).
|
||||
|
@ -79,11 +144,11 @@ impl AesGmacSiv {
|
|||
*self.tag.as_mut_ptr().cast::<u64>().offset(1) = *tmp ^ *tmp.offset(1);
|
||||
}
|
||||
let mut tag_tmp = [0_u8; 32];
|
||||
let _ = Crypter::new(Cipher::aes_256_ecb(), Mode::Encrypt, &self.k1, None).unwrap().update(&self.tag, &mut tag_tmp);
|
||||
let _ = Crypter::new(aes_ecb_by_key_size(self.k1.len()), Mode::Encrypt, self.k1.as_slice(), None).unwrap().update(&self.tag, &mut tag_tmp);
|
||||
self.tag.copy_from_slice(&tag_tmp[0..16]);
|
||||
self.tmp.copy_from_slice(&self.tag);
|
||||
self.tmp[12] &= 0x7f;
|
||||
let _ = self.ctr.replace(Crypter::new(Cipher::aes_256_ctr(), Mode::Encrypt, &self.k1, Some(&self.tmp)).unwrap());
|
||||
let _ = self.ctr.replace(Crypter::new(aes_ctr_by_key_size(self.k1.len()), Mode::Encrypt, self.k1.as_slice(), Some(&self.tmp)).unwrap());
|
||||
}
|
||||
|
||||
/// Feed plaintext for second pass and write ciphertext to supplied buffer.
|
||||
|
@ -110,16 +175,16 @@ impl AesGmacSiv {
|
|||
#[inline(always)]
|
||||
fn decrypt_init_internal(&mut self) {
|
||||
self.tmp[12] &= 0x7f;
|
||||
let _ = self.ctr.replace(Crypter::new(Cipher::aes_256_ctr(), Mode::Decrypt, &self.k1, Some(&self.tmp)).unwrap());
|
||||
let _ = self.ctr.replace(Crypter::new(aes_ctr_by_key_size(self.k1.len()), Mode::Decrypt, self.k1.as_slice(), Some(&self.tmp)).unwrap());
|
||||
let mut tag_tmp = [0_u8; 32];
|
||||
let _ = Crypter::new(Cipher::aes_256_ecb(), Mode::Decrypt, &self.k1, None).unwrap().update(&self.tag, &mut tag_tmp);
|
||||
let _ = Crypter::new(aes_ecb_by_key_size(self.k1.len()), Mode::Decrypt, self.k1.as_slice(), None).unwrap().update(&self.tag, &mut tag_tmp);
|
||||
self.tag.copy_from_slice(&tag_tmp[0..16]);
|
||||
unsafe { // tmp[0..8] = tag[0..8], tmp[8..16] = 0
|
||||
let tmp = self.tmp.as_mut_ptr().cast::<u64>();
|
||||
*tmp = *self.tag.as_mut_ptr().cast::<u64>();
|
||||
*tmp.offset(1) = 0;
|
||||
}
|
||||
let _ = self.gmac.replace(Crypter::new(Cipher::aes_256_gcm(), Mode::Encrypt, &self.k0, Some(&self.tmp)).unwrap());
|
||||
let _ = self.gmac.replace(Crypter::new(aes_gcm_by_key_size(self.k0.len()), Mode::Encrypt, self.k0.as_slice(), Some(&self.tmp)).unwrap());
|
||||
}
|
||||
|
||||
/// Initialize this cipher for decryption.
|
||||
|
|
|
@ -8,7 +8,7 @@ mod impl_gcrypt;
|
|||
mod impl_openssl;
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
pub use impl_macos::AesGmacSiv;
|
||||
pub use impl_macos::{AesCtr, AesGmacSiv};
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "ios", target_arch = "s390x", target_arch = "powerpc64le", target_arch = "powerpc64")))]
|
||||
pub use impl_gcrypt::AesGmacSiv;
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
use std::convert::TryInto;
|
||||
use std::mem::size_of;
|
||||
use std::ptr::write_volatile;
|
||||
|
||||
/// Container for secrets that clears them on drop.
|
||||
///
|
||||
/// We can't be totally sure that things like libraries are doing this and it's
|
||||
/// hard to get every use of a secret anywhere, but using this in our code at
|
||||
/// least reduces the number of secrets that are left lying around in memory.
|
||||
///
|
||||
/// This is generally a low-risk thing since it's process memory that's protected,
|
||||
/// but it's still not a bad idea due to things like swap or obscure side channel
|
||||
/// attacks that allow memory to be read.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Secret<const L: usize>(pub(crate) [u8; L]);
|
||||
|
||||
|
@ -20,9 +29,17 @@ impl<const L: usize> Secret<L> {
|
|||
|
||||
impl<const L: usize> Drop for Secret<L> {
|
||||
fn drop(&mut self) {
|
||||
let p = self.0.as_mut_ptr();
|
||||
for i in 0..L {
|
||||
unsafe { write_volatile(p.offset(i as isize), 0_u8) };
|
||||
unsafe {
|
||||
let p = self.0.as_mut_ptr();
|
||||
if (L % size_of::<usize>()) == 0 {
|
||||
for i in 0..(L / size_of::<usize>()) {
|
||||
write_volatile(p.cast::<usize>().offset(i as isize), 0_usize);
|
||||
}
|
||||
} else {
|
||||
for i in 0..L {
|
||||
write_volatile(p.offset(i as isize), 0_u8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use std::mem::size_of;
|
||||
use std::ptr::NonNull;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
@ -14,7 +15,7 @@ struct PoolEntry<O, F: PoolFactory<O>> {
|
|||
return_pool: Weak<PoolInner<O, F>>,
|
||||
}
|
||||
|
||||
struct PoolInner<O, F: PoolFactory<O>>(F, Mutex<Vec<*mut PoolEntry<O, F>>>);
|
||||
struct PoolInner<O, F: PoolFactory<O>>(F, Mutex<Vec<NonNull<PoolEntry<O, F>>>>);
|
||||
|
||||
/// Container for pooled objects that have been checked out of the pool.
|
||||
///
|
||||
|
@ -26,7 +27,7 @@ struct PoolInner<O, F: PoolFactory<O>>(F, Mutex<Vec<*mut PoolEntry<O, F>>>);
|
|||
/// Note that pooled objects are not clonable. If you want to share them use Rc<>
|
||||
/// or Arc<>.
|
||||
#[repr(transparent)]
|
||||
pub struct Pooled<O, F: PoolFactory<O>>(*mut PoolEntry<O, F>);
|
||||
pub struct Pooled<O, F: PoolFactory<O>>(NonNull<PoolEntry<O, F>>);
|
||||
|
||||
impl<O, F: PoolFactory<O>> Pooled<O, F> {
|
||||
/// Get a raw pointer to the object wrapped by this pooled object container.
|
||||
|
@ -34,9 +35,8 @@ impl<O, F: PoolFactory<O>> Pooled<O, F> {
|
|||
/// from_raw() or memory will leak.
|
||||
#[inline(always)]
|
||||
pub unsafe fn into_raw(self) -> *mut O {
|
||||
debug_assert!(!self.0.is_null());
|
||||
debug_assert_eq!(self.0.cast::<u8>(), (&mut (*self.0).obj as *mut O).cast::<u8>());
|
||||
let ptr = self.0.cast::<O>();
|
||||
debug_assert_eq!((&self.0.as_ref().obj as *const O).cast::<u8>(), (self.0.as_ref() as *const PoolEntry<O, F>).cast::<u8>());
|
||||
let ptr = self.0.as_ptr().cast::<O>();
|
||||
std::mem::forget(self);
|
||||
ptr
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ impl<O, F: PoolFactory<O>> Pooled<O, F> {
|
|||
#[inline(always)]
|
||||
pub unsafe fn from_raw(raw: *mut O) -> Option<Self> {
|
||||
if !raw.is_null() {
|
||||
Some(Self(raw.cast()))
|
||||
Some(Self(NonNull::new_unchecked(raw.cast())))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -60,44 +60,43 @@ impl<O, F: PoolFactory<O>> std::ops::Deref for Pooled<O, F> {
|
|||
|
||||
#[inline(always)]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
debug_assert!(!self.0.is_null());
|
||||
unsafe { &(*self.0).obj }
|
||||
unsafe { &self.0.as_ref().obj }
|
||||
}
|
||||
}
|
||||
|
||||
impl<O, F: PoolFactory<O>> AsRef<O> for Pooled<O, F> {
|
||||
#[inline(always)]
|
||||
fn as_ref(&self) -> &O {
|
||||
debug_assert!(!self.0.is_null());
|
||||
unsafe { &(*self.0).obj }
|
||||
unsafe { &self.0.as_ref().obj }
|
||||
}
|
||||
}
|
||||
|
||||
impl<O, F: PoolFactory<O>> std::ops::DerefMut for Pooled<O, F> {
|
||||
#[inline(always)]
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
debug_assert!(!self.0.is_null());
|
||||
unsafe { &mut (*self.0).obj }
|
||||
unsafe { &mut self.0.as_mut().obj }
|
||||
}
|
||||
}
|
||||
|
||||
impl<O, F: PoolFactory<O>> AsMut<O> for Pooled<O, F> {
|
||||
#[inline(always)]
|
||||
fn as_mut(&mut self) -> &mut O {
|
||||
debug_assert!(!self.0.is_null());
|
||||
unsafe { &mut (*self.0).obj }
|
||||
unsafe { &mut self.0.as_mut().obj }
|
||||
}
|
||||
}
|
||||
|
||||
impl<O, F: PoolFactory<O>> Drop for Pooled<O, F> {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
Weak::upgrade(&(*self.0).return_pool).map_or_else(|| {
|
||||
drop(Box::from_raw(self.0))
|
||||
}, |p| {
|
||||
p.0.reset(&mut (*self.0).obj);
|
||||
p.1.lock().push(self.0)
|
||||
})
|
||||
let p = Weak::upgrade(&self.0.as_ref().return_pool);
|
||||
if p.is_some() {
|
||||
let p = p.unwrap();
|
||||
p.0.reset(&mut self.0.as_mut().obj);
|
||||
let mut q = p.1.lock();
|
||||
q.push(self.0.clone())
|
||||
} else {
|
||||
drop(Box::from_raw(self.0.as_ptr()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -114,33 +113,31 @@ impl<O, F: PoolFactory<O>> Pool<O, F> {
|
|||
|
||||
/// Get a pooled object, or allocate one if the pool is empty.
|
||||
pub fn get(&self) -> Pooled<O, F> {
|
||||
Pooled::<O, F>(self.0.1.lock().pop().map_or_else(|| {
|
||||
Box::into_raw(Box::new(PoolEntry::<O, F> {
|
||||
obj: self.0.0.create(),
|
||||
return_pool: Arc::downgrade(&self.0),
|
||||
unsafe {
|
||||
Pooled::<O, F>(self.0.1.lock().pop().unwrap_or_else(|| {
|
||||
NonNull::new_unchecked(Box::into_raw(Box::new(PoolEntry::<O, F> {
|
||||
obj: self.0.0.create(),
|
||||
return_pool: Arc::downgrade(&self.0),
|
||||
})))
|
||||
}))
|
||||
}, |obj| {
|
||||
debug_assert!(!obj.is_null());
|
||||
obj
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get approximate memory use in bytes (does not include checked out objects).
|
||||
#[inline(always)]
|
||||
pub fn pool_memory_bytes(&self) -> usize {
|
||||
self.0.1.lock().len() * (size_of::<PoolEntry<O, F>>() + size_of::<usize>())
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispose of all pooled objects, freeing any memory they use.
|
||||
///
|
||||
/// If get() is called after this new objects will be allocated, and any outstanding
|
||||
/// objects will still be returned on drop unless the pool itself is dropped. This can
|
||||
/// be done to free some memory if there has been a spike in memory use.
|
||||
pub fn purge(&self) {
|
||||
let mut p = self.0.1.lock();
|
||||
for obj in p.iter() {
|
||||
drop(unsafe { Box::from_raw(*obj) });
|
||||
loop {
|
||||
let o = p.pop();
|
||||
if o.is_some() {
|
||||
drop(unsafe { Box::from_raw(o.unwrap().as_ptr()) })
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
p.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -185,8 +182,9 @@ mod tests {
|
|||
for _ in 0..16384 {
|
||||
let mut o1 = p2.get();
|
||||
o1.push('a');
|
||||
let mut o2 = p2.get();
|
||||
let o2 = p2.get();
|
||||
drop(o1);
|
||||
let mut o2 = unsafe { Pooled::<String, TestPoolFactory>::from_raw(o2.into_raw()).unwrap() };
|
||||
o2.push('b');
|
||||
ctr2.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
@ -198,6 +196,5 @@ mod tests {
|
|||
break;
|
||||
}
|
||||
}
|
||||
//println!("pool memory size: {}", p.pool_memory_bytes());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
use std::mem::size_of;
|
||||
use std::io::Write;
|
||||
use std::mem::{size_of, MaybeUninit};
|
||||
use std::ptr::write_bytes;
|
||||
|
||||
use crate::util::pool::PoolFactory;
|
||||
|
||||
const OVERFLOW_ERR_MSG: &'static str = "overflow";
|
||||
|
||||
/// Annotates a type as containing only primitive types like integers and arrays.
|
||||
/// This means it's safe to abuse with raw copy, raw zero, or "type punning."
|
||||
/// This is ONLY used for packed protocol header or segment objects.
|
||||
/// Annotates a structure as containing only primitive types.
|
||||
///
|
||||
/// This indicates structs that are safe to abuse like raw memory by casting from
|
||||
/// byte arrays of the same size, etc. It also generally implies packed representation
|
||||
/// and alignment should not be assumed since these can be fetched using struct
|
||||
/// extracting methods of Buffer that do not check alignment.
|
||||
pub unsafe trait RawObject: Sized {}
|
||||
|
||||
/// A byte array that supports safe and efficient appending of data or raw objects.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
/// A safe bounds checked I/O buffer with extensions for convenient appending of RawObject types.
|
||||
pub struct Buffer<const L: usize>(usize, [u8; L]);
|
||||
|
||||
unsafe impl<const L: usize> RawObject for Buffer<L> {}
|
||||
|
@ -22,23 +24,25 @@ impl<const L: usize> Default for Buffer<L> {
|
|||
}
|
||||
}
|
||||
|
||||
const OVERFLOW_ERR_MSG: &'static str = "overflow";
|
||||
|
||||
impl<const L: usize> Buffer<L> {
|
||||
#[inline(always)]
|
||||
pub fn new() -> Self {
|
||||
Self(0, [0_u8; L])
|
||||
}
|
||||
|
||||
/// Get a Buffer that is a copy of a byte slice, or return None if the slice doesn't fit.
|
||||
/// Get a Buffer initialized with a copy of a byte slice.
|
||||
#[inline(always)]
|
||||
pub fn from_bytes(b: &[u8]) -> Option<Self> {
|
||||
pub fn from_bytes(b: &[u8]) -> std::io::Result<Self> {
|
||||
let l = b.len();
|
||||
if l <= L {
|
||||
let mut tmp = Self::new();
|
||||
tmp.0 = l;
|
||||
tmp.1[0..l].copy_from_slice(b);
|
||||
Some(tmp)
|
||||
Ok(tmp)
|
||||
} else {
|
||||
None
|
||||
Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, OVERFLOW_ERR_MSG))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,8 +68,7 @@ impl<const L: usize> Buffer<L> {
|
|||
|
||||
#[inline(always)]
|
||||
pub fn clear(&mut self) {
|
||||
self.0 = 0;
|
||||
self.1.fill(0);
|
||||
unsafe { write_bytes((self as *mut Self).cast::<u8>(), 0, L); }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
|
@ -78,6 +81,22 @@ impl<const L: usize> Buffer<L> {
|
|||
self.0 == 0
|
||||
}
|
||||
|
||||
/// Explicitly set the size of the data in this buffer, returning an error on overflow.
|
||||
/// If the new size is larger than the old size, the new space is zeroed.
|
||||
#[inline(always)]
|
||||
pub fn set_size(&mut self, new_size: usize) -> std::io::Result<()> {
|
||||
if new_size <= L {
|
||||
let old_size = self.0;
|
||||
self.0 = new_size;
|
||||
if old_size < new_size {
|
||||
self.1[old_size..new_size].fill(0);
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, OVERFLOW_ERR_MSG))
|
||||
}
|
||||
}
|
||||
|
||||
/// Append a packed structure and call a function to initialize it in place.
|
||||
/// Anything not initialized will be zero.
|
||||
#[inline(always)]
|
||||
|
@ -126,7 +145,6 @@ impl<const L: usize> Buffer<L> {
|
|||
}
|
||||
|
||||
/// Append a dynamic byte slice (copy into buffer).
|
||||
/// Use append_and_init_ functions if possible as these avoid extra copies.
|
||||
#[inline(always)]
|
||||
pub fn append_bytes(&mut self, buf: &[u8]) -> std::io::Result<()> {
|
||||
let ptr = self.0;
|
||||
|
|
|
@ -16,12 +16,16 @@ pub const KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K0: u8 = b'0';
|
|||
/// KBKDF usage label for the second AES-GMAC-SIV key.
|
||||
pub const KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K1: u8 = b'1';
|
||||
|
||||
/// KBKDF usage label for acknowledgement of a shared secret.
|
||||
pub const KBKDF_KEY_USAGE_LABEL_EPHEMERAL_ACK: u8 = b'A';
|
||||
|
||||
/// Size of packet header that lies outside the encryption envelope.
|
||||
pub const PACKET_HEADER_SIZE: usize = 27;
|
||||
|
||||
/// Maximum packet payload size including the verb/flags field.
|
||||
/// This is large enough to carry "jumbo MTU" packets. The size is
|
||||
/// odd because 10005+27 == 10032 which is divisible by 16. This
|
||||
///
|
||||
/// This is large enough to carry "jumbo MTU" packets. The exact
|
||||
/// value is because 10005+27 == 10032 which is divisible by 16. This
|
||||
/// improves memory layout and alignment when buffers are allocated.
|
||||
/// This value could technically be increased but it would require a
|
||||
/// protocol version bump and only new nodes would be able to accept
|
||||
|
@ -97,8 +101,14 @@ pub const VERB_FLAG_COMPRESSED: u8 = 0x80;
|
|||
/// Mask to get only the verb from the verb + verb flags byte.
|
||||
pub const VERB_MASK: u8 = 0x1f;
|
||||
|
||||
/// Maximum number of verbs that the protocol can support.
|
||||
pub const VERB_MAX_COUNT: usize = 32;
|
||||
|
||||
/// Maximum number of packet hops allowed by the protocol.
|
||||
pub const PROTOCOL_MAX_HOPS: usize = 7;
|
||||
pub const PROTOCOL_MAX_HOPS: u8 = 7;
|
||||
|
||||
/// Maximum number of hops to allow.
|
||||
pub const FORWARD_MAX_HOPS: u8 = 3;
|
||||
|
||||
/// Frequency for WHOIS retries
|
||||
pub const WHOIS_RETRY_INTERVAL: i64 = 1000;
|
||||
|
@ -114,3 +124,6 @@ pub const LOCATOR_MAX_ENDPOINTS: usize = 32;
|
|||
|
||||
/// Keepalive interval for paths in milliseconds.
|
||||
pub const PATH_KEEPALIVE_INTERVAL: i64 = 20000;
|
||||
|
||||
/// Interval for servicing and background operations on peers.
|
||||
pub const PEER_SERVICE_INTERVAL: i64 = 30000;
|
||||
|
|
|
@ -11,6 +11,16 @@ pub(crate) struct FragmentedPacket {
|
|||
}
|
||||
|
||||
impl FragmentedPacket {
|
||||
#[inline(always)]
|
||||
pub fn new(ts: i64) -> Self {
|
||||
Self {
|
||||
ts_ticks: ts,
|
||||
frags: [None, None, None, None, None, None, None, None],
|
||||
have: 0,
|
||||
expecting: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a fragment to this fragment set and return true if the packet appears complete.
|
||||
/// This will panic if 'no' is out of bounds.
|
||||
#[inline(always)]
|
||||
|
@ -19,7 +29,7 @@ impl FragmentedPacket {
|
|||
if self.frags[no as usize].replace(frag).is_none() {
|
||||
self.have = self.have.wrapping_add(1);
|
||||
self.expecting |= expecting; // in valid streams expecting is either 0 or the (same) total
|
||||
return self.have == self.expecting;
|
||||
return self.have == self.expecting && self.have < FRAGMENT_COUNT_MAX as u8;
|
||||
}
|
||||
}
|
||||
false
|
||||
|
|
|
@ -468,14 +468,9 @@ impl Identity {
|
|||
/// On success the identity and the number of bytes actually read from the slice are
|
||||
/// returned.
|
||||
pub fn unmarshal_from_bytes(bytes: &[u8]) -> std::io::Result<(Identity, usize)> {
|
||||
let buf = Buffer::<2048>::from_bytes(bytes);
|
||||
if buf.is_none() {
|
||||
std::io::Result::Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "data object too large"))
|
||||
} else {
|
||||
let mut cursor: usize = 0;
|
||||
let id = Self::unmarshal(buf.as_ref().unwrap(), &mut cursor)?;
|
||||
Ok((id, cursor))
|
||||
}
|
||||
let mut cursor: usize = 0;
|
||||
let id = Self::unmarshal(&Buffer::<2048>::from_bytes(bytes)?, &mut cursor)?;
|
||||
Ok((id, cursor))
|
||||
}
|
||||
|
||||
/// Get this identity in string format, including its secret keys.
|
||||
|
|
|
@ -5,13 +5,13 @@ use std::time::Duration;
|
|||
use dashmap::DashMap;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::crypto::random::SecureRandom;
|
||||
use crate::crypto::random::{SecureRandom, next_u64_secure};
|
||||
use crate::error::InvalidParameterError;
|
||||
use crate::util::gate::IntervalGate;
|
||||
use crate::util::pool::{Pool, Pooled};
|
||||
use crate::vl1::{Address, Endpoint, Identity, Locator};
|
||||
use crate::vl1::buffer::{Buffer, PooledBufferFactory};
|
||||
use crate::vl1::constants::PACKET_SIZE_MAX;
|
||||
use crate::vl1::constants::{PACKET_SIZE_MAX, FORWARD_MAX_HOPS};
|
||||
use crate::vl1::path::Path;
|
||||
use crate::vl1::peer::Peer;
|
||||
use crate::vl1::protocol::*;
|
||||
|
@ -117,15 +117,17 @@ pub trait VL1PacketHandler {
|
|||
struct BackgroundTaskIntervals {
|
||||
whois: IntervalGate<{ WhoisQueue::INTERVAL }>,
|
||||
paths: IntervalGate<{ Path::INTERVAL }>,
|
||||
peers: IntervalGate<{ Peer::INTERVAL }>,
|
||||
}
|
||||
|
||||
pub struct Node {
|
||||
instance_id: u64,
|
||||
identity: Identity,
|
||||
intervals: Mutex<BackgroundTaskIntervals>,
|
||||
locator: Mutex<Option<Locator>>,
|
||||
paths: DashMap<Endpoint, Arc<Path>>,
|
||||
peers: DashMap<Address, Arc<Peer>>,
|
||||
root: Mutex<Option<Arc<Peer>>>,
|
||||
roots: Mutex<Vec<Arc<Peer>>>,
|
||||
whois: WhoisQueue,
|
||||
buffer_pool: Pool<Buffer<{ PACKET_SIZE_MAX }>, PooledBufferFactory<{ PACKET_SIZE_MAX }>>,
|
||||
secure_prng: SecureRandom,
|
||||
|
@ -158,12 +160,13 @@ impl Node {
|
|||
};
|
||||
|
||||
Ok(Self {
|
||||
instance_id: next_u64_secure(),
|
||||
identity: id,
|
||||
intervals: Mutex::new(BackgroundTaskIntervals::default()),
|
||||
locator: Mutex::new(None),
|
||||
paths: DashMap::new(),
|
||||
peers: DashMap::new(),
|
||||
root: Mutex::new(None),
|
||||
roots: Mutex::new(Vec::new()),
|
||||
whois: WhoisQueue::new(),
|
||||
buffer_pool: Pool::new(64, PooledBufferFactory),
|
||||
secure_prng: SecureRandom::get(),
|
||||
|
@ -213,9 +216,19 @@ impl Node {
|
|||
if intervals.whois.gate(tt) {
|
||||
self.whois.on_interval(self, ci, tt);
|
||||
}
|
||||
|
||||
if intervals.paths.gate(tt) {
|
||||
self.paths.retain(|_, path| {
|
||||
path.on_interval(ci, tt);
|
||||
todo!();
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
if intervals.peers.gate(tt) {
|
||||
self.peers.retain(|_, peer| {
|
||||
peer.on_interval(ci, tt);
|
||||
todo!();
|
||||
true
|
||||
});
|
||||
}
|
||||
|
@ -231,8 +244,10 @@ impl Node {
|
|||
let time_ticks = ci.time_ticks();
|
||||
let dest = Address::from(&fragment_header.dest);
|
||||
if dest == self.identity.address() {
|
||||
|
||||
let path = self.path(source_endpoint, source_local_socket, source_local_interface);
|
||||
if fragment_header.is_fragment() {
|
||||
|
||||
let _ = path.receive_fragment(fragment_header.id, fragment_header.fragment_no(), fragment_header.total_fragments(), data, time_ticks).map(|assembled_packet| {
|
||||
if assembled_packet.frags[0].is_some() {
|
||||
let frag0 = assembled_packet.frags[0].as_ref().unwrap();
|
||||
|
@ -249,7 +264,9 @@ impl Node {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
path.receive_other(time_ticks);
|
||||
let packet_header = data.struct_at::<PacketHeader>(0);
|
||||
if packet_header.is_ok() {
|
||||
|
@ -262,16 +279,34 @@ impl Node {
|
|||
self.whois.query(self, ci, source, Some(QueuedPacket::Singular(data)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if fragment_header.is_fragment() {
|
||||
if fragment_header.increment_hops() > FORWARD_MAX_HOPS {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
let packet_header = data.struct_mut_at::<PacketHeader>(0);
|
||||
if packet_header.is_ok() {
|
||||
if packet_header.unwrap().increment_hops() > FORWARD_MAX_HOPS {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let _ = self.peer(dest).map(|peer| peer.forward(ci, time_ticks, data));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current best root peer that we should use for WHOIS, relaying, etc.
|
||||
#[inline(always)]
|
||||
pub(crate) fn root(&self) -> Option<Arc<Peer>> {
|
||||
self.root.lock().clone()
|
||||
self.roots.lock().first().map(|p| p.clone())
|
||||
}
|
||||
|
||||
/// Get the canonical Path object for a given endpoint and local socket information.
|
||||
|
|
|
@ -71,14 +71,7 @@ impl Path {
|
|||
}
|
||||
}
|
||||
|
||||
let frag = fp.entry(packet_id).or_insert_with(|| FragmentedPacket {
|
||||
ts_ticks: time_ticks,
|
||||
frags: [None, None, None, None, None, None, None, None],
|
||||
have: 0,
|
||||
expecting: 0,
|
||||
});
|
||||
|
||||
if frag.add_fragment(packet, fragment_no, fragment_expecting_count) {
|
||||
if fp.entry(packet_id).or_insert_with(|| FragmentedPacket::new(time_ticks)).add_fragment(packet, fragment_no, fragment_expecting_count) {
|
||||
fp.remove(&packet_id)
|
||||
} else {
|
||||
None
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::sync::Arc;
|
|||
use std::sync::atomic::{AtomicI64, AtomicU64, AtomicU8, Ordering};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use aes_gmac_siv::AesGmacSiv;
|
||||
use aes_gmac_siv::{AesGmacSiv, AesCtr};
|
||||
|
||||
use crate::crypto::c25519::C25519KeyPair;
|
||||
use crate::crypto::kbkdf::zt_kbkdf_hmac_sha384;
|
||||
|
@ -23,7 +23,7 @@ struct AesGmacSivPoolFactory(Secret<48>, Secret<48>);
|
|||
impl PoolFactory<AesGmacSiv> for AesGmacSivPoolFactory {
|
||||
#[inline(always)]
|
||||
fn create(&self) -> AesGmacSiv {
|
||||
AesGmacSiv::new(self.0.as_ref(), self.1.as_ref())
|
||||
AesGmacSiv::new(&self.0.0[0..32], &self.1.0[0..32])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
|
@ -43,7 +43,7 @@ struct PeerSecret {
|
|||
secret: Secret<48>,
|
||||
|
||||
// Reusable AES-GMAC-SIV ciphers initialized with secret.
|
||||
// These can't be used concurrently so they're pooled to allow multithreaded use.
|
||||
// These can't be used concurrently so they're pooled to allow low-contention concurrency.
|
||||
aes: Pool<AesGmacSiv, AesGmacSivPoolFactory>,
|
||||
}
|
||||
|
||||
|
@ -71,8 +71,8 @@ pub struct Peer {
|
|||
// Static shared secret computed from agreement with identity.
|
||||
static_secret: PeerSecret,
|
||||
|
||||
// Derived static secret used to encrypt the dictionary part of HELLO.
|
||||
static_secret_hello_dictionary_encrypt: Secret<48>,
|
||||
// Derived static secret (in initialized cipher) used to encrypt the dictionary part of HELLO.
|
||||
static_secret_hello_dictionary: Mutex<AesCtr>,
|
||||
|
||||
// Derived static secret used to add full HMAC-SHA384 to packets, currently just HELLO.
|
||||
static_secret_packet_hmac: Secret<48>,
|
||||
|
@ -83,11 +83,18 @@ pub struct Peer {
|
|||
// Either None or the current ephemeral key pair whose public keys are on offer.
|
||||
ephemeral_pair: Mutex<Option<EphemeralKeyPair>>,
|
||||
|
||||
// Statistics
|
||||
// Paths sorted in ascending order of quality / preference.
|
||||
paths: Mutex<Vec<Arc<Path>>>,
|
||||
|
||||
// Statistics and times of events.
|
||||
last_send_time_ticks: AtomicI64,
|
||||
last_receive_time_ticks: AtomicI64,
|
||||
last_forward_time_ticks: AtomicI64,
|
||||
total_bytes_sent: AtomicU64,
|
||||
total_bytes_sent_indirect: AtomicU64,
|
||||
total_bytes_received: AtomicU64,
|
||||
total_bytes_received_indirect: AtomicU64,
|
||||
total_bytes_forwarded: AtomicU64,
|
||||
|
||||
// Counter for assigning packet IV's a.k.a. PacketIDs.
|
||||
packet_iv_counter: AtomicU64,
|
||||
|
@ -95,9 +102,6 @@ pub struct Peer {
|
|||
// Remote peer version information.
|
||||
remote_version: AtomicU64,
|
||||
remote_protocol_version: AtomicU8,
|
||||
|
||||
// Paths sorted in ascending order of quality / preference.
|
||||
paths: Mutex<Vec<Arc<Path>>>,
|
||||
}
|
||||
|
||||
/// Derive per-packet key for Sals20/12 encryption (and Poly1305 authentication).
|
||||
|
@ -108,24 +112,21 @@ pub struct Peer {
|
|||
/// is different the key will be wrong and MAC will fail.
|
||||
///
|
||||
/// This is only used for Salsa/Poly modes.
|
||||
#[inline(always)]
|
||||
fn salsa_derive_per_packet_key(key: &Secret<48>, header: &PacketHeader, packet_size: usize) -> Secret<48> {
|
||||
let hb = header.as_bytes();
|
||||
let mut k = key.clone();
|
||||
|
||||
for i in 0..18 {
|
||||
k.0[i] ^= hb[i];
|
||||
}
|
||||
|
||||
k.0[18] ^= hb[HEADER_FLAGS_FIELD_INDEX] & HEADER_FLAGS_FIELD_MASK_HIDE_HOPS;
|
||||
|
||||
k.0[19] ^= (packet_size >> 8) as u8;
|
||||
k.0[20] ^= packet_size as u8;
|
||||
|
||||
k
|
||||
}
|
||||
|
||||
impl Peer {
|
||||
pub(crate) const INTERVAL: i64 = PEER_SERVICE_INTERVAL;
|
||||
|
||||
/// Create a new peer.
|
||||
/// This only returns None if this_node_identity does not have its secrets or if some
|
||||
/// fatal error occurs performing key agreement between the two identities.
|
||||
|
@ -134,7 +135,7 @@ impl Peer {
|
|||
let aes_factory = AesGmacSivPoolFactory(
|
||||
zt_kbkdf_hmac_sha384(&static_secret.0, KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K0, 0, 0),
|
||||
zt_kbkdf_hmac_sha384(&static_secret.0, KBKDF_KEY_USAGE_LABEL_AES_GMAC_SIV_K1, 0, 0));
|
||||
let static_secret_hello_dictionary_encrypt = zt_kbkdf_hmac_sha384(&static_secret.0, KBKDF_KEY_USAGE_LABEL_HELLO_DICTIONARY_ENCRYPT, 0, 0);
|
||||
let static_secret_hello_dictionary = zt_kbkdf_hmac_sha384(&static_secret.0, KBKDF_KEY_USAGE_LABEL_HELLO_DICTIONARY_ENCRYPT, 0, 0);
|
||||
let static_secret_packet_hmac = zt_kbkdf_hmac_sha384(&static_secret.0, KBKDF_KEY_USAGE_LABEL_PACKET_HMAC, 0, 0);
|
||||
Peer {
|
||||
identity: id,
|
||||
|
@ -144,18 +145,22 @@ impl Peer {
|
|||
secret: static_secret,
|
||||
aes: Pool::new(4, aes_factory),
|
||||
},
|
||||
static_secret_hello_dictionary_encrypt,
|
||||
static_secret_hello_dictionary: Mutex::new(AesCtr::new(&static_secret_hello_dictionary.0[0..32])),
|
||||
static_secret_packet_hmac,
|
||||
ephemeral_secret: Mutex::new(None),
|
||||
ephemeral_pair: Mutex::new(None),
|
||||
paths: Mutex::new(Vec::new()),
|
||||
last_send_time_ticks: AtomicI64::new(0),
|
||||
last_receive_time_ticks: AtomicI64::new(0),
|
||||
last_forward_time_ticks: AtomicI64::new(0),
|
||||
total_bytes_sent: AtomicU64::new(0),
|
||||
total_bytes_sent_indirect: AtomicU64::new(0),
|
||||
total_bytes_received: AtomicU64::new(0),
|
||||
total_bytes_received_indirect: AtomicU64::new(0),
|
||||
total_bytes_forwarded: AtomicU64::new(0),
|
||||
packet_iv_counter: AtomicU64::new(next_u64_secure()),
|
||||
remote_version: AtomicU64::new(0),
|
||||
remote_protocol_version: AtomicU8::new(0),
|
||||
paths: Mutex::new(Vec::new()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -166,6 +171,7 @@ impl Peer {
|
|||
pub(crate) fn receive<CI: VL1CallerInterface, PH: VL1PacketHandler>(&self, node: &Node, ci: &CI, ph: &PH, time_ticks: i64, source_path: &Arc<Path>, header: &PacketHeader, packet: &Buffer<{ PACKET_SIZE_MAX }>, fragments: &[Option<PacketBuffer>]) {
|
||||
let _ = packet.as_bytes_starting_at(PACKET_VERB_INDEX).map(|packet_frag0_payload_bytes| {
|
||||
let mut payload: Buffer<{ PACKET_SIZE_MAX }> = Buffer::new();
|
||||
|
||||
let cipher = header.cipher();
|
||||
let mut forward_secrecy = true;
|
||||
let ephemeral_secret = self.ephemeral_secret.lock().clone();
|
||||
|
@ -245,9 +251,10 @@ impl Peer {
|
|||
} else {
|
||||
// If ephemeral failed, static secret will be tried. Set forward secrecy to false.
|
||||
forward_secrecy = false;
|
||||
payload.clear();
|
||||
let _ = payload.set_size(0);
|
||||
}
|
||||
}
|
||||
drop(ephemeral_secret);
|
||||
|
||||
// If decryption and authentication succeeded, the code above will break out of the
|
||||
// for loop and end up here. Otherwise it returns from the whole function.
|
||||
|
@ -260,16 +267,15 @@ impl Peer {
|
|||
// if it didn't handle the packet, in which case it's handled at VL1.
|
||||
if !ph.handle_packet(self, source_path, forward_secrecy, verb, &payload) {
|
||||
match verb {
|
||||
VERB_VL1_NOP => {}
|
||||
VERB_VL1_HELLO => {}
|
||||
VERB_VL1_ERROR => {}
|
||||
VERB_VL1_OK => {}
|
||||
VERB_VL1_WHOIS => {}
|
||||
VERB_VL1_RENDEZVOUS => {}
|
||||
VERB_VL1_ECHO => {}
|
||||
VERB_VL1_PUSH_DIRECT_PATHS => {}
|
||||
VERB_VL1_USER_MESSAGE => {}
|
||||
VERB_VL1_REMOTE_TRACE => {}
|
||||
//VERB_VL1_NOP => {}
|
||||
VERB_VL1_HELLO => self.receive_hello(ci, node, time_ticks, source_path, &payload),
|
||||
VERB_VL1_ERROR => self.receive_error(ci, node, time_ticks, source_path, &payload),
|
||||
VERB_VL1_OK => self.receive_ok(ci, node, time_ticks, source_path, &payload),
|
||||
VERB_VL1_WHOIS => self.receive_whois(ci, node, time_ticks, source_path, &payload),
|
||||
VERB_VL1_RENDEZVOUS => self.receive_rendezvous(ci, node, time_ticks, source_path, &payload),
|
||||
VERB_VL1_ECHO => self.receive_echo(ci, node, time_ticks, source_path, &payload),
|
||||
VERB_VL1_PUSH_DIRECT_PATHS => self.receive_push_direct_paths(ci, node, time_ticks, source_path, &payload),
|
||||
VERB_VL1_USER_MESSAGE => self.receive_user_message(ci, node, time_ticks, source_path, &payload),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -277,7 +283,26 @@ impl Peer {
|
|||
});
|
||||
}
|
||||
|
||||
pub(crate) fn send_hello<CI: VL1CallerInterface>(&self, ci: &CI, to_endpoint: &Endpoint) {
|
||||
/// Send a packet to this peer.
|
||||
///
|
||||
/// This will go directly if there is an active path, or otherwise indirectly
|
||||
/// via a root or some other route.
|
||||
pub(crate) fn send<CI: VL1CallerInterface>(&self, ci: &CI, time_ticks: i64, data: PacketBuffer) {
|
||||
self.last_send_time_ticks.store(time_ticks, Ordering::Relaxed);
|
||||
let _ = self.total_bytes_sent.fetch_add(data.len() as u64, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Forward a packet to this peer.
|
||||
///
|
||||
/// This is called when we receive a packet not addressed to this node and
|
||||
/// want to pass it along.
|
||||
///
|
||||
/// This doesn't support fragmenting since fragments are forwarded individually.
|
||||
/// Intermediates don't need to adjust fragmentation.
|
||||
pub(crate) fn forward<CI: VL1CallerInterface>(&self, ci: &CI, time_ticks: i64, data: PacketBuffer) {
|
||||
self.last_forward_time_ticks.store(time_ticks, Ordering::Relaxed);
|
||||
let _ = self.total_bytes_forwarded.fetch_add(data.len() as u64, Ordering::Relaxed);
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Get the remote version of this peer: major, minor, revision, and build.
|
||||
|
@ -300,4 +325,41 @@ impl Peer {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Called every INTERVAL during background tasks.
|
||||
#[inline(always)]
|
||||
pub fn on_interval<CI: VL1CallerInterface>(&self, ct: &CI, time_ticks: i64) {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn receive_hello<CI: VL1CallerInterface>(&self, ci: &CI, node: &Node, time_ticks: i64, source_path: &Arc<Path>, packet: &Buffer<{ PACKET_SIZE_MAX }>) {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn receive_error<CI: VL1CallerInterface>(&self, ci: &CI, node: &Node, time_ticks: i64, source_path: &Arc<Path>, packet: &Buffer<{ PACKET_SIZE_MAX }>) {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn receive_ok<CI: VL1CallerInterface>(&self, ci: &CI, node: &Node, time_ticks: i64, source_path: &Arc<Path>, packet: &Buffer<{ PACKET_SIZE_MAX }>) {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn receive_whois<CI: VL1CallerInterface>(&self, ci: &CI, node: &Node, time_ticks: i64, source_path: &Arc<Path>, packet: &Buffer<{ PACKET_SIZE_MAX }>) {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn receive_rendezvous<CI: VL1CallerInterface>(&self, ci: &CI, node: &Node, time_ticks: i64, source_path: &Arc<Path>, packet: &Buffer<{ PACKET_SIZE_MAX }>) {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn receive_echo<CI: VL1CallerInterface>(&self, ci: &CI, node: &Node, time_ticks: i64, source_path: &Arc<Path>, packet: &Buffer<{ PACKET_SIZE_MAX }>) {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn receive_push_direct_paths<CI: VL1CallerInterface>(&self, ci: &CI, node: &Node, time_ticks: i64, source_path: &Arc<Path>, packet: &Buffer<{ PACKET_SIZE_MAX }>) {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn receive_user_message<CI: VL1CallerInterface>(&self, ci: &CI, node: &Node, time_ticks: i64, source_path: &Arc<Path>, packet: &Buffer<{ PACKET_SIZE_MAX }>) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ pub const VERB_VL1_RENDEZVOUS: u8 = 0x05;
|
|||
pub const VERB_VL1_ECHO: u8 = 0x08;
|
||||
pub const VERB_VL1_PUSH_DIRECT_PATHS: u8 = 0x10;
|
||||
pub const VERB_VL1_USER_MESSAGE: u8 = 0x14;
|
||||
pub const VERB_VL1_REMOTE_TRACE: u8 = 0x15;
|
||||
|
||||
/// A unique packet identifier, also the cryptographic nonce.
|
||||
///
|
||||
|
@ -50,9 +49,11 @@ impl PacketHeader {
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn increment_hops(&mut self) {
|
||||
pub fn increment_hops(&mut self) -> u8 {
|
||||
let f = self.flags_cipher_hops;
|
||||
self.flags_cipher_hops = (f & HEADER_FLAGS_FIELD_MASK_HIDE_HOPS) | ((f + 1) & HEADER_FLAGS_FIELD_MASK_HOPS);
|
||||
let h = (f + 1) & HEADER_FLAGS_FIELD_MASK_HOPS;
|
||||
self.flags_cipher_hops = (f & HEADER_FLAGS_FIELD_MASK_HIDE_HOPS) | h;
|
||||
h
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
|
@ -137,9 +138,11 @@ impl FragmentHeader {
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn increment_hops(&mut self) {
|
||||
pub fn increment_hops(&mut self) -> u8 {
|
||||
let f = self.reserved_hops;
|
||||
self.reserved_hops = (f & HEADER_FLAGS_FIELD_MASK_HIDE_HOPS) | ((f + 1) & HEADER_FLAGS_FIELD_MASK_HOPS);
|
||||
let h = (f + 1) & HEADER_FLAGS_FIELD_MASK_HOPS;
|
||||
self.reserved_hops = (f & HEADER_FLAGS_FIELD_MASK_HIDE_HOPS) | h;
|
||||
h
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
|
|
208
protocol.md
Normal file
208
protocol.md
Normal file
|
@ -0,0 +1,208 @@
|
|||
GVSP (ZeroTier) Protocol Specification
|
||||
------
|
||||
|
||||
*(c) 2021 ZeroTier, Inc.*
|
||||
|
||||
# Introduction
|
||||
|
||||
The ZeroTier protocol, or *Global Virtual Switch Protocol* (GVSP), provides what is essentially a planetary scale Ethernet switch with VLAN support and sophisticated security monitoring, access control, and traffic control capabilities. Its purpose is to provide a single software defined networking layer that can carry almost any protocol and that achieves the goals of numerous discrete products and services in a single system.
|
||||
|
||||
GVSP is organized into two closely coupled but separate layers: VL1 and VL2.
|
||||
|
||||
VL1 (Virtual Layer 1) provides a global scale link layer in the form of a secure cryptographically addressed peer to peer protocol. VL1 only concerns itself with efficiently moving packets and providing transport level encryption and authentication. Its operation could be compared to the transport security aspects of IPSec, WireGuard, or OpenSSL, but with a global address space. VL1 exposes its cryptographic tokens, identifiers, assurances, and primitives to higher layers, which makes implementation of security features at VL2 considerably easier.
|
||||
|
||||
VL2 (Virtual Layer 2) is an Ethernet emulation layer somewhat similar to VXLAN. It provides virtual Ethernet enclaves with certificate access control, globally configurable rules, and numerous other features normally only found in enterprise smart switches and SDN systems. Placing an Ethernet virtualization protocol on top of a global peer to peer protocol creates a kind of "planetary data center" or "planetary cloud region."
|
||||
|
||||
## VL1: Virtual Layer 1 (Cryptogaphically Addressed P2P Link Layer)
|
||||
|
||||
### Identities and Addressing
|
||||
|
||||
#### Identity Type 0: Curve25519 ECDH / Ed25519 EDDSA
|
||||
|
||||
#### Identity Type 1: Curve25519 ECDH + NIST P-521 ECDH / Ed25519 EDDSA + NIST P-521 ECDSA
|
||||
|
||||
### Packet Structure
|
||||
|
||||
### Fragmentation
|
||||
|
||||
### Encryption and Authentication (AEAD)
|
||||
|
||||
### Forward Secrecy (Ephemeral Keys)
|
||||
|
||||
#### AES-GMAC-SIV
|
||||
|
||||
#### Salsa20/12-Poly1305 (deprecated)
|
||||
|
||||
#### NONE/Poly1305 (used with HELLO, otherwise deprecated)
|
||||
|
||||
In this mode Poly1305 is initialized as it is with Salsa20/12-Poly1305 but is then used to authenticate the payload as plaintext. The payload is not encrypted, but a Poly1305 MAC that can be checked by the receiver is still generated.
|
||||
|
||||
### Root Nodes
|
||||
|
||||
### Establishment of Peer to Peer Connectivity
|
||||
|
||||
### Root Fall-Back and Re-Establishment of Connectivity on Failure
|
||||
|
||||
### Packet Types ("Verbs")
|
||||
|
||||
#### 0x00 / NOP
|
||||
|
||||
NOP, as the name suggests, does nothing. Any payload is ignored.
|
||||
|
||||
#### 0x01 / HELLO
|
||||
|
||||
| [Size] Type | Description |
|
||||
| ------------- | ------------------------------------------------- |
|
||||
| [1] u8 | Protocol version |
|
||||
| [1] u8 | Software major version (0 if unspecified) |
|
||||
| [1] u8 | Software minor version (0 if unspecified) |
|
||||
| [2] u16 | Software revision (0 if unspecified) |
|
||||
| [8] u64 | Timestamp (milliseconds since epoch) |
|
||||
| Identity | Binary serialized sender identity |
|
||||
| Endpoint | Physical endpoint to which HELLO was sent |
|
||||
| [12] [u8; 12] | 96-bit IV for CTR-encrypted section |
|
||||
| [6] [u8; 6] | *(reserved)* (contains legacy compatibility data) |
|
||||
| -- | -- START of AES-256-CTR encrypted section -- |
|
||||
| [2] u16 | Length of encrypted Dictionary in bytes |
|
||||
| Dictionary | Key/value dictionary containing additional fields |
|
||||
| -- | -- END of AES-256-CTR encrypted section -- |
|
||||
| [48] [u8; 48] | HMAC-SHA384 extended strength MAC |
|
||||
|
||||
HELLO establishes a full session with another peer and carries information such as protocol and software versions, the full identity of the peer, and ephemeral keys for forward secrecy. Without a HELLO exchange only limited communication with the most conservative assumptions is possible, and communication without a session may be completely removed in the future. (It's only allowed now for backward compatibility with ZeroTier 1.x, and must be disabled in FIPS mode.)
|
||||
|
||||
HELLO is sent without payload encryption. Poly1305 MAC alone is applied to the plaintext. The OK(HELLO) reply is sent with normal payload encryption.
|
||||
|
||||
Legacy Poly1305 MAC is always used when sending HELLO for backward compatiblity with version 1.4 and earlier, since we don't know the remote peer's version until the HELLO exchange is complete. Version 2.0 and newer add HMAC-SHA384 authentication to the HELLO exchange to provide much stronger (and FIPS compliant) authentication for session establishment.
|
||||
|
||||
HELLO and OK(HELLO) are always sent using the static secret (no forward secrecy), since the HELLO exchange is the mechanism by which ephemeral keys are negotiated. There is no information in HELLO or OK(HELLO) packets that would have significant impact if the static secret were compromised.
|
||||
|
||||
These messages contain a dictionary that is used for a variety of additional fields. Since HELLO is sent in the clear, this section is encrypted in HELLO packets with AES-256-CTR. The key for AES is derived from the static secret using KBKDF (label `H`), and a 96-bit random IV is included as well. The dictionary in OK(HELLO) doesn't need this treatment as OK is encrypted normally. Encrypting the dictionary is only a defense in depth measure. It would technically be safe to send it in the clear, but it can contain system and node information that it's prudent to avoid revealing.
|
||||
|
||||
OK(HELLO) response payload, which must be sent if the HELLO receipient wishes to communicate:
|
||||
|
||||
| Type(s) | Description |
|
||||
| ------------- | ------------------------------------------------- |
|
||||
| [8] u64 | HELLO timestamp (echoed to determine latency) |
|
||||
| [1] u8 | Responding node protocol version |
|
||||
| [1] u8 | Responding node major version (0 if unspecified) |
|
||||
| [1] u8 | Responding node minor version (0 if unspecified) |
|
||||
| [2] u16 | Responding node revision (0 if unspecified) |
|
||||
| Endpoint | Physical endpoint where OK(HELLO) was sent |
|
||||
| [2] u16 | *(reserved)* (set to zero for legacy reasons) |
|
||||
| [2] u16 | Length of encrypted Dictionary in bytes |
|
||||
| Dictionary | Key/value dictionary containing additional fields |
|
||||
| [48] [u8; 48] | HMAC-SHA384 extended strength MAC |
|
||||
|
||||
HMAC-SHA384 authentication is computed over the payload of HELLO and OK(HELLO). For HELLO it is computed after AES-256-CTR encryption is applied to the dictionary section. and is checked before anything is done with a payload. For OK(HELLO) it is computed prior to normal packet armoring and is itself included in the encrypted payload.
|
||||
|
||||
Recommended dictionary fields in both HELLO and OK(HELLO):
|
||||
|
||||
| Name | Key | Type | Description |
|
||||
| -------------------- | --- | ------------ | ------------------------------------------------ |
|
||||
| INSTANCE_ID | `I` | u64 | Random integer generated at node startup |
|
||||
| CLOCK | `C` | u64 | Clock at sending node (milliseconds since epoch) |
|
||||
| LOCATOR | `L` | Locator | Signed locator for sending node |
|
||||
| EPHEMERAL_C25519 | `E0` | [u8; 32] | Curve25519 ECDH public key |
|
||||
| EPHEMERAL_P521 | `E1` | [u8; 132] | NIST P-521 ECDH public key |
|
||||
|
||||
Dictionary fields that are only meaningful in OK(HELLO):
|
||||
|
||||
| Name | Key | Type | Description |
|
||||
| -------------------- | --- | ------------ | ------------------------------------------------ |
|
||||
| EPHEMERAL_ACK | `e` | [u8; 48] | SHA384(KBKDF(shared secret, label: `A`)) |
|
||||
| HELLO_ORIGIN | `@` | Endpoint | Endpoint from which HELLO was received |
|
||||
|
||||
Optional dictionary fields that can be included in either HELLO or OK(HELLO):
|
||||
|
||||
| Name | Key | Type | Description |
|
||||
| -------------------- | --- | ------------ | ------------------------------------------------------------ |
|
||||
| SYS_ARCH | `Sa` | string | Host architecture (e.g. x86_64, aarch64) |
|
||||
| SYS_BITS | `Sb` | u64 | sizeof(pointer), e.g. 32 or 64 |
|
||||
| OS_NAME | `On` | string | Name of host operating system |
|
||||
| OS_VERSION | `Ov` | string | Operating system version |
|
||||
| VENDOR | `V` | string | Node software vendor if not ZeroTier, Inc. |
|
||||
| FLAGS | `+` | string | Flags (see below) |
|
||||
|
||||
FLAGS is a string that can contain the following boolean flags: `F` to indicate that the node is running in FIPS compliant mode, and `w` to indicate that the node is a "wimp." "Wimpy" nodes are things like mobile phones, and this flag can be used to exempt these devices from selection for any intensive role (such as use in VL2 to propagate multicasts).
|
||||
|
||||
#### 0x02 / ERROR
|
||||
|
||||
| [Size] Type | Description |
|
||||
| ------------- | ------------------------------------------------- |
|
||||
| [1] u8 | Verb (message type) that generated the error |
|
||||
| [8] [u8; 8] | Packet ID of packet that generated the error |
|
||||
| [1] u8 | Error code |
|
||||
| ... | Error code dependent payload (if any) |
|
||||
|
||||
If an error is unrelated to any specific packet, both the verb and packet ID field will be zero.
|
||||
|
||||
#### 0x03 / OK
|
||||
|
||||
| [Size] Type | Description |
|
||||
| ------------- | ------------------------------------------------- |
|
||||
| [1] u8 | Verb (message type) that generated the response |
|
||||
| [8] [u8; 8] | Packet ID of packet that generated the response |
|
||||
| ... | Response-specific payload (based on verb) |
|
||||
|
||||
#### 0x04 / WHOIS
|
||||
|
||||
| [Size] Type | Description |
|
||||
| ------------- | ------------------------------------------------- |
|
||||
| ... | One or more 5-byte ZeroTier addresses to look up |
|
||||
|
||||
OK(WHOIS) response payload:
|
||||
|
||||
| [Size] Type | Description |
|
||||
| ------------- | ------------------------------------------------- |
|
||||
| Identity | Identity of address |
|
||||
| [1] bool | If non-zero, a locator is included |
|
||||
| Locator | Locator associated with node (if any) |
|
||||
| ... | Additional tuples of identity, [locator] |
|
||||
|
||||
#### 0x05 / RENDEZVOUS
|
||||
|
||||
| [Size] Type | Description |
|
||||
| ------------- | ------------------------------------------------- |
|
||||
| [1] u8 | Flags, currently unused and always 0 |
|
||||
| [5] Address | ZeroTier address of counter-party (i.e. not you) |
|
||||
| [2] u16 | 16-bit IP port |
|
||||
| [1] u8 | Length of IP address, 4 or 16 |
|
||||
| ... | IP address bytes |
|
||||
|
||||
RENDEZVOUS is only sent by roots, and is sent to help peers engage in time-synchronized UDP hole punching. Since it's only used for that purpose the endpoint type and protocol are assumed to be IP/UDP. (This message also predates the Endpoint type and canonical InetAddress serialized formats, but there's no need to change it as it is only needed for IP/UDP.)
|
||||
|
||||
When received from a trusted root and if the node wishes to communicate with the counter-party, it should send a HELLO to the indicated IP and port. Since the root will send both sides a RENDEZVOUS packet at the same time, it's likely that the two HELLOs sent by the two sides will "pass in the air." This assists with UDP hole punching with some routers.
|
||||
|
||||
RENDEZVOUS should be ignored if it does not come from a trusted root. It does not generate an OK response.
|
||||
|
||||
#### 0x08 / ECHO
|
||||
|
||||
Echo payload is arbitrary and may be (but is not required to be) echoed back in an OK(ECHO) response.
|
||||
|
||||
#### 0x10 / PUSH_DIRECT_PATHS
|
||||
|
||||
| [Size] Type | Description |
|
||||
| ------------- | ------------------------------------------------- |
|
||||
| [2] u16 | Number of endpoints |
|
||||
| ... | One or more Endpoint object(s) |
|
||||
|
||||
PUSH_DIRECT_PATHS is sent by a peer to explicitly suggest to another peer physical endpoints where it may be reached. This is how peers learn of direct LAN connections, uPnP mappings, or other endpoints not handled by RENDEZVOUS. If the receiver wishes to communicate with the sender it may try one or more of these endpoints.
|
||||
|
||||
If the recipient peer is older than ZeroTier 2.0, the following alternate format is used:
|
||||
|
||||
| [Size] Type | Description |
|
||||
| ------------- | ------------------------------------------------- |
|
||||
| [2] u16 | Number of IPs |
|
||||
| | -- One or more of the following record -- |
|
||||
| [3] [u8; 3] | Reserved bytes, always zero |
|
||||
| [1] u8 | Address type (4 or 6) |
|
||||
| [1] u8 | Address length in bytes |
|
||||
| ... | 6-byte IPv4 or 18-byte IPv6 IP/port address |
|
||||
|
||||
Older versions used this older format, and any suggested endpoints are assumed to be IP/UDP of course.
|
||||
|
||||
No OK or ERROR response is generated.
|
||||
|
||||
#### 0x14 / USER_MESSAGE
|
||||
|
||||
## VL2: Virtual Layer 2 (Ethernet Virtualization with Secure Access Control)
|
Loading…
Add table
Reference in a new issue