fix: harden tunnel security and timeout handling

This commit is contained in:
fawney19
2026-05-23 14:17:04 +08:00
parent 74f7348529
commit 6447fda852
19 changed files with 571 additions and 92 deletions

View File

@@ -185,6 +185,12 @@ pub struct RequestMeta {
pub method: String,
pub url: String,
pub headers: std::collections::HashMap<String, String>,
#[serde(default, skip_serializing_if = "is_false")]
pub stream: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_timeout_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stream_first_byte_timeout_ms: Option<u64>,
#[serde(default = "default_timeout", deserialize_with = "deserialize_timeout")]
pub timeout: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]

View File

@@ -39,6 +39,8 @@ pub enum TunnelSecurityError {
MissingEncryptedFlag,
#[error("secure tunnel frame payload is too short")]
PayloadTooShort,
#[error("secure tunnel frame sequence is not the expected next value")]
UnexpectedSequence,
#[error("secure tunnel frame encryption failed")]
Encrypt,
#[error("secure tunnel frame decryption failed")]
@@ -51,6 +53,7 @@ pub struct SecureFrameCodec {
seal_prefix: [u8; 4],
open_prefix: [u8; 4],
next_sequence: AtomicU64,
next_open_sequence: AtomicU64,
}
impl SecureFrameCodec {
@@ -90,6 +93,7 @@ impl SecureFrameCodec {
seal_prefix,
open_prefix,
next_sequence: AtomicU64::new(0),
next_open_sequence: AtomicU64::new(0),
})
}
@@ -132,6 +136,10 @@ impl SecureFrameCodec {
let mut payload = frame.payload.clone();
let sequence = payload.get_u64();
let expected_sequence = self.next_open_sequence.load(Ordering::Relaxed);
if sequence != expected_sequence {
return Err(TunnelSecurityError::UnexpectedSequence);
}
let nonce_bytes = nonce_bytes(self.open_prefix, sequence);
let nonce = Nonce::from_slice(&nonce_bytes);
let clear_flags = frame.flags & !FLAG_ENCRYPTED;
@@ -146,6 +154,8 @@ impl SecureFrameCodec {
},
)
.map_err(|_| TunnelSecurityError::Decrypt)?;
self.next_open_sequence
.store(expected_sequence.wrapping_add(1), Ordering::Relaxed);
Ok(Frame::new(
frame.stream_id,
@@ -243,6 +253,68 @@ mod tests {
));
}
#[test]
fn secure_frame_rejects_replayed_sequence() {
let client = SecureFrameCodec::new(&test_key(), "session-1", TunnelSecurityRole::Client)
.expect("client codec");
let server = SecureFrameCodec::new(&test_key(), "session-1", TunnelSecurityRole::Server)
.expect("server codec");
let encrypted = client
.encrypt_frame(Frame::new(
1,
MsgType::RequestBody,
0,
Bytes::from_static(b"secret"),
))
.expect("encrypt");
let wire = Frame::decode(encrypted).expect("wire frame");
server.decrypt_frame(wire.clone()).expect("first decrypt");
assert!(matches!(
server.decrypt_frame(wire),
Err(TunnelSecurityError::UnexpectedSequence)
));
}
#[test]
fn secure_frame_rejects_out_of_order_sequence_without_advancing() {
let client = SecureFrameCodec::new(&test_key(), "session-1", TunnelSecurityRole::Client)
.expect("client codec");
let server = SecureFrameCodec::new(&test_key(), "session-1", TunnelSecurityRole::Server)
.expect("server codec");
let first = Frame::decode(
client
.encrypt_frame(Frame::new(
1,
MsgType::RequestBody,
0,
Bytes::from_static(b"first"),
))
.expect("encrypt first"),
)
.expect("first wire frame");
let second = Frame::decode(
client
.encrypt_frame(Frame::new(
1,
MsgType::RequestBody,
0,
Bytes::from_static(b"second"),
))
.expect("encrypt second"),
)
.expect("second wire frame");
assert!(matches!(
server.decrypt_frame(second),
Err(TunnelSecurityError::UnexpectedSequence)
));
assert_eq!(
server.decrypt_frame(first).expect("first decrypt").payload,
Bytes::from_static(b"first")
);
}
#[test]
fn secure_frame_uses_session_in_key_derivation() {
let session_a = "node-1:connection-a";