From 1b1278f39ec3158699079a8ae852f8635376f063 Mon Sep 17 00:00:00 2001 From: DuProcess <273172371+DuProcess@users.noreply.github.com> Date: Sat, 13 Jun 2026 22:30:15 -0400 Subject: [PATCH] Add explicit environment propagation --- docs/NATIVE_V1_SPEC.md | 3 + src/bin/dosh-client.rs | 195 +++++++++++++++++++++++++++++++++++-- src/bin/dosh-server.rs | 152 +++++++++++++++++++++++++++-- src/config.rs | 26 +++++ src/native.rs | 9 ++ src/protocol.rs | 6 +- src/pty.rs | 4 + src/ssh_agent.rs | 1 + tests/integration_smoke.rs | 1 + 9 files changed, 380 insertions(+), 17 deletions(-) diff --git a/docs/NATIVE_V1_SPEC.md b/docs/NATIVE_V1_SPEC.md index f14bbed..bde2365 100644 --- a/docs/NATIVE_V1_SPEC.md +++ b/docs/NATIVE_V1_SPEC.md @@ -491,6 +491,8 @@ credential_cache = "~/.local/share/dosh/credentials" identity_files = ["~/.ssh/id_ed25519"] use_ssh_agent = true forward_agent = false +send_env = ["LANG", "LC_*", "TERM", "COLORTERM"] +set_env = {} forwardings = [] ``` @@ -505,6 +507,7 @@ attach_ticket_ttl_secs = 86400 allow_tcp_forwarding = true allow_remote_forwarding = false allow_agent_forwarding = false +accept_env = ["LANG", "LC_*", "TERM", "COLORTERM"] ``` ## 15. Migration Plan diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index 5989cfa..1e751d7 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -9,8 +9,8 @@ use dosh::auth::{ use dosh::config::{expand_tilde, load_client_config, load_hosts_config, load_server_config}; use dosh::crypto; use dosh::native::{ - ForwardingKind, ForwardingRequest, KnownHostStatus, TrustResult, derive_native_session_key, - generate_native_ephemeral, host_fingerprint, load_ed25519_identity, + EnvVar, ForwardingKind, ForwardingRequest, KnownHostStatus, TrustResult, + derive_native_session_key, generate_native_ephemeral, host_fingerprint, load_ed25519_identity, load_ed25519_identity_with_passphrase, parse_host_public_key_line, remove_trusted_host, sign_user_auth, trust_host, verify_known_host, verify_server_hello, }; @@ -270,6 +270,7 @@ async fn main() -> Result<()> { } else { None }; + let cache_requested_env = requested_env(&config, &host, resolved_ssh_config.as_ref()); let socket = UdpSocket::bind("0.0.0.0:0").await?; @@ -278,7 +279,8 @@ async fn main() -> Result<()> { cached.udp_port = dosh_port; log_timing(args.verbose, "credential_lookup_end", started.elapsed()); if config.cache_attach_tickets && allow_cache { - match try_ticket_attach(&socket, &cached, cols, rows).await { + match try_ticket_attach(&socket, &cached, cols, rows, cache_requested_env.clone()).await + { Ok((frame, cred)) => { log_timing(args.verbose, "udp_ticket_attach_ready", started.elapsed()); save_cache(&cache_path, &cred)?; @@ -347,6 +349,10 @@ async fn main() -> Result<()> { } if !args.local_auth && auth_allows(&auth_preference, "native") { + if resolved_ssh_config.is_none() { + resolved_ssh_config = Some(ssh_config(&server, ssh_port).unwrap_or_default()); + } + let cold_requested_env = requested_env(&config, &host, resolved_ssh_config.as_ref()); let native_start = Instant::now(); match try_native_auth( &socket, @@ -363,6 +369,7 @@ async fn main() -> Result<()> { rows, forwarding_requests(&local_forwards, &remote_forwards, &dynamic_forwards), resolved_ssh_config.as_ref(), + cold_requested_env, ) .await { @@ -408,6 +415,10 @@ async fn main() -> Result<()> { } let bootstrap_start = Instant::now(); + if !args.local_auth && resolved_ssh_config.is_none() { + resolved_ssh_config = Some(ssh_config(&server, ssh_port).unwrap_or_default()); + } + let cold_requested_env = requested_env(&config, &host, resolved_ssh_config.as_ref()); let bootstrap = if args.local_auth { local_bootstrap(&session, &mode, cols, rows, dosh_port, target_udp_host)? } else { @@ -429,7 +440,8 @@ async fn main() -> Result<()> { }; log_timing(args.verbose, "ssh_bootstrap", bootstrap_start.elapsed()); - let (ok, mut cred) = bootstrap_attach(&socket, &server, &bootstrap, cols, rows).await?; + let (ok, mut cred) = + bootstrap_attach(&socket, &server, &bootstrap, cols, rows, cold_requested_env).await?; cred.last_rendered_seq = ok.initial_seq; if allow_cache { save_cache(&cache_path, &cred)?; @@ -1181,6 +1193,8 @@ struct SshConfig { identities_only: bool, proxy_jump: Option, proxy_command: Option, + send_env: Vec, + set_env: Vec, } fn ssh_config(server: &str, ssh_port: Option) -> Result { @@ -1227,15 +1241,117 @@ fn parse_ssh_config(raw: &str) -> SshConfig { config.proxy_jump = Some(value.to_string()); } else if key.eq_ignore_ascii_case("proxycommand") && !empty_or_none(value) { config.proxy_command = Some(value.to_string()); + } else if key.eq_ignore_ascii_case("sendenv") && !empty_or_none(value) { + config + .send_env + .extend(value.split_whitespace().map(ToString::to_string)); + } else if key.eq_ignore_ascii_case("setenv") && !empty_or_none(value) { + config.set_env.extend(parse_set_env_values(value)); } } config } +fn parse_set_env_values(value: &str) -> Vec { + value + .split_whitespace() + .filter_map(|entry| { + let (name, value) = entry.split_once('=')?; + if valid_env_name(name) && !value.as_bytes().contains(&0) { + Some(EnvVar { + name: name.to_string(), + value: value.to_string(), + }) + } else { + None + } + }) + .collect() +} + fn empty_or_none(value: &str) -> bool { value.is_empty() || value.eq_ignore_ascii_case("none") } +fn requested_env( + config: &dosh::config::ClientConfig, + host: &dosh::config::HostConfig, + ssh_config: Option<&SshConfig>, +) -> Vec { + let mut values = BTreeMap::new(); + let mut patterns = host + .send_env + .clone() + .unwrap_or_else(|| config.send_env.clone()); + if let Some(ssh_config) = ssh_config { + patterns.extend(ssh_config.send_env.clone()); + } + + for (name, value) in std::env::vars() { + if valid_env_name(&name) && patterns.iter().any(|pattern| glob_matches(pattern, &name)) { + values.insert(name, value); + } + } + for (name, value) in &config.set_env { + if valid_env_name(name) && !value.as_bytes().contains(&0) { + values.insert(name.clone(), value.clone()); + } + } + for (name, value) in &host.set_env { + if valid_env_name(name) && !value.as_bytes().contains(&0) { + values.insert(name.clone(), value.clone()); + } + } + if let Some(ssh_config) = ssh_config { + for env in &ssh_config.set_env { + values.insert(env.name.clone(), env.value.clone()); + } + } + values + .into_iter() + .map(|(name, value)| EnvVar { name, value }) + .collect() +} + +fn valid_env_name(name: &str) -> bool { + let mut chars = name.chars(); + let Some(first) = chars.next() else { + return false; + }; + if !(first == '_' || first.is_ascii_alphabetic()) { + return false; + } + chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric()) +} + +fn glob_matches(pattern: &str, value: &str) -> bool { + let pattern = pattern.as_bytes(); + let value = value.as_bytes(); + let (mut p, mut v) = (0, 0); + let mut star = None; + let mut star_value = 0; + while v < value.len() { + if p < pattern.len() && (pattern[p] == b'?' || pattern[p] == value[v]) { + p += 1; + v += 1; + } else if p < pattern.len() && pattern[p] == b'*' { + star = Some(p); + p += 1; + star_value = v; + } else if let Some(star_pos) = star { + p = star_pos + 1; + star_value += 1; + v = star_value; + } else { + return false; + } + } + while p < pattern.len() && pattern[p] == b'*' { + p += 1; + } + p == pattern.len() +} + fn resolve_addr(host: &str, port: u16) -> Result { (host, port) .to_socket_addrs() @@ -1259,6 +1375,7 @@ async fn try_native_auth( rows: u16, requested_forwardings: Vec, ssh_config_hint: Option<&SshConfig>, + requested_env: Vec, ) -> Result<(Frame, CachedCredential)> { let addr = resolve_addr(udp_host, udp_port)?; let owned_ssh_config; @@ -1285,6 +1402,7 @@ async fn try_native_auth( supported_user_key_algorithms: vec!["ssh-ed25519".to_string()], cached_host_key_fingerprint: None, attach_ticket_envelope: None, + requested_env, }; let packet = protocol::encode_plain( PacketKind::NativeClientHello, @@ -1440,6 +1558,7 @@ async fn try_native_auth_check( supported_user_key_algorithms: vec!["ssh-ed25519".to_string()], cached_host_key_fingerprint: None, attach_ticket_envelope: None, + requested_env: Vec::new(), }; let packet = protocol::encode_plain( PacketKind::NativeClientHello, @@ -1651,12 +1770,14 @@ async fn bootstrap_attach( bootstrap: &BootstrapResponse, cols: u16, rows: u16, + requested_env: Vec, ) -> Result<(AttachOk, CachedCredential)> { let addr = resolve_addr(&bootstrap.udp_host, bootstrap.udp_port)?; let req = BootstrapAttachRequest { bootstrap: bootstrap.clone(), cols, rows, + requested_env, }; let body = protocol::to_body(&req)?; let packet = @@ -1701,6 +1822,7 @@ async fn try_ticket_attach( cached: &CachedCredential, cols: u16, rows: u16, + requested_env: Vec, ) -> Result<(Frame, CachedCredential)> { let addr = resolve_addr(&cached.udp_host, cached.udp_port)?; let client_nonce = crypto::random_12(); @@ -1714,6 +1836,7 @@ async fn try_ticket_attach( mode: cached.mode.clone(), cols, rows, + requested_env, })?; let ciphertext = crypto::seal( &request_key, @@ -2453,7 +2576,8 @@ async fn reconnect( return Ok(Some(frame)); } - let Ok((frame, next)) = try_ticket_attach(socket, cred, size.0, size.1).await else { + let Ok((frame, next)) = try_ticket_attach(socket, cred, size.0, size.1, Vec::new()).await + else { return Ok(None); }; *cred = next; @@ -2840,10 +2964,11 @@ mod tests { DynamicForward, FrameBuffer, LocalForward, Predictor, RemoteForward, SshConfig, auth_allows, load_first_native_identity_with_prompt, parse_dynamic_forward, parse_local_forward, parse_remote_forward, parse_ssh_config, raw_contains_host_table, - recv_response_until, ssh_destination_host, ssh_username, startup_command, + recv_response_until, requested_env, ssh_destination_host, ssh_username, startup_command, toml_bare_key_or_quoted, }; - use dosh::config::ClientConfig; + use dosh::config::{ClientConfig, HostConfig}; + use dosh::native::EnvVar; use dosh::protocol::{self, Frame, PacketKind}; use ed25519_dalek::{SigningKey, VerifyingKey}; @@ -2891,7 +3016,9 @@ mod tests { identitiesonly yes\n\ proxyjump bastion\n\ proxycommand ssh bastion -W %h:%p\n\ - identityfile none\n", + identityfile none\n\ + sendenv LANG LC_*\n\ + setenv DOSH_MODE=native DOSH_COLOR=true\n", ); assert_eq!( @@ -2904,9 +3031,61 @@ mod tests { parsed.proxy_command, Some("ssh bastion -W %h:%p".to_string()) ); + assert_eq!( + parsed.send_env, + vec!["LANG".to_string(), "LC_*".to_string()] + ); + assert_eq!( + parsed.set_env, + vec![ + EnvVar { + name: "DOSH_MODE".to_string(), + value: "native".to_string() + }, + EnvVar { + name: "DOSH_COLOR".to_string(), + value: "true".to_string() + } + ] + ); assert!(parsed.identity_files.is_empty()); } + #[test] + fn requested_env_merges_config_host_and_ssh_setenv() { + let mut config = ClientConfig::default(); + config.send_env.clear(); + config.set_env.insert("DOSH_MODE".into(), "client".into()); + config.set_env.insert("DOSH_KEEP".into(), "yes".into()); + let mut host = HostConfig::default(); + host.set_env.insert("DOSH_MODE".into(), "host".into()); + let ssh_config = SshConfig { + set_env: vec![EnvVar { + name: "DOSH_SSH".to_string(), + value: "true".to_string(), + }], + ..SshConfig::default() + }; + + assert_eq!( + requested_env(&config, &host, Some(&ssh_config)), + vec![ + EnvVar { + name: "DOSH_KEEP".to_string(), + value: "yes".to_string() + }, + EnvVar { + name: "DOSH_MODE".to_string(), + value: "host".to_string() + }, + EnvVar { + name: "DOSH_SSH".to_string(), + value: "true".to_string() + } + ] + ); + } + #[test] fn detects_existing_imported_host_tables() { assert!(raw_contains_host_table( diff --git a/src/bin/dosh-server.rs b/src/bin/dosh-server.rs index a134471..e241236 100644 --- a/src/bin/dosh-server.rs +++ b/src/bin/dosh-server.rs @@ -7,9 +7,9 @@ use dosh::auth::{ use dosh::config::{ServerConfig, load_server_config}; use dosh::crypto; use dosh::native::{ - ForwardingKind, ForwardingRequest, NativeAuthOk, NativeServerHello, derive_native_session_key, - generate_native_ephemeral, host_public_key, host_public_key_line, load_or_create_host_key, - sign_server_hello, verify_native_user_auth_from_config, + EnvVar, ForwardingKind, ForwardingRequest, NativeAuthOk, NativeServerHello, + derive_native_session_key, generate_native_ephemeral, host_public_key, host_public_key_line, + load_or_create_host_key, sign_server_hello, verify_native_user_auth_from_config, }; use dosh::protocol::{ self, AttachOk, AttachReject, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input, @@ -115,7 +115,7 @@ async fn serve(config_path: Option) -> Result<()> { { let mut locked = state.lock().expect("server state poisoned"); for session in config.prewarm_sessions.clone() { - locked.ensure_session(&session, 80, 24, "read-write")?; + locked.ensure_session(&session, 80, 24, "read-write", &[])?; } } @@ -225,7 +225,15 @@ impl ServerState { } } - fn ensure_session(&mut self, name: &str, cols: u16, rows: u16, mode: &str) -> Result<()> { + fn ensure_session( + &mut self, + name: &str, + cols: u16, + rows: u16, + mode: &str, + requested_env: &[EnvVar], + ) -> Result<()> { + let env = accepted_env(&self.config, requested_env)?; if let Some(session) = self.sessions.get_mut(name) { if session.pty.is_none() && mode_uses_pty(mode) { session.pty = Some(spawn_pty_session( @@ -233,6 +241,7 @@ impl ServerState { &self.config.shell, cols.max(1), rows.max(1), + &env, self.pty_tx.clone(), )?); } @@ -244,6 +253,7 @@ impl ServerState { &self.config.shell, cols.max(1), rows.max(1), + &env, self.pty_tx.clone(), )?) } else { @@ -271,6 +281,69 @@ fn mode_allows_terminal_updates(mode: &str) -> bool { mode != "view-only" && mode != "forward-only" } +fn accepted_env(config: &ServerConfig, requested: &[EnvVar]) -> Result> { + let mut env = Vec::new(); + for entry in requested { + anyhow::ensure!( + valid_env_name(&entry.name), + "invalid environment variable name {:?}", + entry.name + ); + anyhow::ensure!( + !entry.value.as_bytes().contains(&0), + "environment variable {:?} contains NUL", + entry.name + ); + if config + .accept_env + .iter() + .any(|pattern| glob_matches(pattern, &entry.name)) + { + env.push((entry.name.clone(), entry.value.clone())); + } + } + Ok(env) +} + +fn valid_env_name(name: &str) -> bool { + let mut chars = name.chars(); + let Some(first) = chars.next() else { + return false; + }; + if !(first == '_' || first.is_ascii_alphabetic()) { + return false; + } + chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric()) +} + +fn glob_matches(pattern: &str, value: &str) -> bool { + let pattern = pattern.as_bytes(); + let value = value.as_bytes(); + let (mut p, mut v) = (0, 0); + let mut star = None; + let mut star_value = 0; + while v < value.len() { + if p < pattern.len() && (pattern[p] == b'?' || pattern[p] == value[v]) { + p += 1; + v += 1; + } else if p < pattern.len() && pattern[p] == b'*' { + star = Some(p); + p += 1; + star_value = v; + } else if let Some(star_pos) = star { + p = star_pos + 1; + star_value += 1; + v = star_value; + } else { + return false; + } + } + while p < pattern.len() && pattern[p] == b'*' { + p += 1; + } + p == pattern.len() +} + async fn handle_packet( state: &Arc>, socket: &Arc, @@ -495,7 +568,13 @@ async fn handle_native_user_auth( if !locked.sessions.contains_key(&session_name) && !locked.config.create_on_attach { return Err(anyhow!("session does not exist")); } - locked.ensure_session(&session_name, cols, rows, &mode)?; + locked.ensure_session( + &session_name, + cols, + rows, + &mode, + &pending.client.requested_env, + )?; let session_key = pending.session_key; let key_id = protocol::session_key_id(&session_key); let attach_ticket_psk = crypto::random_32(); @@ -636,6 +715,7 @@ async fn handle_bootstrap_attach( req.cols, req.rows, &req.bootstrap.mode, + &req.requested_env, )?; let session = locked .sessions @@ -742,7 +822,13 @@ async fn handle_ticket_attach( 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)?; + locked.ensure_session( + &req.session, + req.cols, + req.rows, + &req.mode, + &req.requested_env, + )?; let session = locked .sessions .get_mut(&req.session) @@ -1709,9 +1795,59 @@ mod tests { let mut state = ServerState::new(ServerConfig::default(), [0u8; 32], pty_tx); state - .ensure_session("forward", 80, 24, "forward-only") + .ensure_session("forward", 80, 24, "forward-only", &[]) .unwrap(); assert!(state.sessions["forward"].pty.is_none()); } + + #[test] + fn accepted_env_filters_by_server_patterns() { + let config = ServerConfig { + accept_env: vec!["LANG".to_string(), "LC_*".to_string()], + ..ServerConfig::default() + }; + + assert_eq!( + accepted_env( + &config, + &[ + EnvVar { + name: "LANG".to_string(), + value: "en_US.UTF-8".to_string() + }, + EnvVar { + name: "LC_TIME".to_string(), + value: "C".to_string() + }, + EnvVar { + name: "SECRET_TOKEN".to_string(), + value: "nope".to_string() + } + ] + ) + .unwrap(), + vec![ + ("LANG".to_string(), "en_US.UTF-8".to_string()), + ("LC_TIME".to_string(), "C".to_string()) + ] + ); + } + + #[test] + fn accepted_env_rejects_invalid_names() { + let err = accepted_env( + &ServerConfig::default(), + &[EnvVar { + name: "BAD-NAME".to_string(), + value: "value".to_string(), + }], + ) + .unwrap_err(); + + assert!( + err.to_string() + .contains("invalid environment variable name") + ); + } } diff --git a/src/config.rs b/src/config.rs index 5430a6e..5b8f8f9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -34,6 +34,8 @@ pub struct ServerConfig { pub allow_remote_forwarding: bool, #[serde(default)] pub allow_agent_forwarding: bool, + #[serde(default = "default_accept_env")] + pub accept_env: Vec, } impl Default for ServerConfig { @@ -60,6 +62,7 @@ impl Default for ServerConfig { allow_tcp_forwarding: true, allow_remote_forwarding: false, allow_agent_forwarding: false, + accept_env: default_accept_env(), } } } @@ -94,6 +97,10 @@ pub struct ClientConfig { pub use_ssh_agent: bool, #[serde(default)] pub forward_agent: bool, + #[serde(default = "default_send_env")] + pub send_env: Vec, + #[serde(default)] + pub set_env: HashMap, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -104,6 +111,10 @@ pub struct HostConfig { pub ssh_port: Option, pub default_command: Option, pub predict: Option, + #[serde(default)] + pub send_env: Option>, + #[serde(default)] + pub set_env: HashMap, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -135,6 +146,8 @@ impl Default for ClientConfig { identity_files: default_identity_files(), use_ssh_agent: true, forward_agent: false, + send_env: default_send_env(), + set_env: HashMap::new(), } } } @@ -174,6 +187,19 @@ fn default_identity_files() -> Vec { vec!["~/.ssh/id_ed25519".to_string()] } +fn default_send_env() -> Vec { + vec![ + "LANG".to_string(), + "LC_*".to_string(), + "TERM".to_string(), + "COLORTERM".to_string(), + ] +} + +fn default_accept_env() -> Vec { + default_send_env() +} + pub fn load_server_config(path: Option) -> Result { let path = path.unwrap_or_else(|| expand_tilde("~/.config/dosh/server.toml")); if !path.exists() { diff --git a/src/native.rs b/src/native.rs index 7baf056..d3af508 100644 --- a/src/native.rs +++ b/src/native.rs @@ -46,6 +46,14 @@ pub struct NativeClientHello { pub supported_user_key_algorithms: Vec, pub cached_host_key_fingerprint: Option, pub attach_ticket_envelope: Option>, + #[serde(default)] + pub requested_env: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EnvVar { + pub name: String, + pub value: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -1234,6 +1242,7 @@ mod tests { supported_user_key_algorithms: vec!["ssh-ed25519".to_string()], cached_host_key_fingerprint: None, attach_ticket_envelope: None, + requested_env: Vec::new(), } } diff --git a/src/protocol.rs b/src/protocol.rs index ad0899b..ebbc2fc 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,6 +1,6 @@ use crate::auth::BootstrapResponse; use crate::crypto; -use crate::native::{NativeAuthOk, NativeClientHello, NativeServerHello, NativeUserAuth}; +use crate::native::{EnvVar, NativeAuthOk, NativeClientHello, NativeServerHello, NativeUserAuth}; use anyhow::{Context, Result, bail}; use serde::{Deserialize, Serialize}; @@ -235,6 +235,8 @@ pub struct BootstrapAttachRequest { pub bootstrap: BootstrapResponse, pub cols: u16, pub rows: u16, + #[serde(default)] + pub requested_env: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -250,6 +252,8 @@ pub struct TicketAttachBody { pub mode: String, pub cols: u16, pub rows: u16, + #[serde(default)] + pub requested_env: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/pty.rs b/src/pty.rs index 99f5b85..e20f571 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -42,6 +42,7 @@ pub fn spawn_pty_session( shell: &str, cols: u16, rows: u16, + env: &[(String, String)], tx: mpsc::UnboundedSender, ) -> Result { let pty_system = NativePtySystem::default(); @@ -56,6 +57,9 @@ pub fn spawn_pty_session( let mut cmd = CommandBuilder::new(shell); cmd.env("TERM", "xterm-256color"); cmd.env("COLORTERM", "truecolor"); + for (name, value) in env { + cmd.env(name, value); + } cmd.env("SHELL", shell); if let Some(home) = dirs::home_dir() { cmd.env("HOME", home.as_os_str()); diff --git a/src/ssh_agent.rs b/src/ssh_agent.rs index ba1ef01..779f9ca 100644 --- a/src/ssh_agent.rs +++ b/src/ssh_agent.rs @@ -262,6 +262,7 @@ mod tests { supported_user_key_algorithms: vec!["ssh-ed25519".to_string()], cached_host_key_fingerprint: None, attach_ticket_envelope: None, + requested_env: Vec::new(), } } diff --git a/tests/integration_smoke.rs b/tests/integration_smoke.rs index 5c9b04d..0acf62c 100644 --- a/tests/integration_smoke.rs +++ b/tests/integration_smoke.rs @@ -390,6 +390,7 @@ fn direct_attach( bootstrap: bootstrap.clone(), cols: 80, rows: 24, + requested_env: Vec::new(), }; let packet = protocol::encode_plain( PacketKind::BootstrapAttachRequest,