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
321 lines
11 KiB
Rust
321 lines
11 KiB
Rust
use anyhow::{Context, Result};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ServerConfig {
|
|
pub port: u16,
|
|
pub bind: String,
|
|
pub scrollback: usize,
|
|
pub auth_ttl_secs: u64,
|
|
pub attach_ticket_ttl_secs: u64,
|
|
pub allow_attach_tickets: bool,
|
|
pub client_timeout_secs: u64,
|
|
pub retransmit_window: usize,
|
|
#[serde(default = "default_output_frame_interval_ms")]
|
|
pub output_frame_interval_ms: u64,
|
|
pub default_input_mode: String,
|
|
pub prewarm_sessions: Vec<String>,
|
|
pub create_on_attach: bool,
|
|
pub shell: String,
|
|
pub sessions_dir: String,
|
|
pub secret_path: String,
|
|
#[serde(default = "default_true")]
|
|
pub native_auth: bool,
|
|
#[serde(default = "default_host_key")]
|
|
pub host_key: String,
|
|
#[serde(default = "default_authorized_keys")]
|
|
pub authorized_keys: Vec<String>,
|
|
#[serde(default = "default_native_auth_rate_limit_per_minute")]
|
|
pub native_auth_rate_limit_per_minute: u32,
|
|
/// Rotate a client's transport traffic key after this many packets in the
|
|
/// current epoch (spec §11). `0` disables the packet-count trigger.
|
|
#[serde(default = "default_rekey_after_packets")]
|
|
pub rekey_after_packets: u64,
|
|
/// Rotate a client's transport traffic key after this many wall-clock seconds
|
|
/// in the current epoch (spec §11). `0` disables the time trigger.
|
|
#[serde(default = "default_rekey_after_secs")]
|
|
pub rekey_after_secs: u64,
|
|
#[serde(default = "default_true")]
|
|
pub allow_tcp_forwarding: bool,
|
|
#[serde(default)]
|
|
pub allow_remote_forwarding: bool,
|
|
#[serde(default)]
|
|
pub allow_remote_non_loopback_bind: bool,
|
|
#[serde(default)]
|
|
pub allow_agent_forwarding: bool,
|
|
#[serde(default = "default_accept_env")]
|
|
pub accept_env: Vec<String>,
|
|
/// Run terminal sessions under per-session holder processes so shells
|
|
/// survive a quick `dosh-server` restart and clients can reconnect.
|
|
#[serde(default = "default_true")]
|
|
pub persist_sessions: bool,
|
|
}
|
|
|
|
impl Default for ServerConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
port: 50000,
|
|
bind: "0.0.0.0".to_string(),
|
|
scrollback: 5000,
|
|
auth_ttl_secs: 30,
|
|
attach_ticket_ttl_secs: 3600,
|
|
allow_attach_tickets: true,
|
|
client_timeout_secs: 2_592_000,
|
|
retransmit_window: 256,
|
|
output_frame_interval_ms: default_output_frame_interval_ms(),
|
|
default_input_mode: "read-write".to_string(),
|
|
prewarm_sessions: vec!["default".to_string()],
|
|
create_on_attach: true,
|
|
shell: std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()),
|
|
sessions_dir: "~/.local/share/dosh/sessions".to_string(),
|
|
secret_path: "~/.config/dosh/secret".to_string(),
|
|
native_auth: true,
|
|
host_key: default_host_key(),
|
|
authorized_keys: default_authorized_keys(),
|
|
native_auth_rate_limit_per_minute: default_native_auth_rate_limit_per_minute(),
|
|
rekey_after_packets: default_rekey_after_packets(),
|
|
rekey_after_secs: default_rekey_after_secs(),
|
|
allow_tcp_forwarding: true,
|
|
allow_remote_forwarding: false,
|
|
allow_remote_non_loopback_bind: false,
|
|
allow_agent_forwarding: false,
|
|
accept_env: default_accept_env(),
|
|
persist_sessions: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ClientConfig {
|
|
pub update_repo: Option<String>,
|
|
pub update_port: Option<u16>,
|
|
pub server: String,
|
|
pub dosh_host: Option<String>,
|
|
pub ssh_auth_command: Option<String>,
|
|
pub ssh_port: Option<u16>,
|
|
pub dosh_port: u16,
|
|
pub default_session: String,
|
|
pub reconnect_timeout_secs: u64,
|
|
pub view_only: bool,
|
|
#[serde(default = "default_true")]
|
|
pub predict: bool,
|
|
/// Prediction display policy: "off", "experimental" (adaptive, the default),
|
|
/// or "always". Controls when speculative local echo is shown.
|
|
#[serde(default = "default_predict_mode")]
|
|
pub predict_mode: String,
|
|
pub cache_attach_tickets: bool,
|
|
pub credential_cache: String,
|
|
#[serde(default = "default_auth_preference")]
|
|
pub auth_preference: String,
|
|
#[serde(default)]
|
|
pub trust_on_first_use: bool,
|
|
#[serde(default = "default_native_auth_timeout_ms")]
|
|
pub native_auth_timeout_ms: u64,
|
|
#[serde(default = "default_known_hosts")]
|
|
pub known_hosts: String,
|
|
#[serde(default = "default_identity_files")]
|
|
pub identity_files: Vec<String>,
|
|
#[serde(default = "default_true")]
|
|
pub use_ssh_agent: bool,
|
|
#[serde(default)]
|
|
pub forward_agent: bool,
|
|
/// Show a non-destructive, mosh-style status line on the bottom screen row
|
|
/// when UDP packets stop arriving ("[dosh] last contact Ns ago …"). Drawn
|
|
/// with save/restore cursor so it never moves the app cursor or corrupts a
|
|
/// full-screen TUI, and cleared the instant packets resume. Default on.
|
|
#[serde(default = "default_true")]
|
|
pub disconnect_status: bool,
|
|
#[serde(default = "default_escape_key")]
|
|
pub escape_key: String,
|
|
#[serde(default = "default_send_env")]
|
|
pub send_env: Vec<String>,
|
|
#[serde(default)]
|
|
pub set_env: HashMap<String, String>,
|
|
/// Optional client-side command extensions. These are just shell command
|
|
/// templates expanded into the remote session's startup input; Dosh does not
|
|
/// depend on the tools they name.
|
|
#[serde(default)]
|
|
pub extensions: HashMap<String, CommandExtension>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct HostConfig {
|
|
pub ssh: Option<String>,
|
|
pub user: Option<String>,
|
|
pub dosh_host: Option<String>,
|
|
pub port: Option<u16>,
|
|
pub ssh_port: Option<u16>,
|
|
pub default_command: Option<String>,
|
|
pub predict: Option<bool>,
|
|
#[serde(default)]
|
|
pub send_env: Option<Vec<String>>,
|
|
#[serde(default)]
|
|
pub set_env: HashMap<String, String>,
|
|
/// Per-host extension overrides. A host can replace a global extension or set
|
|
/// `disabled = true` to opt out of it.
|
|
#[serde(default)]
|
|
pub extensions: HashMap<String, CommandExtension>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct CommandExtension {
|
|
/// Remote shell command template. `{args}` expands to shell-quoted extra
|
|
/// words after the extension name. When absent, extra args are appended.
|
|
#[serde(default)]
|
|
pub command: Option<String>,
|
|
#[serde(default)]
|
|
pub description: Option<String>,
|
|
#[serde(default)]
|
|
pub disabled: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct HostsConfig {
|
|
#[serde(flatten)]
|
|
pub hosts: HashMap<String, HostConfig>,
|
|
}
|
|
|
|
impl Default for ClientConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
update_repo: None,
|
|
update_port: None,
|
|
server: "user@example.com".to_string(),
|
|
dosh_host: None,
|
|
ssh_auth_command: Some("~/.local/bin/dosh-auth".to_string()),
|
|
ssh_port: None,
|
|
dosh_port: 50000,
|
|
default_session: "new".to_string(),
|
|
reconnect_timeout_secs: 5,
|
|
view_only: false,
|
|
predict: true,
|
|
predict_mode: default_predict_mode(),
|
|
cache_attach_tickets: true,
|
|
credential_cache: "~/.local/share/dosh/credentials".to_string(),
|
|
auth_preference: default_auth_preference(),
|
|
trust_on_first_use: false,
|
|
native_auth_timeout_ms: default_native_auth_timeout_ms(),
|
|
known_hosts: default_known_hosts(),
|
|
identity_files: default_identity_files(),
|
|
use_ssh_agent: true,
|
|
forward_agent: false,
|
|
disconnect_status: true,
|
|
escape_key: default_escape_key(),
|
|
send_env: default_send_env(),
|
|
set_env: HashMap::new(),
|
|
extensions: HashMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn default_true() -> bool {
|
|
true
|
|
}
|
|
|
|
fn default_host_key() -> String {
|
|
"~/.config/dosh/host_key".to_string()
|
|
}
|
|
|
|
fn default_authorized_keys() -> Vec<String> {
|
|
vec![
|
|
"~/.ssh/authorized_keys".to_string(),
|
|
"~/.config/dosh/authorized_keys".to_string(),
|
|
]
|
|
}
|
|
|
|
fn default_native_auth_rate_limit_per_minute() -> u32 {
|
|
30
|
|
}
|
|
|
|
fn default_output_frame_interval_ms() -> u64 {
|
|
16
|
|
}
|
|
|
|
fn default_rekey_after_packets() -> u64 {
|
|
// Rotate well before any AEAD nonce-reuse concern; ChaCha20-Poly1305 with a
|
|
// per-direction monotonic seq is safe far beyond this, but a bounded epoch
|
|
// keeps forward-secrecy windows small.
|
|
100_000
|
|
}
|
|
|
|
fn default_rekey_after_secs() -> u64 {
|
|
// One hour per epoch by default.
|
|
3600
|
|
}
|
|
|
|
fn default_auth_preference() -> String {
|
|
"native,ssh".to_string()
|
|
}
|
|
|
|
fn default_native_auth_timeout_ms() -> u64 {
|
|
700
|
|
}
|
|
|
|
fn default_predict_mode() -> String {
|
|
"experimental".to_string()
|
|
}
|
|
|
|
fn default_known_hosts() -> String {
|
|
"~/.config/dosh/known_hosts".to_string()
|
|
}
|
|
|
|
fn default_escape_key() -> String {
|
|
"^]".to_string()
|
|
}
|
|
|
|
fn default_identity_files() -> Vec<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> {
|
|
let path = path.unwrap_or_else(|| expand_tilde("~/.config/dosh/server.toml"));
|
|
if !path.exists() {
|
|
return Ok(ServerConfig::default());
|
|
}
|
|
let raw = fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
|
|
toml::from_str(&raw).with_context(|| format!("parse {}", path.display()))
|
|
}
|
|
|
|
pub fn load_client_config(path: Option<PathBuf>) -> Result<ClientConfig> {
|
|
let path = path.unwrap_or_else(|| expand_tilde("~/.config/dosh/client.toml"));
|
|
if !path.exists() {
|
|
return Ok(ClientConfig::default());
|
|
}
|
|
let raw = fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
|
|
toml::from_str(&raw).with_context(|| format!("parse {}", path.display()))
|
|
}
|
|
|
|
pub fn load_hosts_config(path: Option<PathBuf>) -> Result<HostsConfig> {
|
|
let path = path.unwrap_or_else(|| expand_tilde("~/.config/dosh/hosts.toml"));
|
|
if !path.exists() {
|
|
return Ok(HostsConfig::default());
|
|
}
|
|
let raw = fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
|
|
toml::from_str(&raw).with_context(|| format!("parse {}", path.display()))
|
|
}
|
|
|
|
pub fn expand_tilde(path: &str) -> PathBuf {
|
|
if let Some(rest) = path.strip_prefix("~/")
|
|
&& let Some(home) = dirs::home_dir()
|
|
{
|
|
return home.join(rest);
|
|
}
|
|
PathBuf::from(path)
|
|
}
|