Harden release readiness gates
ci / test (push) Has been cancelled
ci / fuzz-smoke (push) Has been cancelled
ci / windows-client (push) Has been cancelled
ci / package-release (linux-x86_64, ubuntu-latest) (push) Has been cancelled
ci / package-release (macos-aarch64, macos-14) (push) Has been cancelled
ci / package-release (macos-x86_64, macos-13) (push) Has been cancelled
ci / package-release (windows-x86_64, windows-latest) (push) Has been cancelled
ci / remote-bench (push) Has been cancelled

This commit is contained in:
DuProcess
2026-06-22 17:14:59 -04:00
parent fb6cf4e7c2
commit f9a378a9cd
7 changed files with 231 additions and 206 deletions
+3
View File
@@ -95,6 +95,7 @@ pub fn load_or_create_server_secret(config: &ServerConfig) -> Result<[u8; 32]> {
Ok(secret)
}
#[allow(clippy::too_many_arguments)]
pub fn build_bootstrap(
config: &ServerConfig,
secret: &[u8; 32],
@@ -161,6 +162,7 @@ pub fn build_bootstrap(
})
}
#[allow(clippy::too_many_arguments)]
pub fn attach_token(
secret: &[u8; 32],
user: &str,
@@ -206,6 +208,7 @@ pub fn verify_bootstrap(resp: &BootstrapResponse, secret: &[u8; 32]) -> Result<b
Ok(expected == resp.attach_token)
}
#[allow(clippy::too_many_arguments)]
pub fn build_attach_ticket(
secret: &[u8; 32],
server_id: [u8; 32],
+18 -13
View File
@@ -905,7 +905,7 @@ async fn run_doctor_for_host(
match try_native_auth_check(
&socket,
config,
&requested,
requested,
&server,
ssh_port,
&udp_host,
@@ -1776,6 +1776,7 @@ fn select_session(requested: Option<&str>, _force_new: bool) -> String {
protocol::generate_implicit_session_name()
}
#[allow(clippy::too_many_arguments)]
fn ssh_bootstrap(
server: &str,
ssh_port: Option<u16>,
@@ -1836,10 +1837,10 @@ fn ssh_destination_host(server: &str) -> String {
.split('/')
.next()
.unwrap_or(without_user);
if let Some(stripped) = without_path.strip_prefix('[') {
if let Some((host, _)) = stripped.split_once(']') {
return host.to_string();
}
if let Some(stripped) = without_path.strip_prefix('[')
&& let Some((host, _)) = stripped.split_once(']')
{
return host.to_string();
}
without_path
.split_once(':')
@@ -2068,6 +2069,7 @@ fn resolve_addr(host: &str, port: u16) -> Result<SocketAddr> {
.ok_or_else(|| anyhow!("no UDP address resolved for {host}:{port}"))
}
#[allow(clippy::too_many_arguments)]
async fn try_native_auth(
socket: &UdpSocket,
config: &dosh::config::ClientConfig,
@@ -2173,7 +2175,7 @@ async fn try_native_auth(
let auth = sign_native_user_auth(
config,
cli_identity,
&ssh_config,
ssh_config,
&hello,
&server_hello.hello,
requested_forwardings,
@@ -2230,6 +2232,7 @@ async fn try_native_auth(
Ok((frame, cred))
}
#[allow(clippy::too_many_arguments)]
async fn try_native_auth_check(
socket: &UdpSocket,
config: &dosh::config::ClientConfig,
@@ -2329,7 +2332,7 @@ async fn try_native_auth_check(
let auth = sign_native_user_auth(
config,
cli_identity,
&ssh_config,
ssh_config,
&hello,
&server_hello.hello,
Vec::new(),
@@ -2757,6 +2760,7 @@ where
}
}
#[allow(clippy::too_many_arguments)]
async fn run_terminal(
socket: UdpSocket,
mut cred: CachedCredential,
@@ -3844,6 +3848,7 @@ async fn send_stream_data(
send_stream_packet(socket, addr, cred, send_seq, PacketKind::StreamData, &body).await
}
#[allow(clippy::too_many_arguments)]
async fn queue_or_send_stream_data(
socket: &UdpSocket,
addr: SocketAddr,
@@ -3884,6 +3889,7 @@ async fn queue_or_send_stream_data(
send_stream_data(socket, addr, cred, send_seq, stream_id, offset, bytes).await
}
#[allow(clippy::too_many_arguments)]
async fn flush_stream_pending_data(
socket: &UdpSocket,
addr: SocketAddr,
@@ -3898,10 +3904,7 @@ async fn flush_stream_pending_data(
let Some(pending) = stream_pending_data.get_mut(&stream_id) else {
return Ok(());
};
loop {
let Some(bytes) = pending.front() else {
break;
};
while let Some(bytes) = pending.front() {
let credit = stream_send_credit.get(&stream_id).copied().unwrap_or(0);
if credit < bytes.len() {
break;
@@ -5391,8 +5394,10 @@ mod tests {
let path = dir.path().join("id_ed25519");
let signing = SigningKey::from_bytes(&[63u8; 32]);
write_identity(&path, &signing);
let mut config = ClientConfig::default();
config.identity_files = vec![path.display().to_string()];
let config = ClientConfig {
identity_files: vec![path.display().to_string()],
..ClientConfig::default()
};
let ssh_config = SshConfig {
identities_only: true,
..SshConfig::default()
+195 -183
View File
@@ -651,10 +651,10 @@ impl ServerState {
/// Remove a client from whichever session holds it, keeping the index in
/// sync. Returns true when a client was actually removed.
fn remove_client_everywhere(&mut self, client_id: &[u8; 16]) -> bool {
if let Some(session_name) = self.client_index.remove(client_id) {
if let Some(session) = self.sessions.get_mut(&session_name) {
return session.clients.remove(client_id).is_some();
}
if let Some(session_name) = self.client_index.remove(client_id)
&& let Some(session) = self.sessions.get_mut(&session_name)
{
return session.clients.remove(client_id).is_some();
}
false
}
@@ -987,9 +987,9 @@ async fn handle_native_user_auth(
let body = protocol::decrypt_body(packet, &pending.session_key, CLIENT_TO_SERVER)?;
let req: NativeUserAuthBody = protocol::from_body(&body)?;
if pending.client.requested_mode == "doctor" {
let check = {
let check_result = {
let locked = state.lock().expect("server state poisoned");
if let Err(err) = verify_native_user_auth_from_config(
verify_native_user_auth_from_config(
&locked.config,
&pending.client,
&pending.server,
@@ -997,16 +997,7 @@ async fn handle_native_user_auth(
Some(peer.ip()),
)
.context("verify native user auth")
{
return send_reject_to_client(
socket,
peer,
packet.header.conn_id,
&format!("{err:#}"),
)
.await;
}
NativeAuthCheckOkBody {
.map(|_| NativeAuthCheckOkBody {
requested_user: pending.client.requested_user.clone(),
native_auth_enabled: locked.config.native_auth,
allow_tcp_forwarding: locked.config.allow_tcp_forwarding,
@@ -1014,6 +1005,18 @@ async fn handle_native_user_auth(
allow_agent_forwarding: locked.config.allow_agent_forwarding,
policy_flags: Vec::new(),
server_version: env!("CARGO_PKG_VERSION").to_string(),
})
};
let check = match check_result {
Ok(check) => check,
Err(err) => {
return send_reject_to_client(
socket,
peer,
packet.header.conn_id,
&format!("{err:#}"),
)
.await;
}
};
let body = protocol::to_body(&check)?;
@@ -1065,17 +1068,19 @@ async fn handle_native_user_auth(
});
}
let attached: Result<(
[u8; 16],
[u8; 16],
Vec<u8>,
[u8; 32],
String,
String,
u64,
Vec<u8>,
Vec<ForwardingRequest>,
)> = {
struct NativeAttachResult {
client_id: [u8; 16],
key_id: [u8; 16],
attach_ticket: Vec<u8>,
attach_ticket_psk: [u8; 32],
session_name: String,
mode: String,
output_seq: u64,
snapshot: Vec<u8>,
requested_forwardings: Vec<ForwardingRequest>,
}
let attached: Result<NativeAttachResult> = {
let mut locked = state.lock().expect("server state poisoned");
verify_native_user_auth_from_config(
&locked.config,
@@ -1150,7 +1155,7 @@ async fn handle_native_user_auth(
previous_session_key: None,
},
);
Ok((
Ok(NativeAttachResult {
client_id,
key_id,
attach_ticket,
@@ -1159,22 +1164,12 @@ async fn handle_native_user_auth(
mode,
output_seq,
snapshot,
req.auth.requested_forwardings.clone(),
))
requested_forwardings: req.auth.requested_forwardings.clone(),
})
})
};
let (
client_id,
key_id,
attach_ticket,
attach_ticket_psk,
session_name,
mode,
output_seq,
snapshot,
requested_forwardings,
) = match attached {
let attached = match attached {
Ok(attached) => attached,
Err(err) => {
// Auth/session setup failed: clean up the agent socket we bound early.
@@ -1185,11 +1180,12 @@ async fn handle_native_user_auth(
.await;
}
};
let client_id = attached.client_id;
if let Err(err) = start_remote_forwards(
state,
socket,
client_id,
requested_forwardings.clone(),
attached.requested_forwardings.clone(),
pending.client.requested_session.clone(),
)
.await
@@ -1207,14 +1203,14 @@ async fn handle_native_user_auth(
let ok = NativeAuthOk {
client_id,
session: session_name,
mode,
session: attached.session_name,
mode: attached.mode,
session_key: pending.session_key,
session_key_id: key_id,
attach_ticket,
attach_ticket_psk,
initial_seq: output_seq,
snapshot,
session_key_id: attached.key_id,
attach_ticket: attached.attach_ticket,
attach_ticket_psk: attached.attach_ticket_psk,
initial_seq: attached.output_seq,
snapshot: attached.snapshot,
policy_flags: Vec::new(),
};
let body = protocol::to_body(&NativeAuthOkBody { ok })?;
@@ -1238,78 +1234,84 @@ async fn handle_bootstrap_attach(
body: Vec<u8>,
) -> Result<()> {
let req: BootstrapAttachRequest = protocol::from_body(&body)?;
let (client_id, key, key_id, session_name, mode, output_seq, snapshot) = {
type BootstrapAttachResult = ([u8; 16], [u8; 32], [u8; 16], String, String, u64, Vec<u8>);
let attached: Result<BootstrapAttachResult> = {
let mut locked = state.lock().expect("server state poisoned");
if !verify_bootstrap(&req.bootstrap, &locked.secret)? {
return send_reject(socket, peer, "invalid or expired bootstrap").await;
}
if !locked.sessions.contains_key(&req.bootstrap.session) && !locked.config.create_on_attach
Err(anyhow!("invalid or expired bootstrap"))
} else if !locked.sessions.contains_key(&req.bootstrap.session)
&& !locked.config.create_on_attach
{
return send_reject(socket, peer, "session does not exist").await;
}
locked.ensure_session(
&req.bootstrap.session,
req.cols,
req.rows,
&req.bootstrap.mode,
&req.requested_env,
)?;
let session = locked
.sessions
.get_mut(&req.bootstrap.session)
.expect("session exists");
if mode_allows_terminal_updates(&req.bootstrap.mode) {
apply_terminal_size(
session.pty.as_ref(),
&mut session.parser,
Err(anyhow!("session does not exist"))
} else {
locked.ensure_session(
&req.bootstrap.session,
req.cols,
req.rows,
&req.bootstrap.mode,
&req.requested_env,
)?;
let session = locked
.sessions
.get_mut(&req.bootstrap.session)
.expect("session exists");
if mode_allows_terminal_updates(&req.bootstrap.mode) {
apply_terminal_size(
session.pty.as_ref(),
&mut session.parser,
req.cols,
req.rows,
)?;
}
let client_id = crypto::random_16();
let snapshot = screen_snapshot(session.parser.screen());
let output_seq = session.output_seq;
let bootstrap_session = req.bootstrap.session.clone();
locked.insert_client(
&bootstrap_session,
client_id,
ClientState {
endpoint: peer,
mode: req.bootstrap.mode.clone(),
session_key: req.bootstrap.session_key,
last_acked: output_seq,
replay: ReplayWindow::default(),
send_seq: 1,
cols: req.cols,
rows: req.rows,
last_seen: Instant::now(),
pending: VecDeque::new(),
last_frame_sent: Instant::now() - Duration::from_secs(3600),
paced_snapshot_due: false,
allowed_forwardings: Vec::new(),
stream_writers: HashMap::new(),
opened_streams: HashSet::new(),
stream_send_credit: HashMap::new(),
stream_pending_data: HashMap::new(),
stream_next_send_offset: HashMap::new(),
stream_sent_data: HashMap::new(),
stream_next_recv_offset: HashMap::new(),
stream_recv_pending: HashMap::new(),
epoch: 0,
epoch_started: Instant::now(),
epoch_packets: 0,
previous_session_key: None,
},
);
Ok((
client_id,
req.bootstrap.session_key,
req.bootstrap.session_key_id,
req.bootstrap.session.clone(),
req.bootstrap.mode.clone(),
output_seq,
snapshot,
))
}
let client_id = crypto::random_16();
let snapshot = screen_snapshot(session.parser.screen());
let output_seq = session.output_seq;
let bootstrap_session = req.bootstrap.session.clone();
locked.insert_client(
&bootstrap_session,
client_id,
ClientState {
endpoint: peer,
mode: req.bootstrap.mode.clone(),
session_key: req.bootstrap.session_key,
last_acked: output_seq,
replay: ReplayWindow::default(),
send_seq: 1,
cols: req.cols,
rows: req.rows,
last_seen: Instant::now(),
pending: VecDeque::new(),
last_frame_sent: Instant::now() - Duration::from_secs(3600),
paced_snapshot_due: false,
allowed_forwardings: Vec::new(),
stream_writers: HashMap::new(),
opened_streams: HashSet::new(),
stream_send_credit: HashMap::new(),
stream_pending_data: HashMap::new(),
stream_next_send_offset: HashMap::new(),
stream_sent_data: HashMap::new(),
stream_next_recv_offset: HashMap::new(),
stream_recv_pending: HashMap::new(),
epoch: 0,
epoch_started: Instant::now(),
epoch_packets: 0,
previous_session_key: None,
},
);
(
client_id,
req.bootstrap.session_key,
req.bootstrap.session_key_id,
req.bootstrap.session.clone(),
req.bootstrap.mode.clone(),
output_seq,
snapshot,
)
};
let (client_id, key, key_id, session_name, mode, output_seq, snapshot) = match attached {
Ok(attached) => attached,
Err(err) => return send_reject(socket, peer, &err.to_string()).await,
};
let ok = AttachOk {
client_id,
@@ -1341,24 +1343,29 @@ async fn handle_ticket_attach(
body: Vec<u8>,
) -> Result<()> {
let env: TicketAttachEnvelope = protocol::from_body(&body)?;
let (ticket, request_plain) = {
let opened = {
let locked = state.lock().expect("server state poisoned");
if !locked.config.allow_attach_tickets {
return send_reject(socket, peer, "attach tickets disabled").await;
Err(anyhow!("attach tickets disabled"))
} else {
let ticket = open_attach_ticket(&locked.secret, &env.ticket)?;
let request_key = crypto::hkdf32(
&ticket.psk,
&env.client_nonce,
b"dosh/ticket-attach-request/v1",
)?;
let request_plain = crypto::open(
&request_key,
&env.client_nonce,
b"dosh-ticket-attach-request-v1",
&env.ciphertext,
)?;
Ok((ticket, request_plain))
}
let ticket = open_attach_ticket(&locked.secret, &env.ticket)?;
let request_key = crypto::hkdf32(
&ticket.psk,
&env.client_nonce,
b"dosh/ticket-attach-request/v1",
)?;
let request_plain = crypto::open(
&request_key,
&env.client_nonce,
b"dosh-ticket-attach-request-v1",
&env.ciphertext,
)?;
(ticket, request_plain)
};
let (ticket, request_plain) = match opened {
Ok(opened) => opened,
Err(err) => return send_reject(socket, peer, &err.to_string()).await,
};
let req: TicketAttachBody = protocol::from_body(&request_plain)?;
if req.session != ticket.session || req.mode != ticket.mode {
@@ -1367,66 +1374,71 @@ async fn handle_ticket_attach(
let session_key = crypto::random_32();
let session_key_id = protocol::session_key_id(&session_key);
let (client_id, output_seq, snapshot) = {
let attached = {
let mut locked = state.lock().expect("server state poisoned");
if !locked.sessions.contains_key(&req.session) && !locked.config.create_on_attach {
return send_reject(socket, peer, "session does not exist").await;
}
locked.ensure_session(
&req.session,
req.cols,
req.rows,
&req.mode,
&req.requested_env,
)?;
let session = locked
.sessions
.get_mut(&req.session)
.expect("session exists");
if mode_allows_terminal_updates(&req.mode) {
apply_terminal_size(
session.pty.as_ref(),
&mut session.parser,
Err(anyhow!("session does not exist"))
} else {
locked.ensure_session(
&req.session,
req.cols,
req.rows,
&req.mode,
&req.requested_env,
)?;
let session = locked
.sessions
.get_mut(&req.session)
.expect("session exists");
if mode_allows_terminal_updates(&req.mode) {
apply_terminal_size(
session.pty.as_ref(),
&mut session.parser,
req.cols,
req.rows,
)?;
}
let client_id = crypto::random_16();
let snapshot = screen_snapshot(session.parser.screen());
let output_seq = session.output_seq;
let ticket_session = req.session.clone();
locked.insert_client(
&ticket_session,
client_id,
ClientState {
endpoint: peer,
mode: req.mode.clone(),
session_key,
last_acked: output_seq,
replay: ReplayWindow::default(),
send_seq: 1,
cols: req.cols,
rows: req.rows,
last_seen: Instant::now(),
pending: VecDeque::new(),
last_frame_sent: Instant::now() - Duration::from_secs(3600),
paced_snapshot_due: false,
allowed_forwardings: Vec::new(),
stream_writers: HashMap::new(),
opened_streams: HashSet::new(),
stream_send_credit: HashMap::new(),
stream_pending_data: HashMap::new(),
stream_next_send_offset: HashMap::new(),
stream_sent_data: HashMap::new(),
stream_next_recv_offset: HashMap::new(),
stream_recv_pending: HashMap::new(),
epoch: 0,
epoch_started: Instant::now(),
epoch_packets: 0,
previous_session_key: None,
},
);
Ok((client_id, output_seq, snapshot))
}
let client_id = crypto::random_16();
let snapshot = screen_snapshot(session.parser.screen());
let output_seq = session.output_seq;
let ticket_session = req.session.clone();
locked.insert_client(
&ticket_session,
client_id,
ClientState {
endpoint: peer,
mode: req.mode.clone(),
session_key,
last_acked: output_seq,
replay: ReplayWindow::default(),
send_seq: 1,
cols: req.cols,
rows: req.rows,
last_seen: Instant::now(),
pending: VecDeque::new(),
last_frame_sent: Instant::now() - Duration::from_secs(3600),
paced_snapshot_due: false,
allowed_forwardings: Vec::new(),
stream_writers: HashMap::new(),
opened_streams: HashSet::new(),
stream_send_credit: HashMap::new(),
stream_pending_data: HashMap::new(),
stream_next_send_offset: HashMap::new(),
stream_sent_data: HashMap::new(),
stream_next_recv_offset: HashMap::new(),
stream_recv_pending: HashMap::new(),
epoch: 0,
epoch_started: Instant::now(),
epoch_packets: 0,
previous_session_key: None,
},
);
(client_id, output_seq, snapshot)
};
let (client_id, output_seq, snapshot) = match attached {
Ok(attached) => attached,
Err(err) => return send_reject(socket, peer, &err.to_string()).await,
};
let ok = AttachOk {
+4 -4
View File
@@ -311,10 +311,10 @@ pub fn load_hosts_config(path: Option<PathBuf>) -> Result<HostsConfig> {
}
pub fn expand_tilde(path: &str) -> PathBuf {
if let Some(rest) = path.strip_prefix("~/") {
if let Some(home) = dirs::home_dir() {
return home.join(rest);
}
if let Some(rest) = path.strip_prefix("~/")
&& let Some(home) = dirs::home_dir()
{
return home.join(rest);
}
PathBuf::from(path)
}
+2 -5
View File
@@ -297,6 +297,8 @@ fn spawn_reader_thread(
mod tests {
use super::*;
const _: () = assert!(PTY_OUTPUT_CHUNK_BYTES <= 1200);
#[test]
fn terminfo_available_detects_known_and_unknown() {
// A near-universal entry should be present on any host with ncurses.
@@ -305,9 +307,4 @@ mod tests {
assert!(!terminfo_available("definitely-not-a-real-terminal-xyz"));
assert!(!terminfo_available(""));
}
#[test]
fn pty_output_chunk_size_stays_mtu_safe() {
assert!(PTY_OUTPUT_CHUNK_BYTES <= 1200);
}
}
+6 -1
View File
@@ -18,6 +18,11 @@
//! Determinism: the relay's drop/reorder/dup behavior is driven by a fixed-seed
//! PRNG and by explicit one-shot toggles, never by wall-clock timing, so the
//! tests are reproducible and fast.
#![allow(
clippy::collapsible_if,
clippy::explicit_counter_loop,
clippy::single_match
)]
use std::fs;
use std::io::{Read, Write};
@@ -768,7 +773,7 @@ fn session_survives_packet_loss_and_reorder() {
let port = free_udp_port();
let config = write_server_config(&dir, port);
let mut server = start_server(&dir, &config);
let relay = Relay::spawn(port, 0x105_5u64 ^ 0x1111);
let relay = Relay::spawn(port, 0x1055_u64 ^ 0x1111);
let (socket, bootstrap, ok) = attach_through_relay(&config, &relay);
+3
View File
@@ -1,3 +1,5 @@
#![allow(clippy::collapsible_if)]
use std::fs;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream, UdpSocket};
@@ -499,6 +501,7 @@ fn direct_attach_session(
(socket, bootstrap, ok)
}
#[allow(clippy::too_many_arguments)]
fn send_encrypted(
socket: &UdpSocket,
port: u16,