mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-06 17:59:11 +00:00
fix: refresh ACL groups and enable TCP_NODELAY for WebSocket (#2118)
* fix: refresh ACL groups and enable TCP_NODELAY for WebSocket * add remove_peers to remove list of peer id in ospf route * fix secure tunnel for unreliable udp tunnel * fix(web-client): timeout secure tunnel handshake * fix(web-server): tolerate delayed secure hello * fix quic endpoint panic * fix replay check
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use bytes::BytesMut;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use snow::{Builder, TransportState, params::NoiseParams};
|
||||
use snow::{Builder, params::NoiseParams};
|
||||
|
||||
use crate::{
|
||||
common::config::EncryptionAlgorithm,
|
||||
peers::secure_datagram::{SecureDatagramDirection, SecureDatagramSession},
|
||||
proto::common::TunnelInfo,
|
||||
tunnel::{
|
||||
SplitTunnel, StreamItem, Tunnel, TunnelError, ZCPacketSink, ZCPacketStream,
|
||||
@@ -17,6 +18,11 @@ use crate::{
|
||||
const NOISE_MAGIC: &[u8] = b"ET_WEB_NOISE_V1:";
|
||||
const NOISE_PROLOGUE: &[u8] = b"easytier-webclient-noise-v1";
|
||||
const NOISE_PATTERN: &str = "Noise_NN_25519_ChaChaPoly_SHA256";
|
||||
const WEB_SECURE_CIPHER_ALGORITHM: &str = "aes-gcm";
|
||||
const WEB_SESSION_GENERATION: u32 = 1;
|
||||
const WEB_INITIAL_EPOCH: u32 = 0;
|
||||
const WEB_SECURE_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
const WEB_SECURE_ACCEPT_TIMEOUT: Duration = WEB_SECURE_HANDSHAKE_TIMEOUT;
|
||||
|
||||
struct RawSplitTunnel {
|
||||
info: Option<TunnelInfo>,
|
||||
@@ -50,24 +56,42 @@ impl Tunnel for RawSplitTunnel {
|
||||
}
|
||||
}
|
||||
|
||||
struct NoiseTunnelFilter {
|
||||
transport: Arc<Mutex<TransportState>>,
|
||||
#[derive(Clone, Copy)]
|
||||
enum SecureTunnelRole {
|
||||
Initiator,
|
||||
Responder,
|
||||
}
|
||||
|
||||
impl TunnelFilter for NoiseTunnelFilter {
|
||||
impl SecureTunnelRole {
|
||||
fn send_dir(self) -> SecureDatagramDirection {
|
||||
match self {
|
||||
Self::Initiator => SecureDatagramDirection::AToB,
|
||||
Self::Responder => SecureDatagramDirection::BToA,
|
||||
}
|
||||
}
|
||||
|
||||
fn recv_dir(self) -> SecureDatagramDirection {
|
||||
match self {
|
||||
Self::Initiator => SecureDatagramDirection::BToA,
|
||||
Self::Responder => SecureDatagramDirection::AToB,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SecureDatagramTunnelFilter {
|
||||
session: Arc<SecureDatagramSession>,
|
||||
role: SecureTunnelRole,
|
||||
}
|
||||
|
||||
impl TunnelFilter for SecureDatagramTunnelFilter {
|
||||
type FilterOutput = ();
|
||||
|
||||
fn before_send(&self, data: ZCPacket) -> Option<ZCPacket> {
|
||||
let plain = data.tunnel_payload();
|
||||
let mut encrypted = vec![0u8; plain.len() + 64];
|
||||
let len = self
|
||||
.transport
|
||||
.lock()
|
||||
.unwrap()
|
||||
.write_message(plain, &mut encrypted)
|
||||
.ok()?;
|
||||
let mut packet = ZCPacket::new_with_payload(&encrypted[..len]);
|
||||
let mut packet = ZCPacket::new_with_payload(data.tunnel_payload());
|
||||
packet.fill_peer_manager_hdr(0, 0, PacketType::Data as u8);
|
||||
self.session
|
||||
.encrypt_payload(self.role.send_dir(), &mut packet)
|
||||
.ok()?;
|
||||
Some(packet)
|
||||
}
|
||||
|
||||
@@ -76,23 +100,24 @@ impl TunnelFilter for NoiseTunnelFilter {
|
||||
Ok(v) => v,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
let cipher = packet.payload();
|
||||
let mut plain = vec![0u8; cipher.len() + 64];
|
||||
let len = match self
|
||||
.transport
|
||||
.lock()
|
||||
|
||||
let mut cipher = ZCPacket::new_with_payload(packet.payload());
|
||||
cipher.fill_peer_manager_hdr(0, 0, PacketType::Data as u8);
|
||||
cipher
|
||||
.mut_peer_manager_header()
|
||||
.unwrap()
|
||||
.read_message(cipher, &mut plain)
|
||||
.set_encrypted(true);
|
||||
if let Err(e) = self
|
||||
.session
|
||||
.decrypt_payload(self.role.recv_dir(), &mut cipher)
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return Some(Err(TunnelError::InvalidPacket(format!(
|
||||
"noise decrypt failed: {e}"
|
||||
))));
|
||||
}
|
||||
};
|
||||
return Some(Err(TunnelError::InvalidPacket(format!(
|
||||
"secure datagram decrypt failed: {e}"
|
||||
))));
|
||||
}
|
||||
|
||||
Some(Ok(ZCPacket::new_from_buf(
|
||||
BytesMut::from(&plain[..len]),
|
||||
cipher.payload_bytes(),
|
||||
ZCPacketType::DummyTunnel,
|
||||
)))
|
||||
}
|
||||
@@ -117,24 +142,50 @@ fn decode_noise_payload(payload: &[u8]) -> Option<&[u8]> {
|
||||
payload.strip_prefix(NOISE_MAGIC)
|
||||
}
|
||||
|
||||
pub fn web_secure_tunnel_supported() -> bool {
|
||||
WEB_SECURE_CIPHER_ALGORITHM
|
||||
.parse::<EncryptionAlgorithm>()
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn web_secure_cipher_algorithm() -> Result<&'static str, TunnelError> {
|
||||
if !web_secure_tunnel_supported() {
|
||||
return Err(TunnelError::InternalError(format!(
|
||||
"web secure tunnel requires {WEB_SECURE_CIPHER_ALGORITHM} support"
|
||||
)));
|
||||
}
|
||||
Ok(WEB_SECURE_CIPHER_ALGORITHM)
|
||||
}
|
||||
|
||||
fn new_web_secure_session(root_key: [u8; 32], algorithm: &str) -> Arc<SecureDatagramSession> {
|
||||
let algo = algorithm.to_string();
|
||||
Arc::new(SecureDatagramSession::new(
|
||||
root_key,
|
||||
WEB_SESSION_GENERATION,
|
||||
WEB_INITIAL_EPOCH,
|
||||
algo.clone(),
|
||||
algo,
|
||||
))
|
||||
}
|
||||
|
||||
fn wrap_secure_tunnel(
|
||||
info: Option<TunnelInfo>,
|
||||
stream: std::pin::Pin<Box<dyn ZCPacketStream>>,
|
||||
sink: std::pin::Pin<Box<dyn ZCPacketSink>>,
|
||||
transport: TransportState,
|
||||
session: Arc<SecureDatagramSession>,
|
||||
role: SecureTunnelRole,
|
||||
) -> Box<dyn Tunnel> {
|
||||
let raw = RawSplitTunnel::new(info, stream, sink);
|
||||
Box::new(TunnelWithFilter::new(
|
||||
raw,
|
||||
NoiseTunnelFilter {
|
||||
transport: Arc::new(Mutex::new(transport)),
|
||||
},
|
||||
SecureDatagramTunnelFilter { session, role },
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn upgrade_client_tunnel(
|
||||
tunnel: Box<dyn Tunnel>,
|
||||
) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let web_cipher_algorithm = web_secure_cipher_algorithm()?;
|
||||
let info = tunnel.info();
|
||||
let (mut stream, mut sink) = tunnel.split();
|
||||
|
||||
@@ -156,19 +207,32 @@ pub async fn upgrade_client_tunnel(
|
||||
)))
|
||||
.await?;
|
||||
|
||||
let msg2_packet = stream.next().await.ok_or(TunnelError::Shutdown)??;
|
||||
let msg2_packet = match tokio::time::timeout(WEB_SECURE_HANDSHAKE_TIMEOUT, stream.next()).await
|
||||
{
|
||||
Ok(Some(Ok(packet))) => packet,
|
||||
Ok(Some(Err(error))) => return Err(error),
|
||||
Ok(None) => return Err(TunnelError::Shutdown),
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
let msg2_cipher = decode_noise_payload(msg2_packet.payload())
|
||||
.ok_or_else(|| TunnelError::InvalidPacket("invalid noise msg2 magic".to_string()))?;
|
||||
let mut msg2 = vec![0u8; 1024];
|
||||
state
|
||||
.read_message(msg2_cipher, &mut msg2)
|
||||
let mut root_key_buf = [0u8; 32];
|
||||
let root_key_len = state
|
||||
.read_message(msg2_cipher, &mut root_key_buf)
|
||||
.map_err(|e| TunnelError::InvalidPacket(format!("read noise msg2 failed: {e}")))?;
|
||||
if root_key_len != root_key_buf.len() {
|
||||
return Err(TunnelError::InvalidPacket(format!(
|
||||
"invalid web secure root key len: {root_key_len}"
|
||||
)));
|
||||
}
|
||||
|
||||
let transport = state
|
||||
.into_transport_mode()
|
||||
.map_err(|e| TunnelError::InternalError(format!("switch transport mode failed: {e}")))?;
|
||||
|
||||
Ok(wrap_secure_tunnel(info, stream, sink, transport))
|
||||
Ok(wrap_secure_tunnel(
|
||||
info,
|
||||
stream,
|
||||
sink,
|
||||
new_web_secure_session(root_key_buf, web_cipher_algorithm),
|
||||
SecureTunnelRole::Initiator,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn accept_or_upgrade_server_tunnel(
|
||||
@@ -179,7 +243,7 @@ pub async fn accept_or_upgrade_server_tunnel(
|
||||
let mut stream = stream;
|
||||
let mut sink = sink;
|
||||
|
||||
let first_packet = match tokio::time::timeout(Duration::from_secs(1), stream.next()).await {
|
||||
let first_packet = match tokio::time::timeout(WEB_SECURE_ACCEPT_TIMEOUT, stream.next()).await {
|
||||
Ok(Some(Ok(packet))) => packet,
|
||||
Ok(Some(Err(error))) => return Err(error),
|
||||
Ok(None) => return Err(TunnelError::Shutdown),
|
||||
@@ -197,6 +261,7 @@ pub async fn accept_or_upgrade_server_tunnel(
|
||||
false,
|
||||
));
|
||||
};
|
||||
let web_cipher_algorithm = web_secure_cipher_algorithm()?;
|
||||
|
||||
let params: NoiseParams = NOISE_PATTERN
|
||||
.parse()
|
||||
@@ -212,18 +277,81 @@ pub async fn accept_or_upgrade_server_tunnel(
|
||||
.read_message(msg1_cipher, &mut msg1)
|
||||
.map_err(|e| TunnelError::InvalidPacket(format!("read noise msg1 failed: {e}")))?;
|
||||
|
||||
let root_key = SecureDatagramSession::new_root_key();
|
||||
let mut msg2 = vec![0u8; 1024];
|
||||
let msg2_len = state
|
||||
.write_message(&[], &mut msg2)
|
||||
.write_message(&root_key, &mut msg2)
|
||||
.map_err(|e| TunnelError::InternalError(format!("write noise msg2 failed: {e}")))?;
|
||||
sink.send(pack_control_packet(&encode_noise_payload(
|
||||
&msg2[..msg2_len],
|
||||
)))
|
||||
.await?;
|
||||
|
||||
let transport = state
|
||||
.into_transport_mode()
|
||||
.map_err(|e| TunnelError::InternalError(format!("switch transport mode failed: {e}")))?;
|
||||
|
||||
Ok((wrap_secure_tunnel(info, stream, sink, transport), true))
|
||||
Ok((
|
||||
wrap_secure_tunnel(
|
||||
info,
|
||||
stream,
|
||||
sink,
|
||||
new_web_secure_session(root_key, web_cipher_algorithm),
|
||||
SecureTunnelRole::Responder,
|
||||
),
|
||||
true,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tunnel::ring::create_ring_tunnel_pair;
|
||||
|
||||
#[test]
|
||||
fn web_secure_cipher_algorithm_matches_support_flag() {
|
||||
let result = web_secure_cipher_algorithm();
|
||||
if web_secure_tunnel_supported() {
|
||||
assert_eq!(result.unwrap(), WEB_SECURE_CIPHER_ALGORITHM);
|
||||
} else {
|
||||
assert!(matches!(result, Err(TunnelError::InternalError(_))));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_secure_session_uses_pinned_cipher_algorithm() {
|
||||
if !web_secure_tunnel_supported() {
|
||||
return;
|
||||
}
|
||||
|
||||
let session = new_web_secure_session(
|
||||
SecureDatagramSession::new_root_key(),
|
||||
web_secure_cipher_algorithm().unwrap(),
|
||||
);
|
||||
session
|
||||
.check_encrypt_algo_same(WEB_SECURE_CIPHER_ALGORITHM, WEB_SECURE_CIPHER_ALGORITHM)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn upgrade_client_tunnel_times_out_when_server_never_replies() {
|
||||
let (server_tunnel, client_tunnel) = create_ring_tunnel_pair();
|
||||
let _server_tunnel = server_tunnel;
|
||||
|
||||
let err = upgrade_client_tunnel(client_tunnel).await.unwrap_err();
|
||||
assert!(matches!(err, TunnelError::Timeout(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn accept_secure_tunnel_after_short_client_delay() {
|
||||
let (server_tunnel, client_tunnel) = create_ring_tunnel_pair();
|
||||
|
||||
let server_task =
|
||||
tokio::spawn(async move { accept_or_upgrade_server_tunnel(server_tunnel).await });
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(1500)).await;
|
||||
|
||||
let client_task = tokio::spawn(async move { upgrade_client_tunnel(client_tunnel).await });
|
||||
|
||||
let (server_res, client_res) = tokio::join!(server_task, client_task);
|
||||
let (_, secure) = server_res.unwrap().unwrap();
|
||||
assert!(secure);
|
||||
assert!(client_res.unwrap().is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user