feat(ssh): support custom SSH agent socket path

This commit is contained in:
t8y2 2026-06-18 00:15:08 +08:00
parent c4c427fb67
commit 00541291e3
13 changed files with 56 additions and 5 deletions

View File

@ -155,6 +155,7 @@ function defaultSshTunnel(): SshTunnelConfig {
connect_timeout_secs: 5,
expose_lan: false,
use_ssh_agent: false,
ssh_agent_sock_path: "",
};
}
@ -172,6 +173,7 @@ function normalizeSshTunnel(hop: Partial<SshTunnelConfig>): SshTunnelConfig {
connect_timeout_secs: Number(hop.connect_timeout_secs) || 5,
expose_lan: !!hop.expose_lan,
use_ssh_agent: !!hop.use_ssh_agent,
ssh_agent_sock_path: hop.ssh_agent_sock_path || "",
};
}
@ -248,6 +250,7 @@ function sshLayersForConfig(config: LegacyConnectionConfig): SshTunnelConfig[] {
connect_timeout_secs: config.ssh_connect_timeout_secs || 5,
expose_lan: config.ssh_expose_lan || false,
use_ssh_agent: false,
ssh_agent_sock_path: "",
}),
];
}
@ -3477,6 +3480,10 @@ function openExternalUrl(url: string) {
<span class="text-xs text-muted-foreground">{{ t("connection.sshUseAgent") }}</span>
</label>
</div>
<div v-if="selectedSshLayer.use_ssh_agent" class="grid grid-cols-4 items-center gap-4">
<Label class="text-right text-xs">{{ t("connection.sshAgentSockPath") }}</Label>
<Input v-model="selectedSshLayer.ssh_agent_sock_path" class="col-span-3" :placeholder="t('connection.sshAgentSockPathPlaceholder')" :disabled="selectedSshLayer.enabled === false" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<span />
<label class="col-span-3 flex items-center gap-2 cursor-pointer">

View File

@ -303,6 +303,8 @@ export default {
sshHopInvalidPort: "{hop}: SSH port must be between 1 and 65535",
sshHopInvalidAuth: "{hop}: password, key path, or ssh-agent is required",
sshUseAgent: "Use ssh-agent",
sshAgentSockPath: "Agent Socket Path",
sshAgentSockPathPlaceholder: "e.g. ~/.ssh/agent.sock",
sshHopInvalidTimeout: "{hop}: SSH timeout must be between 1 and 300 seconds",
connectTimeout: "Connection Timeout (seconds)",
queryTimeout: "Query Timeout (seconds)",

View File

@ -283,6 +283,8 @@ export default {
sshHopInvalidPort: "{hop}: el puerto SSH debe estar entre 1 y 65535",
sshHopInvalidAuth: "{hop}: se requiere contraseña, ruta de clave o ssh-agent",
sshUseAgent: "Usar ssh-agent",
sshAgentSockPath: "Ruta del socket del agente",
sshAgentSockPathPlaceholder: "ej. ~/.ssh/agent.sock",
sshHopInvalidTimeout: "{hop}: el tiempo de espera SSH debe estar entre 1 y 300 segundos",
connectTimeout: "Tiempo de espera de conexión (segundos)",
queryTimeout: "Tiempo de espera de consulta (segundos)",

View File

@ -288,6 +288,8 @@ export default {
sshHopInvalidPort: "{hop}: La porta SSH deve essere compresa tra 1 e 65535",
sshHopInvalidAuth: "{hop}: password, percorso chiave o ssh-agent richiesti",
sshUseAgent: "Usa ssh-agent",
sshAgentSockPath: "Percorso socket agent",
sshAgentSockPathPlaceholder: "es. ~/.ssh/agent.sock",
sshHopInvalidTimeout: "{hop}: Il timeout SSH deve essere compreso tra 1 e 300 secondi",
connectTimeout: "Timeout Connessione (secondi)",
queryTimeout: "Timeout Query (secondi)",

View File

@ -302,6 +302,8 @@ export default {
sshHopInvalidPort: "{hop}: SSHポートは1〜65535の範囲で指定してください",
sshHopInvalidAuth: "{hop}: パスワード、鍵パス、またはssh-agentが必要です",
sshUseAgent: "ssh-agentを使用",
sshAgentSockPath: "Agentソケットパス",
sshAgentSockPathPlaceholder: "例: ~/.ssh/agent.sock",
sshHopInvalidTimeout: "{hop}: SSHタイムアウトは1〜300秒の範囲で指定してください",
connectTimeout: "接続タイムアウト(秒)",
queryTimeout: "クエリタイムアウト(秒)",

View File

@ -288,6 +288,8 @@ export default {
sshHopInvalidPort: "{hop}: a porta SSH deve estar entre 1 e 65535",
sshHopInvalidAuth: "{hop}: senha, caminho da chave ou ssh-agent é obrigatório",
sshUseAgent: "Usar ssh-agent",
sshAgentSockPath: "Caminho do socket do agente",
sshAgentSockPathPlaceholder: "ex. ~/.ssh/agent.sock",
sshHopInvalidTimeout: "{hop}: o timeout SSH deve estar entre 1 e 300 segundos",
connectTimeout: "Timeout de Conexão (segundos)",
queryTimeout: "Timeout de Consulta (segundos)",

View File

@ -304,6 +304,8 @@ export default {
sshHopInvalidPort: "{hop}SSH 端口必须在 1 到 65535 之间",
sshHopInvalidAuth: "{hop}:需要填写密码、密钥路径或勾选使用 ssh-agent",
sshUseAgent: "使用 ssh-agent",
sshAgentSockPath: "Agent Socket 路径",
sshAgentSockPathPlaceholder: "如 ~/.ssh/agent.sock",
sshHopInvalidTimeout: "{hop}SSH 超时时间必须在 1 到 300 秒之间",
connectTimeout: "连接超时(秒)",
queryTimeout: "查询超时(秒)",

View File

@ -288,6 +288,8 @@ export default {
sshHopInvalidPort: "{hop}SSH 埠必須在 1 到 65535 之間",
sshHopInvalidAuth: "{hop}:需要填寫密碼、金鑰路徑或勾選使用 ssh-agent",
sshUseAgent: "使用 ssh-agent",
sshAgentSockPath: "Agent Socket 路徑",
sshAgentSockPathPlaceholder: "如 ~/.ssh/agent.sock",
sshHopInvalidTimeout: "{hop}SSH 逾時時間必須在 1 到 300 秒之間",
connectTimeout: "連線逾時(秒)",
queryTimeout: "查詢逾時(秒)",

View File

@ -122,6 +122,7 @@ export interface SshTunnelConfig {
connect_timeout_secs?: number;
expose_lan?: boolean;
use_ssh_agent?: boolean;
ssh_agent_sock_path?: string;
}
export interface ProxyTunnelConfig {

View File

@ -65,6 +65,7 @@ async fn connect_and_authenticate(
ssh_key_path: &str,
ssh_key_passphrase: &str,
use_ssh_agent: bool,
ssh_agent_sock_path: &str,
connect_timeout_secs: u64,
) -> Result<Handle<SshClient>, String> {
let config = Arc::new(ssh_client_config());
@ -108,7 +109,7 @@ async fn connect_and_authenticate(
return Err("SSH password authentication failed".to_string());
}
} else if use_ssh_agent {
match try_authenticate_with_agent(&mut session, ssh_user, &connect_timeout).await {
match try_authenticate_with_agent(&mut session, ssh_user, ssh_agent_sock_path, &connect_timeout).await {
Ok(()) => {}
Err(agent_err) => return Err(agent_err),
}
@ -124,13 +125,26 @@ async fn connect_and_authenticate(
async fn try_authenticate_with_agent(
session: &mut Handle<SshClient>,
ssh_user: &str,
ssh_agent_sock_path: &str,
connect_timeout: &Duration,
) -> Result<(), String> {
#[cfg(unix)]
let mut agent = match AgentClient::connect_env().await {
Ok(a) => a,
Err(e) => {
return Err(format!("No SSH password or key provided, and ssh-agent is unavailable: {e}"));
let mut agent = if ssh_agent_sock_path.is_empty() {
match AgentClient::connect_env().await {
Ok(a) => a,
Err(e) => {
return Err(format!("No SSH password or key provided, and ssh-agent is unavailable: {e}"));
}
}
} else {
match AgentClient::connect_uds(ssh_agent_sock_path).await {
Ok(a) => a,
Err(e) => {
return Err(format!(
"No SSH password or key provided, and ssh-agent at '{}' is unavailable: {e}",
ssh_agent_sock_path
));
}
}
};
@ -430,6 +444,7 @@ async fn tunnel_reconnect_loop(
ssh_key_path: String,
ssh_key_passphrase: String,
use_ssh_agent: bool,
ssh_agent_sock_path: String,
connect_timeout_secs: u64,
listener: TcpListener,
remote_host: String,
@ -464,6 +479,7 @@ async fn tunnel_reconnect_loop(
&ssh_key_path,
&ssh_key_passphrase,
use_ssh_agent,
&ssh_agent_sock_path,
connect_timeout_secs,
)
.await
@ -533,6 +549,7 @@ impl TunnelManager {
ssh_key_path: &str,
ssh_key_passphrase: &str,
use_ssh_agent: bool,
ssh_agent_sock_path: &str,
connect_timeout_secs: u64,
remote_host: &str,
remote_port: u16,
@ -555,6 +572,7 @@ impl TunnelManager {
ssh_key_path,
ssh_key_passphrase,
use_ssh_agent,
ssh_agent_sock_path,
connect_timeout_secs,
remote_host,
remote_port,
@ -624,6 +642,7 @@ impl TunnelManager {
&hop.key_path,
&hop.key_passphrase,
hop.use_ssh_agent,
&hop.ssh_agent_sock_path,
effective_hop_timeout(hop),
&target_host,
target_port,
@ -671,6 +690,7 @@ async fn spawn_tunnel(
ssh_key_path: &str,
ssh_key_passphrase: &str,
use_ssh_agent: bool,
ssh_agent_sock_path: &str,
connect_timeout_secs: u64,
remote_host: &str,
remote_port: u16,
@ -691,6 +711,7 @@ async fn spawn_tunnel(
ssh_key_path,
ssh_key_passphrase,
use_ssh_agent,
ssh_agent_sock_path,
connect_timeout_secs,
)
.await?;
@ -704,6 +725,7 @@ async fn spawn_tunnel(
ssh_key_path.to_string(),
ssh_key_passphrase.to_string(),
use_ssh_agent,
ssh_agent_sock_path.to_string(),
connect_timeout_secs,
listener,
remote_host.to_string(),

View File

@ -59,6 +59,7 @@ pub async fn start_transport_layers(
&ssh.key_path,
&ssh.key_passphrase,
ssh.use_ssh_agent,
&ssh.ssh_agent_sock_path,
effective_ssh_connect_timeout_secs(ssh.connect_timeout_secs),
&target_endpoint.host,
target_endpoint.port,

View File

@ -148,6 +148,11 @@ pub struct SshTunnelConfig {
pub expose_lan: bool,
#[serde(default)]
pub use_ssh_agent: bool,
/// Custom SSH agent socket path (e.g. `~/.ssh/agent.sock`).
/// When set and `use_ssh_agent` is true, this path is used instead of
/// the `SSH_AUTH_SOCK` environment variable.
#[serde(default)]
pub ssh_agent_sock_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]

View File

@ -45,6 +45,7 @@ export interface SshTunnelConfig {
connect_timeout_secs?: number;
expose_lan?: boolean;
use_ssh_agent?: boolean;
ssh_agent_sock_path?: string;
}
export interface ProxyTunnelConfig {