Add explicit environment propagation
This commit is contained in:
@@ -491,6 +491,8 @@ credential_cache = "~/.local/share/dosh/credentials"
|
|||||||
identity_files = ["~/.ssh/id_ed25519"]
|
identity_files = ["~/.ssh/id_ed25519"]
|
||||||
use_ssh_agent = true
|
use_ssh_agent = true
|
||||||
forward_agent = false
|
forward_agent = false
|
||||||
|
send_env = ["LANG", "LC_*", "TERM", "COLORTERM"]
|
||||||
|
set_env = {}
|
||||||
forwardings = []
|
forwardings = []
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -505,6 +507,7 @@ attach_ticket_ttl_secs = 86400
|
|||||||
allow_tcp_forwarding = true
|
allow_tcp_forwarding = true
|
||||||
allow_remote_forwarding = false
|
allow_remote_forwarding = false
|
||||||
allow_agent_forwarding = false
|
allow_agent_forwarding = false
|
||||||
|
accept_env = ["LANG", "LC_*", "TERM", "COLORTERM"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## 15. Migration Plan
|
## 15. Migration Plan
|
||||||
|
|||||||
+187
-8
@@ -9,8 +9,8 @@ use dosh::auth::{
|
|||||||
use dosh::config::{expand_tilde, load_client_config, load_hosts_config, load_server_config};
|
use dosh::config::{expand_tilde, load_client_config, load_hosts_config, load_server_config};
|
||||||
use dosh::crypto;
|
use dosh::crypto;
|
||||||
use dosh::native::{
|
use dosh::native::{
|
||||||
ForwardingKind, ForwardingRequest, KnownHostStatus, TrustResult, derive_native_session_key,
|
EnvVar, ForwardingKind, ForwardingRequest, KnownHostStatus, TrustResult,
|
||||||
generate_native_ephemeral, host_fingerprint, load_ed25519_identity,
|
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,
|
load_ed25519_identity_with_passphrase, parse_host_public_key_line, remove_trusted_host,
|
||||||
sign_user_auth, trust_host, verify_known_host, verify_server_hello,
|
sign_user_auth, trust_host, verify_known_host, verify_server_hello,
|
||||||
};
|
};
|
||||||
@@ -270,6 +270,7 @@ async fn main() -> Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
let cache_requested_env = requested_env(&config, &host, resolved_ssh_config.as_ref());
|
||||||
|
|
||||||
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||||
|
|
||||||
@@ -278,7 +279,8 @@ async fn main() -> Result<()> {
|
|||||||
cached.udp_port = dosh_port;
|
cached.udp_port = dosh_port;
|
||||||
log_timing(args.verbose, "credential_lookup_end", started.elapsed());
|
log_timing(args.verbose, "credential_lookup_end", started.elapsed());
|
||||||
if config.cache_attach_tickets && allow_cache {
|
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)) => {
|
Ok((frame, cred)) => {
|
||||||
log_timing(args.verbose, "udp_ticket_attach_ready", started.elapsed());
|
log_timing(args.verbose, "udp_ticket_attach_ready", started.elapsed());
|
||||||
save_cache(&cache_path, &cred)?;
|
save_cache(&cache_path, &cred)?;
|
||||||
@@ -347,6 +349,10 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !args.local_auth && auth_allows(&auth_preference, "native") {
|
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();
|
let native_start = Instant::now();
|
||||||
match try_native_auth(
|
match try_native_auth(
|
||||||
&socket,
|
&socket,
|
||||||
@@ -363,6 +369,7 @@ async fn main() -> Result<()> {
|
|||||||
rows,
|
rows,
|
||||||
forwarding_requests(&local_forwards, &remote_forwards, &dynamic_forwards),
|
forwarding_requests(&local_forwards, &remote_forwards, &dynamic_forwards),
|
||||||
resolved_ssh_config.as_ref(),
|
resolved_ssh_config.as_ref(),
|
||||||
|
cold_requested_env,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -408,6 +415,10 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let bootstrap_start = Instant::now();
|
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 {
|
let bootstrap = if args.local_auth {
|
||||||
local_bootstrap(&session, &mode, cols, rows, dosh_port, target_udp_host)?
|
local_bootstrap(&session, &mode, cols, rows, dosh_port, target_udp_host)?
|
||||||
} else {
|
} else {
|
||||||
@@ -429,7 +440,8 @@ async fn main() -> Result<()> {
|
|||||||
};
|
};
|
||||||
log_timing(args.verbose, "ssh_bootstrap", bootstrap_start.elapsed());
|
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;
|
cred.last_rendered_seq = ok.initial_seq;
|
||||||
if allow_cache {
|
if allow_cache {
|
||||||
save_cache(&cache_path, &cred)?;
|
save_cache(&cache_path, &cred)?;
|
||||||
@@ -1181,6 +1193,8 @@ struct SshConfig {
|
|||||||
identities_only: bool,
|
identities_only: bool,
|
||||||
proxy_jump: Option<String>,
|
proxy_jump: Option<String>,
|
||||||
proxy_command: Option<String>,
|
proxy_command: Option<String>,
|
||||||
|
send_env: Vec<String>,
|
||||||
|
set_env: Vec<EnvVar>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ssh_config(server: &str, ssh_port: Option<u16>) -> Result<SshConfig> {
|
fn ssh_config(server: &str, ssh_port: Option<u16>) -> Result<SshConfig> {
|
||||||
@@ -1227,15 +1241,117 @@ fn parse_ssh_config(raw: &str) -> SshConfig {
|
|||||||
config.proxy_jump = Some(value.to_string());
|
config.proxy_jump = Some(value.to_string());
|
||||||
} else if key.eq_ignore_ascii_case("proxycommand") && !empty_or_none(value) {
|
} else if key.eq_ignore_ascii_case("proxycommand") && !empty_or_none(value) {
|
||||||
config.proxy_command = Some(value.to_string());
|
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
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_set_env_values(value: &str) -> Vec<EnvVar> {
|
||||||
|
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 {
|
fn empty_or_none(value: &str) -> bool {
|
||||||
value.is_empty() || value.eq_ignore_ascii_case("none")
|
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<EnvVar> {
|
||||||
|
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<SocketAddr> {
|
fn resolve_addr(host: &str, port: u16) -> Result<SocketAddr> {
|
||||||
(host, port)
|
(host, port)
|
||||||
.to_socket_addrs()
|
.to_socket_addrs()
|
||||||
@@ -1259,6 +1375,7 @@ async fn try_native_auth(
|
|||||||
rows: u16,
|
rows: u16,
|
||||||
requested_forwardings: Vec<ForwardingRequest>,
|
requested_forwardings: Vec<ForwardingRequest>,
|
||||||
ssh_config_hint: Option<&SshConfig>,
|
ssh_config_hint: Option<&SshConfig>,
|
||||||
|
requested_env: Vec<EnvVar>,
|
||||||
) -> Result<(Frame, CachedCredential)> {
|
) -> Result<(Frame, CachedCredential)> {
|
||||||
let addr = resolve_addr(udp_host, udp_port)?;
|
let addr = resolve_addr(udp_host, udp_port)?;
|
||||||
let owned_ssh_config;
|
let owned_ssh_config;
|
||||||
@@ -1285,6 +1402,7 @@ async fn try_native_auth(
|
|||||||
supported_user_key_algorithms: vec!["ssh-ed25519".to_string()],
|
supported_user_key_algorithms: vec!["ssh-ed25519".to_string()],
|
||||||
cached_host_key_fingerprint: None,
|
cached_host_key_fingerprint: None,
|
||||||
attach_ticket_envelope: None,
|
attach_ticket_envelope: None,
|
||||||
|
requested_env,
|
||||||
};
|
};
|
||||||
let packet = protocol::encode_plain(
|
let packet = protocol::encode_plain(
|
||||||
PacketKind::NativeClientHello,
|
PacketKind::NativeClientHello,
|
||||||
@@ -1440,6 +1558,7 @@ async fn try_native_auth_check(
|
|||||||
supported_user_key_algorithms: vec!["ssh-ed25519".to_string()],
|
supported_user_key_algorithms: vec!["ssh-ed25519".to_string()],
|
||||||
cached_host_key_fingerprint: None,
|
cached_host_key_fingerprint: None,
|
||||||
attach_ticket_envelope: None,
|
attach_ticket_envelope: None,
|
||||||
|
requested_env: Vec::new(),
|
||||||
};
|
};
|
||||||
let packet = protocol::encode_plain(
|
let packet = protocol::encode_plain(
|
||||||
PacketKind::NativeClientHello,
|
PacketKind::NativeClientHello,
|
||||||
@@ -1651,12 +1770,14 @@ async fn bootstrap_attach(
|
|||||||
bootstrap: &BootstrapResponse,
|
bootstrap: &BootstrapResponse,
|
||||||
cols: u16,
|
cols: u16,
|
||||||
rows: u16,
|
rows: u16,
|
||||||
|
requested_env: Vec<EnvVar>,
|
||||||
) -> Result<(AttachOk, CachedCredential)> {
|
) -> Result<(AttachOk, CachedCredential)> {
|
||||||
let addr = resolve_addr(&bootstrap.udp_host, bootstrap.udp_port)?;
|
let addr = resolve_addr(&bootstrap.udp_host, bootstrap.udp_port)?;
|
||||||
let req = BootstrapAttachRequest {
|
let req = BootstrapAttachRequest {
|
||||||
bootstrap: bootstrap.clone(),
|
bootstrap: bootstrap.clone(),
|
||||||
cols,
|
cols,
|
||||||
rows,
|
rows,
|
||||||
|
requested_env,
|
||||||
};
|
};
|
||||||
let body = protocol::to_body(&req)?;
|
let body = protocol::to_body(&req)?;
|
||||||
let packet =
|
let packet =
|
||||||
@@ -1701,6 +1822,7 @@ async fn try_ticket_attach(
|
|||||||
cached: &CachedCredential,
|
cached: &CachedCredential,
|
||||||
cols: u16,
|
cols: u16,
|
||||||
rows: u16,
|
rows: u16,
|
||||||
|
requested_env: Vec<EnvVar>,
|
||||||
) -> Result<(Frame, CachedCredential)> {
|
) -> Result<(Frame, CachedCredential)> {
|
||||||
let addr = resolve_addr(&cached.udp_host, cached.udp_port)?;
|
let addr = resolve_addr(&cached.udp_host, cached.udp_port)?;
|
||||||
let client_nonce = crypto::random_12();
|
let client_nonce = crypto::random_12();
|
||||||
@@ -1714,6 +1836,7 @@ async fn try_ticket_attach(
|
|||||||
mode: cached.mode.clone(),
|
mode: cached.mode.clone(),
|
||||||
cols,
|
cols,
|
||||||
rows,
|
rows,
|
||||||
|
requested_env,
|
||||||
})?;
|
})?;
|
||||||
let ciphertext = crypto::seal(
|
let ciphertext = crypto::seal(
|
||||||
&request_key,
|
&request_key,
|
||||||
@@ -2453,7 +2576,8 @@ async fn reconnect(
|
|||||||
return Ok(Some(frame));
|
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);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
*cred = next;
|
*cred = next;
|
||||||
@@ -2840,10 +2964,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,
|
||||||
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,
|
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 dosh::protocol::{self, Frame, PacketKind};
|
||||||
use ed25519_dalek::{SigningKey, VerifyingKey};
|
use ed25519_dalek::{SigningKey, VerifyingKey};
|
||||||
|
|
||||||
@@ -2891,7 +3016,9 @@ mod tests {
|
|||||||
identitiesonly yes\n\
|
identitiesonly yes\n\
|
||||||
proxyjump bastion\n\
|
proxyjump bastion\n\
|
||||||
proxycommand ssh bastion -W %h:%p\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!(
|
assert_eq!(
|
||||||
@@ -2904,9 +3031,61 @@ mod tests {
|
|||||||
parsed.proxy_command,
|
parsed.proxy_command,
|
||||||
Some("ssh bastion -W %h:%p".to_string())
|
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());
|
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]
|
#[test]
|
||||||
fn detects_existing_imported_host_tables() {
|
fn detects_existing_imported_host_tables() {
|
||||||
assert!(raw_contains_host_table(
|
assert!(raw_contains_host_table(
|
||||||
|
|||||||
+144
-8
@@ -7,9 +7,9 @@ use dosh::auth::{
|
|||||||
use dosh::config::{ServerConfig, load_server_config};
|
use dosh::config::{ServerConfig, load_server_config};
|
||||||
use dosh::crypto;
|
use dosh::crypto;
|
||||||
use dosh::native::{
|
use dosh::native::{
|
||||||
ForwardingKind, ForwardingRequest, NativeAuthOk, NativeServerHello, derive_native_session_key,
|
EnvVar, ForwardingKind, ForwardingRequest, NativeAuthOk, NativeServerHello,
|
||||||
generate_native_ephemeral, host_public_key, host_public_key_line, load_or_create_host_key,
|
derive_native_session_key, generate_native_ephemeral, host_public_key, host_public_key_line,
|
||||||
sign_server_hello, verify_native_user_auth_from_config,
|
load_or_create_host_key, sign_server_hello, verify_native_user_auth_from_config,
|
||||||
};
|
};
|
||||||
use dosh::protocol::{
|
use dosh::protocol::{
|
||||||
self, AttachOk, AttachReject, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input,
|
self, AttachOk, AttachReject, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input,
|
||||||
@@ -115,7 +115,7 @@ async fn serve(config_path: Option<std::path::PathBuf>) -> Result<()> {
|
|||||||
{
|
{
|
||||||
let mut locked = state.lock().expect("server state poisoned");
|
let mut locked = state.lock().expect("server state poisoned");
|
||||||
for session in config.prewarm_sessions.clone() {
|
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 let Some(session) = self.sessions.get_mut(name) {
|
||||||
if session.pty.is_none() && mode_uses_pty(mode) {
|
if session.pty.is_none() && mode_uses_pty(mode) {
|
||||||
session.pty = Some(spawn_pty_session(
|
session.pty = Some(spawn_pty_session(
|
||||||
@@ -233,6 +241,7 @@ impl ServerState {
|
|||||||
&self.config.shell,
|
&self.config.shell,
|
||||||
cols.max(1),
|
cols.max(1),
|
||||||
rows.max(1),
|
rows.max(1),
|
||||||
|
&env,
|
||||||
self.pty_tx.clone(),
|
self.pty_tx.clone(),
|
||||||
)?);
|
)?);
|
||||||
}
|
}
|
||||||
@@ -244,6 +253,7 @@ impl ServerState {
|
|||||||
&self.config.shell,
|
&self.config.shell,
|
||||||
cols.max(1),
|
cols.max(1),
|
||||||
rows.max(1),
|
rows.max(1),
|
||||||
|
&env,
|
||||||
self.pty_tx.clone(),
|
self.pty_tx.clone(),
|
||||||
)?)
|
)?)
|
||||||
} else {
|
} else {
|
||||||
@@ -271,6 +281,69 @@ fn mode_allows_terminal_updates(mode: &str) -> bool {
|
|||||||
mode != "view-only" && mode != "forward-only"
|
mode != "view-only" && mode != "forward-only"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn accepted_env(config: &ServerConfig, requested: &[EnvVar]) -> Result<Vec<(String, String)>> {
|
||||||
|
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(
|
async fn handle_packet(
|
||||||
state: &Arc<Mutex<ServerState>>,
|
state: &Arc<Mutex<ServerState>>,
|
||||||
socket: &Arc<UdpSocket>,
|
socket: &Arc<UdpSocket>,
|
||||||
@@ -495,7 +568,13 @@ async fn handle_native_user_auth(
|
|||||||
if !locked.sessions.contains_key(&session_name) && !locked.config.create_on_attach {
|
if !locked.sessions.contains_key(&session_name) && !locked.config.create_on_attach {
|
||||||
return Err(anyhow!("session does not exist"));
|
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 session_key = pending.session_key;
|
||||||
let key_id = protocol::session_key_id(&session_key);
|
let key_id = protocol::session_key_id(&session_key);
|
||||||
let attach_ticket_psk = crypto::random_32();
|
let attach_ticket_psk = crypto::random_32();
|
||||||
@@ -636,6 +715,7 @@ async fn handle_bootstrap_attach(
|
|||||||
req.cols,
|
req.cols,
|
||||||
req.rows,
|
req.rows,
|
||||||
&req.bootstrap.mode,
|
&req.bootstrap.mode,
|
||||||
|
&req.requested_env,
|
||||||
)?;
|
)?;
|
||||||
let session = locked
|
let session = locked
|
||||||
.sessions
|
.sessions
|
||||||
@@ -742,7 +822,13 @@ async fn handle_ticket_attach(
|
|||||||
if !locked.sessions.contains_key(&req.session) && !locked.config.create_on_attach {
|
if !locked.sessions.contains_key(&req.session) && !locked.config.create_on_attach {
|
||||||
return send_reject(socket, peer, "session does not exist").await;
|
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
|
let session = locked
|
||||||
.sessions
|
.sessions
|
||||||
.get_mut(&req.session)
|
.get_mut(&req.session)
|
||||||
@@ -1709,9 +1795,59 @@ mod tests {
|
|||||||
let mut state = ServerState::new(ServerConfig::default(), [0u8; 32], pty_tx);
|
let mut state = ServerState::new(ServerConfig::default(), [0u8; 32], pty_tx);
|
||||||
|
|
||||||
state
|
state
|
||||||
.ensure_session("forward", 80, 24, "forward-only")
|
.ensure_session("forward", 80, 24, "forward-only", &[])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(state.sessions["forward"].pty.is_none());
|
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")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ pub struct ServerConfig {
|
|||||||
pub allow_remote_forwarding: bool,
|
pub allow_remote_forwarding: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub allow_agent_forwarding: bool,
|
pub allow_agent_forwarding: bool,
|
||||||
|
#[serde(default = "default_accept_env")]
|
||||||
|
pub accept_env: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ServerConfig {
|
impl Default for ServerConfig {
|
||||||
@@ -60,6 +62,7 @@ impl Default for ServerConfig {
|
|||||||
allow_tcp_forwarding: true,
|
allow_tcp_forwarding: true,
|
||||||
allow_remote_forwarding: false,
|
allow_remote_forwarding: false,
|
||||||
allow_agent_forwarding: false,
|
allow_agent_forwarding: false,
|
||||||
|
accept_env: default_accept_env(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,6 +97,10 @@ pub struct ClientConfig {
|
|||||||
pub use_ssh_agent: bool,
|
pub use_ssh_agent: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub forward_agent: bool,
|
pub forward_agent: bool,
|
||||||
|
#[serde(default = "default_send_env")]
|
||||||
|
pub send_env: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub set_env: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
@@ -104,6 +111,10 @@ pub struct HostConfig {
|
|||||||
pub ssh_port: Option<u16>,
|
pub ssh_port: Option<u16>,
|
||||||
pub default_command: Option<String>,
|
pub default_command: Option<String>,
|
||||||
pub predict: Option<bool>,
|
pub predict: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub send_env: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub set_env: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
@@ -135,6 +146,8 @@ impl Default for ClientConfig {
|
|||||||
identity_files: default_identity_files(),
|
identity_files: default_identity_files(),
|
||||||
use_ssh_agent: true,
|
use_ssh_agent: true,
|
||||||
forward_agent: false,
|
forward_agent: false,
|
||||||
|
send_env: default_send_env(),
|
||||||
|
set_env: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,6 +187,19 @@ fn default_identity_files() -> Vec<String> {
|
|||||||
vec!["~/.ssh/id_ed25519".to_string()]
|
vec!["~/.ssh/id_ed25519".to_string()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_send_env() -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
"LANG".to_string(),
|
||||||
|
"LC_*".to_string(),
|
||||||
|
"TERM".to_string(),
|
||||||
|
"COLORTERM".to_string(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_accept_env() -> Vec<String> {
|
||||||
|
default_send_env()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_server_config(path: Option<PathBuf>) -> Result<ServerConfig> {
|
pub fn load_server_config(path: Option<PathBuf>) -> Result<ServerConfig> {
|
||||||
let path = path.unwrap_or_else(|| expand_tilde("~/.config/dosh/server.toml"));
|
let path = path.unwrap_or_else(|| expand_tilde("~/.config/dosh/server.toml"));
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ pub struct NativeClientHello {
|
|||||||
pub supported_user_key_algorithms: Vec<String>,
|
pub supported_user_key_algorithms: Vec<String>,
|
||||||
pub cached_host_key_fingerprint: Option<String>,
|
pub cached_host_key_fingerprint: Option<String>,
|
||||||
pub attach_ticket_envelope: Option<Vec<u8>>,
|
pub attach_ticket_envelope: Option<Vec<u8>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub requested_env: Vec<EnvVar>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct EnvVar {
|
||||||
|
pub name: String,
|
||||||
|
pub value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -1234,6 +1242,7 @@ mod tests {
|
|||||||
supported_user_key_algorithms: vec!["ssh-ed25519".to_string()],
|
supported_user_key_algorithms: vec!["ssh-ed25519".to_string()],
|
||||||
cached_host_key_fingerprint: None,
|
cached_host_key_fingerprint: None,
|
||||||
attach_ticket_envelope: None,
|
attach_ticket_envelope: None,
|
||||||
|
requested_env: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -1,6 +1,6 @@
|
|||||||
use crate::auth::BootstrapResponse;
|
use crate::auth::BootstrapResponse;
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use crate::native::{NativeAuthOk, NativeClientHello, NativeServerHello, NativeUserAuth};
|
use crate::native::{EnvVar, NativeAuthOk, NativeClientHello, NativeServerHello, NativeUserAuth};
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -235,6 +235,8 @@ pub struct BootstrapAttachRequest {
|
|||||||
pub bootstrap: BootstrapResponse,
|
pub bootstrap: BootstrapResponse,
|
||||||
pub cols: u16,
|
pub cols: u16,
|
||||||
pub rows: u16,
|
pub rows: u16,
|
||||||
|
#[serde(default)]
|
||||||
|
pub requested_env: Vec<EnvVar>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -250,6 +252,8 @@ pub struct TicketAttachBody {
|
|||||||
pub mode: String,
|
pub mode: String,
|
||||||
pub cols: u16,
|
pub cols: u16,
|
||||||
pub rows: u16,
|
pub rows: u16,
|
||||||
|
#[serde(default)]
|
||||||
|
pub requested_env: Vec<EnvVar>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ pub fn spawn_pty_session(
|
|||||||
shell: &str,
|
shell: &str,
|
||||||
cols: u16,
|
cols: u16,
|
||||||
rows: u16,
|
rows: u16,
|
||||||
|
env: &[(String, String)],
|
||||||
tx: mpsc::UnboundedSender<PtyOutput>,
|
tx: mpsc::UnboundedSender<PtyOutput>,
|
||||||
) -> Result<PtyHandle> {
|
) -> Result<PtyHandle> {
|
||||||
let pty_system = NativePtySystem::default();
|
let pty_system = NativePtySystem::default();
|
||||||
@@ -56,6 +57,9 @@ pub fn spawn_pty_session(
|
|||||||
let mut cmd = CommandBuilder::new(shell);
|
let mut cmd = CommandBuilder::new(shell);
|
||||||
cmd.env("TERM", "xterm-256color");
|
cmd.env("TERM", "xterm-256color");
|
||||||
cmd.env("COLORTERM", "truecolor");
|
cmd.env("COLORTERM", "truecolor");
|
||||||
|
for (name, value) in env {
|
||||||
|
cmd.env(name, value);
|
||||||
|
}
|
||||||
cmd.env("SHELL", shell);
|
cmd.env("SHELL", shell);
|
||||||
if let Some(home) = dirs::home_dir() {
|
if let Some(home) = dirs::home_dir() {
|
||||||
cmd.env("HOME", home.as_os_str());
|
cmd.env("HOME", home.as_os_str());
|
||||||
|
|||||||
@@ -262,6 +262,7 @@ mod tests {
|
|||||||
supported_user_key_algorithms: vec!["ssh-ed25519".to_string()],
|
supported_user_key_algorithms: vec!["ssh-ed25519".to_string()],
|
||||||
cached_host_key_fingerprint: None,
|
cached_host_key_fingerprint: None,
|
||||||
attach_ticket_envelope: None,
|
attach_ticket_envelope: None,
|
||||||
|
requested_env: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -390,6 +390,7 @@ fn direct_attach(
|
|||||||
bootstrap: bootstrap.clone(),
|
bootstrap: bootstrap.clone(),
|
||||||
cols: 80,
|
cols: 80,
|
||||||
rows: 24,
|
rows: 24,
|
||||||
|
requested_env: Vec::new(),
|
||||||
};
|
};
|
||||||
let packet = protocol::encode_plain(
|
let packet = protocol::encode_plain(
|
||||||
PacketKind::BootstrapAttachRequest,
|
PacketKind::BootstrapAttachRequest,
|
||||||
|
|||||||
Reference in New Issue
Block a user