Ignore stale packets during attach resume
This commit is contained in:
+121
-56
@@ -1662,18 +1662,24 @@ async fn bootstrap_attach(
|
|||||||
let packet =
|
let packet =
|
||||||
protocol::encode_plain(PacketKind::BootstrapAttachRequest, [0u8; 16], 1, 0, &body)?;
|
protocol::encode_plain(PacketKind::BootstrapAttachRequest, [0u8; 16], 1, 0, &body)?;
|
||||||
socket.send_to(&packet, addr).await?;
|
socket.send_to(&packet, addr).await?;
|
||||||
let mut buf = vec![0u8; 65535];
|
let expected_key_id = protocol::session_key_id(&bootstrap.session_key);
|
||||||
let (n, _) = tokio::time::timeout(Duration::from_secs(5), socket.recv_from(&mut buf)).await??;
|
let ok = recv_response_until(socket, Duration::from_secs(5), |packet| {
|
||||||
let packet = protocol::decode(&buf[..n])?;
|
if packet.header.kind == PacketKind::AttachReject && packet.header.conn_id == [0u8; 16] {
|
||||||
if packet.header.kind != PacketKind::AttachOk {
|
|
||||||
if packet.header.kind == PacketKind::AttachReject {
|
|
||||||
let reject: AttachReject = protocol::from_body(&packet.body)?;
|
let reject: AttachReject = protocol::from_body(&packet.body)?;
|
||||||
return Err(anyhow!("attach rejected: {}", reject.reason));
|
return Err(anyhow!("attach rejected: {}", reject.reason));
|
||||||
}
|
}
|
||||||
return Err(anyhow!("attach rejected or unexpected response"));
|
if packet.header.kind != PacketKind::AttachOk
|
||||||
}
|
|| packet.header.session_key_id != expected_key_id
|
||||||
let plain = protocol::decrypt_body(&packet, &bootstrap.session_key, SERVER_TO_CLIENT)?;
|
{
|
||||||
let ok: AttachOk = protocol::from_body(&plain)?;
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let Ok(plain) = protocol::decrypt_body(&packet, &bootstrap.session_key, SERVER_TO_CLIENT)
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
Ok(protocol::from_body::<AttachOk>(&plain).ok())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
let cred = CachedCredential {
|
let cred = CachedCredential {
|
||||||
server: server_name.to_string(),
|
server: server_name.to_string(),
|
||||||
session: ok.session.clone(),
|
session: ok.session.clone(),
|
||||||
@@ -1729,33 +1735,36 @@ async fn try_ticket_attach(
|
|||||||
)?;
|
)?;
|
||||||
socket.send_to(&packet, addr).await?;
|
socket.send_to(&packet, addr).await?;
|
||||||
|
|
||||||
let mut buf = vec![0u8; 65535];
|
let ok = recv_response_until(socket, Duration::from_millis(700), |packet| {
|
||||||
let (n, _) =
|
if packet.header.kind == PacketKind::AttachReject && packet.header.conn_id == [0u8; 16] {
|
||||||
tokio::time::timeout(Duration::from_millis(700), socket.recv_from(&mut buf)).await??;
|
|
||||||
let packet = protocol::decode(&buf[..n])?;
|
|
||||||
if packet.header.kind != PacketKind::AttachOk {
|
|
||||||
if packet.header.kind == PacketKind::AttachReject {
|
|
||||||
let reject: AttachReject = protocol::from_body(&packet.body)?;
|
let reject: AttachReject = protocol::from_body(&packet.body)?;
|
||||||
return Err(anyhow!("ticket attach rejected: {}", reject.reason));
|
return Err(anyhow!("ticket attach rejected: {}", reject.reason));
|
||||||
}
|
}
|
||||||
return Err(anyhow!("ticket attach rejected"));
|
if packet.header.kind != PacketKind::AttachOk {
|
||||||
}
|
return Ok(None);
|
||||||
let envelope: TicketAttachOkEnvelope = protocol::from_body(&packet.body)?;
|
}
|
||||||
let mut salt = Vec::with_capacity(24);
|
let Ok(envelope) = protocol::from_body::<TicketAttachOkEnvelope>(&packet.body) else {
|
||||||
salt.extend_from_slice(&client_nonce);
|
return Ok(None);
|
||||||
salt.extend_from_slice(&envelope.server_nonce);
|
};
|
||||||
let response_key = crypto::hkdf32(
|
let mut salt = Vec::with_capacity(24);
|
||||||
&cached.attach_ticket_psk,
|
salt.extend_from_slice(&client_nonce);
|
||||||
&salt,
|
salt.extend_from_slice(&envelope.server_nonce);
|
||||||
b"dosh/ticket-attach-ok/v1",
|
let response_key = crypto::hkdf32(
|
||||||
)?;
|
&cached.attach_ticket_psk,
|
||||||
let plain = crypto::open(
|
&salt,
|
||||||
&response_key,
|
b"dosh/ticket-attach-ok/v1",
|
||||||
&envelope.server_nonce,
|
)?;
|
||||||
b"dosh-ticket-attach-ok-v1",
|
let Ok(plain) = crypto::open(
|
||||||
&envelope.ciphertext,
|
&response_key,
|
||||||
)?;
|
&envelope.server_nonce,
|
||||||
let ok: AttachOk = protocol::from_body(&plain)?;
|
b"dosh-ticket-attach-ok-v1",
|
||||||
|
&envelope.ciphertext,
|
||||||
|
) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
Ok(protocol::from_body::<AttachOk>(&plain).ok())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
let frame = Frame {
|
let frame = Frame {
|
||||||
session: ok.session.clone(),
|
session: ok.session.clone(),
|
||||||
output_seq: ok.initial_seq,
|
output_seq: ok.initial_seq,
|
||||||
@@ -1795,19 +1804,24 @@ async fn try_resume_fresh_process(
|
|||||||
&body,
|
&body,
|
||||||
)?;
|
)?;
|
||||||
socket.send_to(&packet, addr).await?;
|
socket.send_to(&packet, addr).await?;
|
||||||
let mut buf = vec![0u8; 65535];
|
let frame = recv_response_until(socket, Duration::from_millis(700), |packet| {
|
||||||
let (n, _) =
|
if packet.header.conn_id != cached.client_id {
|
||||||
tokio::time::timeout(Duration::from_millis(700), socket.recv_from(&mut buf)).await??;
|
return Ok(None);
|
||||||
let packet = protocol::decode(&buf[..n])?;
|
}
|
||||||
if packet.header.kind != PacketKind::ResumeOk {
|
|
||||||
if packet.header.kind == PacketKind::AttachReject {
|
if packet.header.kind == PacketKind::AttachReject {
|
||||||
let reject: AttachReject = protocol::from_body(&packet.body)?;
|
let reject: AttachReject = protocol::from_body(&packet.body)?;
|
||||||
return Err(anyhow!("resume rejected: {}", reject.reason));
|
return Err(anyhow!("resume rejected: {}", reject.reason));
|
||||||
}
|
}
|
||||||
return Err(anyhow!("resume rejected"));
|
if packet.header.kind != PacketKind::ResumeOk {
|
||||||
}
|
return Ok(None);
|
||||||
let plain = protocol::decrypt_body(&packet, &cached.session_key, SERVER_TO_CLIENT)?;
|
}
|
||||||
let frame: Frame = protocol::from_body(&plain)?;
|
let Ok(plain) = protocol::decrypt_body(&packet, &cached.session_key, SERVER_TO_CLIENT)
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
Ok(protocol::from_body::<Frame>(&plain).ok())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
let mut next = cached.clone();
|
let mut next = cached.clone();
|
||||||
next.last_rendered_seq = frame.output_seq;
|
next.last_rendered_seq = frame.output_seq;
|
||||||
Ok((frame, next))
|
Ok((frame, next))
|
||||||
@@ -1839,27 +1853,53 @@ async fn try_live_resume(
|
|||||||
)?;
|
)?;
|
||||||
*send_seq += 1;
|
*send_seq += 1;
|
||||||
socket.send_to(&packet, addr).await?;
|
socket.send_to(&packet, addr).await?;
|
||||||
let mut buf = vec![0u8; 65535];
|
let frame = recv_response_until(socket, Duration::from_millis(700), |packet| {
|
||||||
let (n, _) =
|
if packet.header.conn_id != cached.client_id {
|
||||||
tokio::time::timeout(Duration::from_millis(700), socket.recv_from(&mut buf)).await??;
|
return Ok(None);
|
||||||
let packet = protocol::decode(&buf[..n])?;
|
}
|
||||||
if packet.header.conn_id != cached.client_id {
|
|
||||||
return Err(anyhow!("resume received stale client packet"));
|
|
||||||
}
|
|
||||||
if packet.header.kind != PacketKind::ResumeOk {
|
|
||||||
if packet.header.kind == PacketKind::AttachReject {
|
if packet.header.kind == PacketKind::AttachReject {
|
||||||
let reject: AttachReject = protocol::from_body(&packet.body)?;
|
let reject: AttachReject = protocol::from_body(&packet.body)?;
|
||||||
return Err(anyhow!("resume rejected: {}", reject.reason));
|
return Err(anyhow!("resume rejected: {}", reject.reason));
|
||||||
}
|
}
|
||||||
return Err(anyhow!("resume rejected"));
|
if packet.header.kind != PacketKind::ResumeOk {
|
||||||
}
|
return Ok(None);
|
||||||
let plain = protocol::decrypt_body(&packet, &cached.session_key, SERVER_TO_CLIENT)?;
|
}
|
||||||
let frame: Frame = protocol::from_body(&plain)?;
|
let Ok(plain) = protocol::decrypt_body(&packet, &cached.session_key, SERVER_TO_CLIENT)
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
Ok(protocol::from_body::<Frame>(&plain).ok())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
let mut next = cached.clone();
|
let mut next = cached.clone();
|
||||||
next.last_rendered_seq = frame.output_seq;
|
next.last_rendered_seq = frame.output_seq;
|
||||||
Ok((frame, next))
|
Ok((frame, next))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn recv_response_until<T, F>(socket: &UdpSocket, wait: Duration, mut accept: F) -> Result<T>
|
||||||
|
where
|
||||||
|
F: FnMut(protocol::Packet) -> Result<Option<T>>,
|
||||||
|
{
|
||||||
|
let started = Instant::now();
|
||||||
|
let mut buf = vec![0u8; 65535];
|
||||||
|
loop {
|
||||||
|
let elapsed = started.elapsed();
|
||||||
|
if elapsed >= wait {
|
||||||
|
return Err(anyhow!("timed out waiting for server response"));
|
||||||
|
}
|
||||||
|
let remaining = wait - elapsed;
|
||||||
|
let (n, _) = tokio::time::timeout(remaining, socket.recv_from(&mut buf))
|
||||||
|
.await
|
||||||
|
.context("timed out waiting for server response")??;
|
||||||
|
let Ok(packet) = protocol::decode(&buf[..n]) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if let Some(response) = accept(packet)? {
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn run_terminal(
|
async fn run_terminal(
|
||||||
socket: UdpSocket,
|
socket: UdpSocket,
|
||||||
mut cred: CachedCredential,
|
mut cred: CachedCredential,
|
||||||
@@ -2800,10 +2840,11 @@ mod tests {
|
|||||||
DynamicForward, FrameBuffer, LocalForward, Predictor, RemoteForward, SshConfig,
|
DynamicForward, FrameBuffer, LocalForward, Predictor, RemoteForward, SshConfig,
|
||||||
auth_allows, load_first_native_identity_with_prompt, parse_dynamic_forward,
|
auth_allows, load_first_native_identity_with_prompt, parse_dynamic_forward,
|
||||||
parse_local_forward, parse_remote_forward, parse_ssh_config, raw_contains_host_table,
|
parse_local_forward, parse_remote_forward, parse_ssh_config, raw_contains_host_table,
|
||||||
ssh_destination_host, ssh_username, startup_command, toml_bare_key_or_quoted,
|
recv_response_until, ssh_destination_host, ssh_username, startup_command,
|
||||||
|
toml_bare_key_or_quoted,
|
||||||
};
|
};
|
||||||
use dosh::config::ClientConfig;
|
use dosh::config::ClientConfig;
|
||||||
use dosh::protocol::Frame;
|
use dosh::protocol::{self, Frame, PacketKind};
|
||||||
use ed25519_dalek::{SigningKey, VerifyingKey};
|
use ed25519_dalek::{SigningKey, VerifyingKey};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2981,6 +3022,30 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn recv_response_until_ignores_unmatched_packets() {
|
||||||
|
let receiver = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let sender = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let addr = receiver.local_addr().unwrap();
|
||||||
|
let stale = protocol::encode_plain(PacketKind::Ping, [1u8; 16], 1, 0, b"stale").unwrap();
|
||||||
|
let valid = protocol::encode_plain(PacketKind::Pong, [2u8; 16], 2, 0, b"valid").unwrap();
|
||||||
|
sender.send_to(&stale, addr).await.unwrap();
|
||||||
|
sender.send_to(&valid, addr).await.unwrap();
|
||||||
|
|
||||||
|
let packet = recv_response_until(&receiver, std::time::Duration::from_secs(1), |packet| {
|
||||||
|
if packet.header.kind == PacketKind::Pong {
|
||||||
|
Ok(Some(packet))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(packet.header.kind, PacketKind::Pong);
|
||||||
|
assert_eq!(packet.body, b"valid");
|
||||||
|
}
|
||||||
|
|
||||||
fn write_identity(path: &std::path::Path, signing: &SigningKey) {
|
fn write_identity(path: &std::path::Path, signing: &SigningKey) {
|
||||||
let keypair = ssh_key::private::Ed25519Keypair::from(signing);
|
let keypair = ssh_key::private::Ed25519Keypair::from(signing);
|
||||||
let private =
|
let private =
|
||||||
|
|||||||
Reference in New Issue
Block a user