diff --git a/Cargo.lock b/Cargo.lock index f29ddeac..f0f4d899 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2033,6 +2033,7 @@ dependencies = [ "network-interface", "nix 0.29.0", "once_cell", + "openssl", "parking_lot", "percent-encoding", "petgraph 0.8.1", @@ -5234,6 +5235,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-src" +version = "300.5.2+3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.103" @@ -5242,6 +5252,7 @@ checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index 9a70c7b6..e3a5d8e1 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -150,6 +150,7 @@ boringtun = { package = "boringtun-easytier", version = "0.6.1", optional = true ring = { version = "0.17", optional = true } bitflags = "2.5" aes-gcm = { version = "0.10.3", optional = true } +openssl = { version = "0.10", optional = true, features = ["vendored"] } # for cli tabled = "0.16" @@ -298,6 +299,7 @@ full = [ "websocket", "wireguard", "aes-gcm", + "openssl-crypto", # need openssl-dev libs "smoltcp", "tun", "socks5", @@ -306,6 +308,7 @@ wireguard = ["dep:boringtun", "dep:ring"] quic = ["dep:quinn", "dep:rustls", "dep:rcgen"] mimalloc = ["dep:mimalloc"] aes-gcm = ["dep:aes-gcm"] +openssl-crypto = ["dep:openssl"] tun = ["dep:tun"] websocket = [ "dep:tokio-websockets", diff --git a/easytier/locales/app.yml b/easytier/locales/app.yml index d817a8a9..67a07437 100644 --- a/easytier/locales/app.yml +++ b/easytier/locales/app.yml @@ -95,6 +95,9 @@ core_clap: disable_encryption: en: "disable encryption for peers communication, default is false, must be same with peers" zh-CN: "禁用对等节点通信的加密,默认为false,必须与对等节点相同" + encryption_algorithm: + en: "encryption algorithm to use, supported: '', 'xor', 'chacha20', 'aes-gcm', 'aes-gcm-256', 'openssl-aes128-gcm', 'openssl-aes256-gcm', 'openssl-chacha20'. Empty string means default (aes-gcm)" + zh-CN: "要使用的加密算法,支持:''(默认aes-gcm)、'xor'、'chacha20'、'aes-gcm'、'aes-gcm-256'、'openssl-aes128-gcm'、'openssl-aes256-gcm'、'openssl-chacha20'" multi_thread: en: "use multi-thread runtime, default is single-thread" zh-CN: "使用多线程运行时,默认为单线程" diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index 776f64d0..780a11a6 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -48,9 +48,79 @@ pub fn gen_default_flags() -> Flags { disable_quic_input: false, foreign_relay_bps_limit: u64::MAX, multi_thread_count: 2, + encryption_algorithm: "".to_string(), // 空字符串表示使用默认的 AES-GCM } } +pub enum EncryptionAlgorithm { + AesGcm, + Aes256Gcm, + Xor, + #[cfg(feature = "wireguard")] + ChaCha20, + + #[cfg(feature = "openssl-crypto")] + OpensslAesGcm, + #[cfg(feature = "openssl-crypto")] + OpensslChacha20, + #[cfg(feature = "openssl-crypto")] + OpensslAes256Gcm, +} + +impl std::fmt::Display for EncryptionAlgorithm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::AesGcm => write!(f, "aes-gcm"), + Self::Aes256Gcm => write!(f, "aes-256-gcm"), + Self::Xor => write!(f, "xor"), + #[cfg(feature = "wireguard")] + Self::ChaCha20 => write!(f, "chacha20"), + #[cfg(feature = "openssl-crypto")] + Self::OpensslAesGcm => write!(f, "openssl-aes-gcm"), + #[cfg(feature = "openssl-crypto")] + Self::OpensslChacha20 => write!(f, "openssl-chacha20"), + #[cfg(feature = "openssl-crypto")] + Self::OpensslAes256Gcm => write!(f, "openssl-aes-256-gcm"), + } + } +} + +impl TryFrom<&str> for EncryptionAlgorithm { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + match value { + "aes-gcm" => Ok(Self::AesGcm), + "aes-256-gcm" => Ok(Self::Aes256Gcm), + "xor" => Ok(Self::Xor), + #[cfg(feature = "wireguard")] + "chacha20" => Ok(Self::ChaCha20), + #[cfg(feature = "openssl-crypto")] + "openssl-aes-gcm" => Ok(Self::OpensslAesGcm), + #[cfg(feature = "openssl-crypto")] + "openssl-chacha20" => Ok(Self::OpensslChacha20), + #[cfg(feature = "openssl-crypto")] + "openssl-aes-256-gcm" => Ok(Self::OpensslAes256Gcm), + _ => Err(anyhow::anyhow!("invalid encryption algorithm")), + } + } +} + +pub fn get_avaliable_encrypt_methods() -> Vec<&'static str> { + let mut r = vec!["aes-gcm", "aes-256-gcm", "xor"]; + if cfg!(feature = "wireguard") { + r.push("chacha20"); + } + if cfg!(feature = "openssl-crypto") { + r.extend(vec![ + "openssl-aes-gcm", + "openssl-chacha20", + "openssl-aes-256-gcm", + ]); + } + r +} + #[auto_impl::auto_impl(Box, &)] pub trait ConfigLoader: Send + Sync { fn get_id(&self) -> uuid::Uuid; diff --git a/easytier/src/common/global_ctx.rs b/easytier/src/common/global_ctx.rs index d2a2eae9..8bd68400 100644 --- a/easytier/src/common/global_ctx.rs +++ b/easytier/src/common/global_ctx.rs @@ -296,6 +296,29 @@ impl GlobalCtx { key } + pub fn get_256_key(&self) -> [u8; 32] { + let mut key = [0u8; 32]; + let secret = self + .config + .get_network_identity() + .network_secret + .unwrap_or_default(); + // fill key according to network secret + let mut hasher = DefaultHasher::new(); + hasher.write(secret.as_bytes()); + hasher.write(b"easytier-256bit-key"); // 添加固定盐值以区分128位和256位密钥 + + // 生成32字节密钥 + for i in 0..4 { + let chunk_start = i * 8; + let chunk_end = chunk_start + 8; + hasher.write(&key[0..chunk_start]); + hasher.write(&[i as u8]); // 添加索引以确保每个8字节块都不同 + key[chunk_start..chunk_end].copy_from_slice(&hasher.finish().to_be_bytes()); + } + key + } + pub fn enable_exit_node(&self) -> bool { self.enable_exit_node } diff --git a/easytier/src/easytier-core.rs b/easytier/src/easytier-core.rs index b5d1a799..3507a268 100644 --- a/easytier/src/easytier-core.rs +++ b/easytier/src/easytier-core.rs @@ -18,8 +18,9 @@ use clap_complete::Shell; use easytier::{ common::{ config::{ - ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader, - NetworkIdentity, PeerConfig, PortForwardConfig, TomlConfigLoader, VpnPortalConfig, + get_avaliable_encrypt_methods, ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, + LoggingConfigLoader, NetworkIdentity, PeerConfig, PortForwardConfig, TomlConfigLoader, + VpnPortalConfig, }, constants::EASYTIER_VERSION, global_ctx::GlobalCtx, @@ -283,6 +284,15 @@ struct NetworkOptions { )] disable_encryption: Option, + #[arg( + long, + env = "ET_ENCRYPTION_ALGORITHM", + help = t!("core_clap.encryption_algorithm").to_string(), + default_value = "aes-gcm", + value_parser = get_avaliable_encrypt_methods() + )] + encryption_algorithm: Option, + #[arg( long, env = "ET_MULTI_THREAD", @@ -846,6 +856,9 @@ impl NetworkOptions { if let Some(v) = self.disable_encryption { f.enable_encryption = !v; } + if let Some(algorithm) = &self.encryption_algorithm { + f.encryption_algorithm = algorithm.clone(); + } if let Some(v) = self.disable_ipv6 { f.enable_ipv6 = !v; } @@ -894,7 +907,9 @@ impl NetworkOptions { .unwrap_or(f.foreign_relay_bps_limit); f.multi_thread_count = self.multi_thread_count.unwrap_or(f.multi_thread_count); f.disable_relay_kcp = self.disable_relay_kcp.unwrap_or(f.disable_relay_kcp); - f.enable_relay_foreign_network_kcp = self.enable_relay_foreign_network_kcp.unwrap_or(f.enable_relay_foreign_network_kcp); + f.enable_relay_foreign_network_kcp = self + .enable_relay_foreign_network_kcp + .unwrap_or(f.enable_relay_foreign_network_kcp); cfg.set_flags(f); if !self.exit_nodes.is_empty() { diff --git a/easytier/src/peers/encrypt/mod.rs b/easytier/src/peers/encrypt/mod.rs index 9bdb485f..8b7e468d 100644 --- a/easytier/src/peers/encrypt/mod.rs +++ b/easytier/src/peers/encrypt/mod.rs @@ -1,11 +1,21 @@ -use crate::tunnel::packet_def::ZCPacket; +use std::sync::Arc; + +use crate::{common::config::EncryptionAlgorithm, tunnel::packet_def::ZCPacket}; #[cfg(feature = "wireguard")] pub mod ring_aes_gcm; +#[cfg(feature = "wireguard")] +pub mod ring_chacha20; + #[cfg(feature = "aes-gcm")] pub mod aes_gcm; +#[cfg(feature = "openssl-crypto")] +pub mod openssl_cipher; + +pub mod xor_cipher; + #[derive(thiserror::Error, Debug)] pub enum Error { #[error("packet is too short. len: {0}")] @@ -39,3 +49,70 @@ impl Encryptor for NullCipher { } } } + +/// Create an encryptor based on the algorithm name +pub fn create_encryptor( + algorithm: &str, + key_128: [u8; 16], + key_256: [u8; 32], +) -> Arc { + let algorithm = match EncryptionAlgorithm::try_from(algorithm) { + Ok(algorithm) => algorithm, + Err(_) => { + eprintln!( + "Unknown encryption algorithm: {}, falling back to default AES-GCM", + algorithm + ); + EncryptionAlgorithm::AesGcm + } + }; + match algorithm { + EncryptionAlgorithm::AesGcm => { + #[cfg(feature = "wireguard")] + { + Arc::new(ring_aes_gcm::AesGcmCipher::new_128(key_128)) + } + #[cfg(all(feature = "aes-gcm", not(feature = "wireguard")))] + { + Arc::new(aes_gcm::AesGcmCipher::new_128(key_128)) + } + #[cfg(all(not(feature = "wireguard"), not(feature = "aes-gcm")))] + { + compile_error!( + "wireguard or aes-gcm feature must be enabled for default encryption" + ); + } + } + + EncryptionAlgorithm::Aes256Gcm => { + #[cfg(feature = "wireguard")] + { + Arc::new(ring_aes_gcm::AesGcmCipher::new_256(key_256)) + } + #[cfg(all(feature = "aes-gcm", not(feature = "wireguard")))] + { + Arc::new(aes_gcm::AesGcmCipher::new_256(key_256)) + } + } + + EncryptionAlgorithm::Xor => Arc::new(xor_cipher::XorCipher::new(&key_128)), + + #[cfg(feature = "wireguard")] + EncryptionAlgorithm::ChaCha20 => Arc::new(ring_chacha20::RingChaCha20Cipher::new(key_256)), + + #[cfg(feature = "openssl-crypto")] + EncryptionAlgorithm::OpensslAesGcm => { + Arc::new(openssl_cipher::OpenSslCipher::new_aes128_gcm(key_128)) + } + + #[cfg(feature = "openssl-crypto")] + EncryptionAlgorithm::OpensslAes256Gcm => { + Arc::new(openssl_cipher::OpenSslCipher::new_aes256_gcm(key_256)) + } + + #[cfg(feature = "openssl-crypto")] + EncryptionAlgorithm::OpensslChacha20 => { + Arc::new(openssl_cipher::OpenSslCipher::new_chacha20(key_256)) + } + } +} diff --git a/easytier/src/peers/encrypt/openssl_cipher.rs b/easytier/src/peers/encrypt/openssl_cipher.rs new file mode 100644 index 00000000..3730fb30 --- /dev/null +++ b/easytier/src/peers/encrypt/openssl_cipher.rs @@ -0,0 +1,241 @@ +use openssl::symm::{Cipher, Crypter, Mode}; +use rand::RngCore; +use zerocopy::{AsBytes, FromBytes, FromZeroes}; + +use crate::tunnel::packet_def::ZCPacket; + +use crate::peers::encrypt::{Encryptor, Error}; + +// OpenSSL 加密尾部结构 +#[repr(C, packed)] +#[derive(AsBytes, FromBytes, FromZeroes, Clone, Debug, Default)] +pub struct OpenSslTail { + pub nonce: [u8; 16], // 使用 16 字节的 nonce/IV +} + +pub const OPENSSL_ENCRYPTION_RESERVED: usize = std::mem::size_of::(); + +#[derive(Clone)] +pub struct OpenSslCipher { + pub(crate) cipher: OpenSslEnum, +} + +pub enum OpenSslEnum { + Aes128Gcm([u8; 16]), + Aes256Gcm([u8; 32]), + Chacha20([u8; 32]), +} + +impl Clone for OpenSslEnum { + fn clone(&self) -> Self { + match &self { + OpenSslEnum::Aes128Gcm(key) => OpenSslEnum::Aes128Gcm(*key), + OpenSslEnum::Aes256Gcm(key) => OpenSslEnum::Aes256Gcm(*key), + OpenSslEnum::Chacha20(key) => OpenSslEnum::Chacha20(*key), + } + } +} + +impl OpenSslCipher { + pub fn new_aes128_gcm(key: [u8; 16]) -> Self { + Self { + cipher: OpenSslEnum::Aes128Gcm(key), + } + } + + pub fn new_aes256_gcm(key: [u8; 32]) -> Self { + Self { + cipher: OpenSslEnum::Aes256Gcm(key), + } + } + + pub fn new_chacha20(key: [u8; 32]) -> Self { + Self { + cipher: OpenSslEnum::Chacha20(key), + } + } + + fn get_cipher_and_key(&self) -> (Cipher, &[u8]) { + match &self.cipher { + OpenSslEnum::Aes128Gcm(key) => (Cipher::aes_128_gcm(), key.as_slice()), + OpenSslEnum::Aes256Gcm(key) => (Cipher::aes_256_gcm(), key.as_slice()), + OpenSslEnum::Chacha20(key) => (Cipher::chacha20_poly1305(), key.as_slice()), + } + } + + fn is_aead_cipher(&self) -> bool { + matches!( + self.cipher, + OpenSslEnum::Aes128Gcm(_) | OpenSslEnum::Aes256Gcm(_) | OpenSslEnum::Chacha20(_) + ) + } + + fn get_nonce_size(&self) -> usize { + match &self.cipher { + OpenSslEnum::Aes128Gcm(_) | OpenSslEnum::Aes256Gcm(_) | OpenSslEnum::Chacha20(_) => 12, // GCM and ChaCha20-Poly1305 use 12-byte nonce + } + } +} + +impl Encryptor for OpenSslCipher { + fn decrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> { + let pm_header = zc_packet.peer_manager_header().unwrap(); + if !pm_header.is_encrypted() { + return Ok(()); + } + + let payload_len = zc_packet.payload().len(); + if payload_len < OPENSSL_ENCRYPTION_RESERVED { + return Err(Error::PacketTooShort(zc_packet.payload().len())); + } + + let (cipher, key) = self.get_cipher_and_key(); + let is_aead = self.is_aead_cipher(); + let nonce_size = self.get_nonce_size(); + + // 提取 nonce/IV + let openssl_tail = OpenSslTail::ref_from_suffix(zc_packet.payload()) + .unwrap() + .clone(); + + let text_len = if is_aead { + payload_len - OPENSSL_ENCRYPTION_RESERVED - 16 // AEAD 需要减去 tag 长度 + } else { + payload_len - OPENSSL_ENCRYPTION_RESERVED + }; + + let mut decrypter = Crypter::new( + cipher, + Mode::Decrypt, + key, + Some(&openssl_tail.nonce[..nonce_size]), + ) + .map_err(|_| Error::DecryptionFailed)?; + + if is_aead { + // 对于 AEAD 模式,需要设置 tag + let tag_start = text_len; + let tag = &zc_packet.payload()[tag_start..tag_start + 16]; + decrypter + .set_tag(tag) + .map_err(|_| Error::DecryptionFailed)?; + } + + let mut output = vec![0u8; text_len + cipher.block_size()]; + let mut count = decrypter + .update(&zc_packet.payload()[..text_len], &mut output) + .map_err(|_| Error::DecryptionFailed)?; + + count += decrypter + .finalize(&mut output[count..]) + .map_err(|_| Error::DecryptionFailed)?; + + // 更新数据包 + zc_packet.mut_payload()[..count].copy_from_slice(&output[..count]); + let pm_header = zc_packet.mut_peer_manager_header().unwrap(); + pm_header.set_encrypted(false); + let old_len = zc_packet.buf_len(); + let new_len = old_len - (payload_len - count); + zc_packet.mut_inner().truncate(new_len); + + Ok(()) + } + + fn encrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> { + let pm_header = zc_packet.peer_manager_header().unwrap(); + if pm_header.is_encrypted() { + tracing::warn!(?zc_packet, "packet is already encrypted"); + return Ok(()); + } + + let (cipher, key) = self.get_cipher_and_key(); + let is_aead = self.is_aead_cipher(); + let nonce_size = self.get_nonce_size(); + + let mut tail = OpenSslTail::default(); + rand::thread_rng().fill_bytes(&mut tail.nonce[..nonce_size]); + + let mut encrypter = + Crypter::new(cipher, Mode::Encrypt, key, Some(&tail.nonce[..nonce_size])) + .map_err(|_| Error::EncryptionFailed)?; + + let payload_len = zc_packet.payload().len(); + let mut output = vec![0u8; payload_len + cipher.block_size()]; + + let mut count = encrypter + .update(zc_packet.payload(), &mut output) + .map_err(|_| Error::EncryptionFailed)?; + + count += encrypter + .finalize(&mut output[count..]) + .map_err(|_| Error::EncryptionFailed)?; + + // 更新数据包内容 + zc_packet.mut_payload()[..count].copy_from_slice(&output[..count]); + + // 对于 AEAD 模式,添加 tag + if is_aead { + let mut tag = vec![0u8; 16]; // GCM 标签通常是 16 字节 + encrypter + .get_tag(&mut tag) + .map_err(|_| Error::EncryptionFailed)?; + zc_packet.mut_inner().extend_from_slice(&tag); + } + + // 添加 nonce/IV + zc_packet.mut_inner().extend_from_slice(tail.as_bytes()); + + let pm_header = zc_packet.mut_peer_manager_header().unwrap(); + pm_header.set_encrypted(true); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + peers::encrypt::{openssl_cipher::OpenSslCipher, Encryptor}, + tunnel::packet_def::ZCPacket, + }; + + use super::OPENSSL_ENCRYPTION_RESERVED; + + #[test] + fn test_openssl_aes128_gcm() { + let key = [0u8; 16]; + let cipher = OpenSslCipher::new_aes128_gcm(key); + let text = b"Hello, World! This is a test message for OpenSSL AES-128-GCM."; + let mut packet = ZCPacket::new_with_payload(text); + packet.fill_peer_manager_hdr(0, 0, 0); + + // 加密 + cipher.encrypt(&mut packet).unwrap(); + assert!(packet.payload().len() > text.len() + OPENSSL_ENCRYPTION_RESERVED); + assert_eq!(packet.peer_manager_header().unwrap().is_encrypted(), true); + + // 解密 + cipher.decrypt(&mut packet).unwrap(); + assert_eq!(packet.payload(), text); + assert_eq!(packet.peer_manager_header().unwrap().is_encrypted(), false); + } + + #[test] + fn test_openssl_chacha20() { + let key = [0u8; 32]; + let cipher = OpenSslCipher::new_chacha20(key); + let text = b"Hello, World! This is a test message for OpenSSL ChaCha20."; + let mut packet = ZCPacket::new_with_payload(text); + packet.fill_peer_manager_hdr(0, 0, 0); + + // 加密 + cipher.encrypt(&mut packet).unwrap(); + assert!(packet.payload().len() > text.len()); + assert_eq!(packet.peer_manager_header().unwrap().is_encrypted(), true); + + // 解密 + cipher.decrypt(&mut packet).unwrap(); + assert_eq!(packet.payload(), text); + assert_eq!(packet.peer_manager_header().unwrap().is_encrypted(), false); + } +} diff --git a/easytier/src/peers/encrypt/ring_chacha20.rs b/easytier/src/peers/encrypt/ring_chacha20.rs new file mode 100644 index 00000000..39d2eef3 --- /dev/null +++ b/easytier/src/peers/encrypt/ring_chacha20.rs @@ -0,0 +1,125 @@ +use rand::RngCore; +use ring::aead::{self, Aad, LessSafeKey, Nonce, UnboundKey}; +use zerocopy::{AsBytes, FromBytes, FromZeroes}; + +use super::{Encryptor, Error}; +use crate::tunnel::packet_def::ZCPacket; + +#[repr(C, packed)] +#[derive(AsBytes, FromBytes, FromZeroes, Clone, Debug, Default)] +pub struct ChaCha20Poly1305Tail { + pub tag: [u8; 16], + pub nonce: [u8; 12], +} + +pub const CHACHA20_POLY1305_ENCRYPTION_RESERVED: usize = + std::mem::size_of::(); + +#[derive(Clone)] +pub struct RingChaCha20Cipher { + cipher: LessSafeKey, + key: [u8; 32], +} + +impl RingChaCha20Cipher { + pub fn new(key: [u8; 32]) -> Self { + let unbound_key = UnboundKey::new(&aead::CHACHA20_POLY1305, &key).unwrap(); + let cipher = LessSafeKey::new(unbound_key); + Self { cipher, key } + } +} + +impl Encryptor for RingChaCha20Cipher { + fn decrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> { + let pm_header = zc_packet.peer_manager_header().unwrap(); + if !pm_header.is_encrypted() { + return Ok(()); + } + + let payload_len = zc_packet.payload().len(); + if payload_len < CHACHA20_POLY1305_ENCRYPTION_RESERVED { + return Err(Error::PacketTooShort(zc_packet.payload().len())); + } + + let text_and_tag_len = payload_len - CHACHA20_POLY1305_ENCRYPTION_RESERVED + 16; + + let chacha20_tail = ChaCha20Poly1305Tail::ref_from_suffix(zc_packet.payload()).unwrap(); + let nonce = Nonce::assume_unique_for_key(chacha20_tail.nonce.clone()); + + let rs = self.cipher.open_in_place( + nonce, + Aad::empty(), + &mut zc_packet.mut_payload()[..text_and_tag_len], + ); + + if rs.is_err() { + return Err(Error::DecryptionFailed); + } + + let pm_header = zc_packet.mut_peer_manager_header().unwrap(); + pm_header.set_encrypted(false); + let old_len = zc_packet.buf_len(); + zc_packet + .mut_inner() + .truncate(old_len - CHACHA20_POLY1305_ENCRYPTION_RESERVED); + + Ok(()) + } + + fn encrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> { + let pm_header = zc_packet.peer_manager_header().unwrap(); + if pm_header.is_encrypted() { + tracing::warn!(?zc_packet, "packet is already encrypted"); + return Ok(()); + } + + let mut tail = ChaCha20Poly1305Tail::default(); + rand::thread_rng().fill_bytes(&mut tail.nonce); + let nonce = Nonce::assume_unique_for_key(tail.nonce.clone()); + + let rs = + self.cipher + .seal_in_place_separate_tag(nonce, Aad::empty(), zc_packet.mut_payload()); + + match rs { + Ok(tag) => { + tail.tag.copy_from_slice(tag.as_ref()); + let pm_header = zc_packet.mut_peer_manager_header().unwrap(); + pm_header.set_encrypted(true); + zc_packet.mut_inner().extend_from_slice(tail.as_bytes()); + Ok(()) + } + Err(_) => Err(Error::EncryptionFailed), + } + } +} + +#[cfg(test)] +mod tests { + use crate::{ + peers::encrypt::{ring_chacha20::RingChaCha20Cipher, Encryptor}, + tunnel::packet_def::ZCPacket, + }; + + use super::CHACHA20_POLY1305_ENCRYPTION_RESERVED; + + #[test] + fn test_ring_chacha20_cipher() { + let key = [0u8; 32]; + let cipher = RingChaCha20Cipher::new(key); + let text = b"Hello, World! This is a test message for Ring ChaCha20-Poly1305."; + let mut packet = ZCPacket::new_with_payload(text); + packet.fill_peer_manager_hdr(0, 0, 0); + + cipher.encrypt(&mut packet).unwrap(); + assert_eq!( + packet.payload().len(), + text.len() + CHACHA20_POLY1305_ENCRYPTION_RESERVED + ); + assert_eq!(packet.peer_manager_header().unwrap().is_encrypted(), true); + + cipher.decrypt(&mut packet).unwrap(); + assert_eq!(packet.payload(), text); + assert_eq!(packet.peer_manager_header().unwrap().is_encrypted(), false); + } +} diff --git a/easytier/src/peers/encrypt/xor_cipher.rs b/easytier/src/peers/encrypt/xor_cipher.rs new file mode 100644 index 00000000..f7b9528f --- /dev/null +++ b/easytier/src/peers/encrypt/xor_cipher.rs @@ -0,0 +1,86 @@ +use crate::tunnel::packet_def::ZCPacket; + +use super::{Encryptor, Error}; + +// XOR 加密不需要额外的尾部数据,因为它是对称的 +pub const XOR_ENCRYPTION_RESERVED: usize = 0; + +#[derive(Clone)] +pub struct XorCipher { + pub(crate) key: Vec, +} + +impl XorCipher { + pub fn new(key: &[u8]) -> Self { + if key.is_empty() { + panic!("XOR key cannot be empty"); + } + Self { key: key.to_vec() } + } + + fn xor_data(&self, data: &mut [u8]) { + for (i, byte) in data.iter_mut().enumerate() { + *byte ^= self.key[i % self.key.len()]; + } + } +} + +impl Encryptor for XorCipher { + fn decrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> { + let pm_header = zc_packet.peer_manager_header().unwrap(); + if !pm_header.is_encrypted() { + return Ok(()); + } + + // XOR 解密(XOR是对称的,加密和解密操作相同) + self.xor_data(zc_packet.mut_payload()); + + let pm_header = zc_packet.mut_peer_manager_header().unwrap(); + pm_header.set_encrypted(false); + + Ok(()) + } + + fn encrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> { + let pm_header = zc_packet.peer_manager_header().unwrap(); + if pm_header.is_encrypted() { + tracing::warn!(?zc_packet, "packet is already encrypted"); + return Ok(()); + } + + // XOR 加密 + self.xor_data(zc_packet.mut_payload()); + + let pm_header = zc_packet.mut_peer_manager_header().unwrap(); + pm_header.set_encrypted(true); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + peers::encrypt::{xor_cipher::XorCipher, Encryptor}, + tunnel::packet_def::ZCPacket, + }; + + #[test] + fn test_xor_cipher() { + let key = b"test_key_123456"; + let cipher = XorCipher::new(key); + let text = b"Hello, World! This is a test message."; + let mut packet = ZCPacket::new_with_payload(text); + packet.fill_peer_manager_hdr(0, 0, 0); + + // 加密 + cipher.encrypt(&mut packet).unwrap(); + assert_eq!(packet.peer_manager_header().unwrap().is_encrypted(), true); + assert_ne!(packet.payload(), text); // 加密后数据应该不同 + + // 解密 + cipher.decrypt(&mut packet).unwrap(); + assert_eq!(packet.payload(), text); + assert_eq!(packet.peer_manager_header().unwrap().is_encrypted(), false); + } +} diff --git a/easytier/src/peers/peer_manager.rs b/easytier/src/peers/peer_manager.rs index 8c3c3035..0b5d94c2 100644 --- a/easytier/src/peers/peer_manager.rs +++ b/easytier/src/peers/peer_manager.rs @@ -71,7 +71,7 @@ struct RpcTransport { packet_recv: Mutex>, peer_rpc_tspt_sender: UnboundedSender, - encryptor: Arc>, + encryptor: Arc, } #[async_trait::async_trait] @@ -147,7 +147,7 @@ pub struct PeerManager { foreign_network_manager: Arc, foreign_network_client: Arc, - encryptor: Arc>, + encryptor: Arc, data_compress_algo: CompressorAlgo, exit_nodes: Vec, @@ -184,25 +184,18 @@ impl PeerManager { my_peer_id, )); - let mut encryptor: Arc> = Arc::new(Box::new(NullCipher)); - if global_ctx.get_flags().enable_encryption { - #[cfg(feature = "wireguard")] - { - use super::encrypt::ring_aes_gcm::AesGcmCipher; - encryptor = Arc::new(Box::new(AesGcmCipher::new_128(global_ctx.get_128_key()))); - } - - #[cfg(all(feature = "aes-gcm", not(feature = "wireguard")))] - { - use super::encrypt::aes_gcm::AesGcmCipher; - encryptor = Arc::new(Box::new(AesGcmCipher::new_128(global_ctx.get_128_key()))); - } - - #[cfg(all(not(feature = "wireguard"), not(feature = "aes-gcm")))] - { - compile_error!("wireguard or aes-gcm feature must be enabled for encryption"); - } - } + let encryptor = if global_ctx.get_flags().enable_encryption { + // 只有在启用加密时才使用工厂函数选择算法 + let algorithm = &global_ctx.get_flags().encryption_algorithm; + super::encrypt::create_encryptor( + algorithm, + global_ctx.get_128_key(), + global_ctx.get_256_key(), + ) + } else { + // disable_encryption = true 时使用 NullCipher + Arc::new(NullCipher) + }; if global_ctx .check_network_in_whitelist(&global_ctx.get_network_name()) @@ -1110,7 +1103,7 @@ impl PeerManager { pub async fn try_compress_and_encrypt( compress_algo: CompressorAlgo, - encryptor: &Box, + encryptor: &Arc, msg: &mut ZCPacket, ) -> Result<(), Error> { let compressor = DefaultCompressor {}; @@ -1375,9 +1368,12 @@ impl PeerManager { return false; } - let next_hop_policy = Self::get_next_hop_policy( self.global_ctx.get_flags().latency_first); + let next_hop_policy = Self::get_next_hop_policy(self.global_ctx.get_flags().latency_first); // check relay node allow relay kcp. - let Some(next_hop_id) = route.get_next_hop_with_policy(dst_peer_id, next_hop_policy).await else { + let Some(next_hop_id) = route + .get_next_hop_with_policy(dst_peer_id, next_hop_policy) + .await + else { return false; }; @@ -1386,7 +1382,11 @@ impl PeerManager { }; // check next hop allow kcp relay - if next_hop_info.feature_flag.map(|x| x.no_relay_kcp).unwrap_or(false) { + if next_hop_info + .feature_flag + .map(|x| x.no_relay_kcp) + .unwrap_or(false) + { return false; } diff --git a/easytier/src/proto/common.proto b/easytier/src/proto/common.proto index 057bfe18..8e01dc10 100644 --- a/easytier/src/proto/common.proto +++ b/easytier/src/proto/common.proto @@ -49,6 +49,9 @@ message FlagsInConfig { // enable relay foreign network kcp packets bool enable_relay_foreign_network_kcp = 28; + + // encryption algorithm to use, empty string means default (aes-gcm) + string encryption_algorithm = 29; } message RpcDescriptor { diff --git a/easytier/src/tests/three_node.rs b/easytier/src/tests/three_node.rs index 82ee09b5..4c314037 100644 --- a/easytier/src/tests/three_node.rs +++ b/easytier/src/tests/three_node.rs @@ -268,8 +268,40 @@ async fn ping6_test(from_netns: &str, target_ip: &str, payload_size: Option