1use crate::{Builder, Inner, IrohSsh, cli::SshOpts};
2use std::{ffi::OsString, path::Path, process::Stdio};
3
4use anyhow::bail;
5use ed25519_dalek::SECRET_KEY_LENGTH;
6use homedir::my_home;
7use std::sync::Arc;
8
9use iroh::{
10 Endpoint, EndpointId, RelayConfig, RelayUrl, SecretKey,
11 endpoint::{Connection, RelayMode},
12 protocol::{ProtocolHandler, Router},
13};
14use tokio::{
15 io::AsyncWriteExt, net::TcpStream, process::{Child, Command}
16};
17
18impl Builder {
19 pub fn new() -> Self {
20 Self {
21 secret_key: SecretKey::generate(&mut rand::rng()).to_bytes(),
22 accept_incoming: false,
23 accept_port: None,
24 key_dir: None,
25 relay_urls: Vec::new(),
26 extra_relay_urls: Vec::new(),
27 }
28 }
29
30 pub fn accept_incoming(mut self, accept_incoming: bool) -> Self {
31 self.accept_incoming = accept_incoming;
32 self
33 }
34
35 pub fn accept_port(mut self, accept_port: u16) -> Self {
36 self.accept_port = Some(accept_port);
37 self
38 }
39
40 pub fn secret_key(mut self, secret_key: &[u8; SECRET_KEY_LENGTH]) -> Self {
41 self.secret_key = *secret_key;
42 self
43 }
44
45 pub fn relay_urls(mut self, urls: Vec<RelayUrl>) -> Self {
46 self.relay_urls = urls;
47 self
48 }
49
50 pub fn extra_relay_urls(mut self, urls: Vec<RelayUrl>) -> Self {
51 self.extra_relay_urls = urls;
52 self
53 }
54
55 pub fn key_dir(mut self, key_dir: Option<std::path::PathBuf>) -> Self {
56 self.key_dir = key_dir;
57 self
58 }
59
60 pub fn dot_ssh_integration(mut self, persist: bool, service: bool) -> Self {
61 tracing::info!(
62 "dot_ssh_integration: persist={}, service={}",
63 persist,
64 service
65 );
66
67 match dot_ssh(
68 &SecretKey::from_bytes(&self.secret_key),
69 persist,
70 service,
71 self.key_dir.as_deref(),
72 ) {
73 Ok(secret_key) => {
74 tracing::info!("dot_ssh_integration: Successfully loaded/created SSH keys");
75 self.secret_key = secret_key.to_bytes();
76 }
77 Err(e) => {
78 tracing::error!(
79 "dot_ssh_integration: Failed to load/create SSH keys: {:#}",
80 e
81 );
82 eprintln!("Warning: Failed to load/create persistent SSH keys: {e:#}");
83 eprintln!("Continuing with ephemeral keys...");
84 }
85 }
86 self
87 }
88
89 pub async fn build(&mut self) -> anyhow::Result<IrohSsh> {
90 let secret_key = SecretKey::from_bytes(&self.secret_key);
92 let mut builder = Endpoint::builder().secret_key(secret_key);
93
94 if !self.relay_urls.is_empty() {
95 let relay_map = self.relay_urls.iter().cloned().collect();
96 builder = builder.relay_mode(RelayMode::Custom(relay_map));
97 } else if !self.extra_relay_urls.is_empty() {
98 let relay_map = RelayMode::Default.relay_map();
99 for url in &self.extra_relay_urls {
100 relay_map.insert(url.clone(), Arc::new(RelayConfig::from(url.clone())));
101 }
102 builder = builder.relay_mode(RelayMode::Custom(relay_map));
103 }
104
105 let endpoint = builder.bind().await?;
106
107 let mut iroh_ssh = IrohSsh {
108 public_key: *endpoint.id().as_bytes(),
109 secret_key: self.secret_key,
110 inner: None,
111 ssh_port: self.accept_port.unwrap_or(22),
112 };
113
114 let router = if self.accept_incoming {
115 Router::builder(endpoint.clone()).accept(IrohSsh::ALPN(), iroh_ssh.clone())
116 } else {
117 Router::builder(endpoint.clone())
118 }
119 .spawn();
120
121 iroh_ssh.add_inner(endpoint, router);
122
123 Ok(iroh_ssh)
124 }
125}
126
127impl Default for Builder {
128 fn default() -> Self {
129 Self::new()
130 }
131}
132
133impl IrohSsh {
134 pub fn builder() -> Builder {
135 Builder::new()
136 }
137
138 #[allow(non_snake_case)]
139 pub fn ALPN() -> Vec<u8> {
140 b"/iroh/ssh".to_vec()
141 }
142
143 fn add_inner(&mut self, endpoint: Endpoint, router: Router) {
144 self.inner = Some(Inner { endpoint, router });
145 }
146
147 pub async fn start_ssh(
148 &self,
149 target: String,
150 ssh_opts: SshOpts,
151 remote_cmd: Vec<OsString>,
152 relay_urls: &[String],
153 extra_relay_urls: &[String],
154 ) -> anyhow::Result<Child> {
155 let cmd = &mut Command::new("ssh");
156
157 let c_exe = std::env::current_exe()?;
158 let mut proxy_cmd = format!("{} proxy", c_exe.display());
159 for url in relay_urls {
160 proxy_cmd.push_str(&format!(" --relay-url {url}"));
161 }
162 for url in extra_relay_urls {
163 proxy_cmd.push_str(&format!(" --extra-relay-url {url}"));
164 }
165 proxy_cmd.push_str(" %h:%p");
166 cmd.arg("-o").arg(format!("ProxyCommand={proxy_cmd}"));
167
168 if let Some(p) = ssh_opts.port {
169 cmd.arg("-p").arg(p.to_string());
170 }
171 if let Some(id) = &ssh_opts.identity_file {
172 cmd.arg("-i").arg(id);
173 }
174 for l in &ssh_opts.local_forward {
175 cmd.arg("-L").arg(l);
176 }
177 for r in &ssh_opts.remote_forward {
178 cmd.arg("-R").arg(r);
179 }
180 for o in &ssh_opts.options {
181 cmd.arg("-o").arg(o);
182 }
183 if ssh_opts.agent {
184 cmd.arg("-A");
185 }
186 if ssh_opts.no_agent {
187 cmd.arg("-a");
188 }
189 if ssh_opts.x11_trusted {
190 cmd.arg("-Y");
191 } else if ssh_opts.x11 {
192 cmd.arg("-X");
193 }
194 if ssh_opts.no_cmd {
195 cmd.arg("-N");
196 }
197 if ssh_opts.force_tty {
198 cmd.arg("-t");
199 }
200 if ssh_opts.no_tty {
201 cmd.arg("-T");
202 }
203 for _ in 0..ssh_opts.verbose {
204 cmd.arg("-v");
205 }
206 if ssh_opts.quiet {
207 cmd.arg("-q");
208 }
209
210 cmd.arg(target);
211
212 if !remote_cmd.is_empty() {
213 cmd.args(remote_cmd.iter());
214 }
215
216 let ssh_process = cmd
217 .stdin(Stdio::inherit())
218 .stdout(Stdio::inherit())
219 .stderr(Stdio::inherit())
220 .spawn()?;
221
222 Ok(ssh_process)
223 }
224
225 pub async fn connect_pubkey(&self, endpoint_id: EndpointId) -> anyhow::Result<()> {
226 let inner = self.inner.as_ref().expect("inner not set");
227 let conn = inner
228 .endpoint
229 .connect(endpoint_id, &IrohSsh::ALPN())
230 .await?;
231 let (mut iroh_send, mut iroh_recv) = conn.open_bi().await?;
232 let (mut local_read, mut local_write) = (tokio::io::stdin(), tokio::io::stdout());
233 let a_to_b = async move {
234 let res = tokio::io::copy(&mut local_read, &mut iroh_send).await;
235 iroh_send.finish().ok();
236 res
237 };
238 let b_to_a = async move { tokio::io::copy(&mut iroh_recv, &mut local_write).await };
239
240 let (_, _) = tokio::join!(a_to_b, b_to_a);
241 Ok(())
242 }
243
244 pub async fn connect_tcpip(&self, host_addr: &str) -> anyhow::Result<()> {
245 let conn = tokio::net::TcpStream::connect(host_addr).await?;
246 let (mut tcp_read, mut tcp_write) = conn.into_split();
247 let (mut local_read, mut local_write) = (tokio::io::stdin(), tokio::io::stdout());
248 let a_to_b = async move {
249 let res = tokio::io::copy(&mut local_read, &mut tcp_write).await;
250 tcp_write.shutdown().await.ok();
251 res
252 };
253 let b_to_a = async move { tokio::io::copy(&mut tcp_read, &mut local_write).await };
254
255 let (_, _) = tokio::join!(a_to_b, b_to_a);
256 Ok(())
257 }
258
259
260 pub fn endpoint_id(&self) -> EndpointId {
261 self.inner.as_ref().expect("inner not set").endpoint.id()
262 }
263}
264
265impl ProtocolHandler for IrohSsh {
266 async fn accept(&self, connection: Connection) -> Result<(), iroh::protocol::AcceptError> {
267 let endpoint_id = connection.remote_id()?;
268
269 match connection.accept_bi().await {
270 Ok((mut iroh_send, mut iroh_recv)) => {
271 println!("Accepted bidirectional stream from {endpoint_id}");
272
273 match TcpStream::connect(format!("127.0.0.1:{}", self.ssh_port)).await {
274 Ok(mut ssh_stream) => {
275 println!("Connected to local SSH server on port {}", self.ssh_port);
276
277 let (mut local_read, mut local_write) = ssh_stream.split();
278
279 let a_to_b = async move {
280 let res = tokio::io::copy(&mut local_read, &mut iroh_send).await;
281 iroh_send.finish().ok();
282 res
283 };
284 let b_to_a =
285 async move { tokio::io::copy(&mut iroh_recv, &mut local_write).await };
286
287 let (_, _) = tokio::join!(a_to_b, b_to_a);
288 }
289 Err(e) => {
290 println!("Failed to connect to SSH server: {e}");
291 }
292 }
293 }
294 Err(e) => {
295 println!("Failed to accept bidirectional stream: {e}");
296 }
297 }
298
299 Ok(())
300 }
301}
302
303pub fn dot_ssh(
304 default_secret_key: &SecretKey,
305 persist: bool,
306 _service: bool,
307 key_dir: Option<&Path>,
308) -> anyhow::Result<SecretKey> {
309 tracing::info!(
310 "dot_ssh: Function called, persist={}, service={}, key_dir={:?}",
311 persist,
312 _service,
313 key_dir,
314 );
315
316 #[allow(unused_mut)]
317 let mut ssh_dir = if let Some(dir) = key_dir {
318 dir.to_path_buf()
319 } else {
320 let distro_home = my_home()?.ok_or_else(|| anyhow::anyhow!("home directory not found"))?;
321 distro_home.join(".ssh")
322 };
323
324 if key_dir.is_none() {
326 #[cfg(target_os = "linux")]
329 if _service {
330 ssh_dir = std::path::PathBuf::from("/root/.ssh");
331 }
332
333 #[cfg(target_os = "windows")]
335 if _service {
336 ssh_dir = std::path::PathBuf::from(crate::service::WindowsService::SERVICE_SSH_DIR);
337 tracing::info!("dot_ssh: Using service SSH dir: {}", ssh_dir.display());
338
339 if !ssh_dir.exists() {
341 tracing::info!("dot_ssh: Service SSH dir doesn't exist, creating it");
342 std::fs::create_dir_all(&ssh_dir)?;
343 }
344 }
345 }
346
347 let pub_key = ssh_dir.join("irohssh_ed25519.pub");
348 let priv_key = ssh_dir.join("irohssh_ed25519");
349
350 tracing::debug!("dot_ssh: ssh_dir exists = {}", ssh_dir.exists());
351 tracing::debug!("dot_ssh: pub_key path = {}", pub_key.display());
352 tracing::debug!("dot_ssh: priv_key path = {}", priv_key.display());
353
354 match (ssh_dir.exists(), persist) {
355 (false, false) => {
356 tracing::error!(
357 "dot_ssh: ssh_dir does not exist and persist=false: {}",
358 ssh_dir.display()
359 );
360 bail!(
361 "key directory {} does not exist, use --persist flag to create it",
362 ssh_dir.display()
363 )
364 }
365 (false, true) => {
366 tracing::info!("dot_ssh: Creating ssh_dir: {}", ssh_dir.display());
367 std::fs::create_dir_all(&ssh_dir)?;
368 println!("[INFO] created .ssh folder: {}", ssh_dir.display());
369 dot_ssh(default_secret_key, persist, _service, key_dir)
370 }
371 (true, true) => {
372 tracing::info!("dot_ssh: Branch (true, true) - directory exists, persist enabled");
373 tracing::debug!("dot_ssh: pub_key.exists() = {}", pub_key.exists());
374 tracing::debug!("dot_ssh: priv_key.exists() = {}", priv_key.exists());
375
376 if pub_key.exists() && priv_key.exists() {
378 tracing::info!("dot_ssh: Keys exist, reading them");
379 if let Ok(secret_key) = std::fs::read(priv_key.clone()) {
381 let mut sk_bytes = [0u8; SECRET_KEY_LENGTH];
382 sk_bytes.copy_from_slice(z32::decode(secret_key.as_slice())?.as_slice());
383 Ok(SecretKey::from_bytes(&sk_bytes))
384 } else {
385 bail!("failed to read secret key from {}", priv_key.display())
386 }
387 } else {
388 tracing::info!("dot_ssh: Keys don't exist, creating new keys");
389 tracing::debug!("dot_ssh: Writing to pub_key: {}", pub_key.display());
390 tracing::debug!("dot_ssh: Writing to priv_key: {}", priv_key.display());
391
392 let secret_key = default_secret_key.clone();
393 let public_key = secret_key.public();
394
395 match std::fs::write(&pub_key, z32::encode(public_key.as_bytes())) {
396 Ok(_) => {
397 tracing::info!("dot_ssh: Successfully wrote pub_key");
398 }
399 Err(e) => {
400 tracing::error!(
401 "dot_ssh: Failed to write pub_key: {} (error kind: {:?})",
402 e,
403 e.kind()
404 );
405 return Err(e.into());
406 }
407 }
408
409 match std::fs::write(&priv_key, z32::encode(&secret_key.to_bytes())) {
410 Ok(_) => {
411 tracing::info!("dot_ssh: Successfully wrote priv_key");
412 }
413 Err(e) => {
414 tracing::error!(
415 "dot_ssh: Failed to write priv_key: {} (error kind: {:?})",
416 e,
417 e.kind()
418 );
419 return Err(e.into());
420 }
421 }
422
423 Ok(secret_key)
424 }
425 }
426 (true, false) => {
427 if pub_key.exists() && priv_key.exists() {
429 if let Ok(secret_key) = std::fs::read(priv_key.clone()) {
431 let mut sk_bytes = [0u8; SECRET_KEY_LENGTH];
432 sk_bytes.copy_from_slice(z32::decode(secret_key.as_slice())?.as_slice());
433 return Ok(SecretKey::from_bytes(&sk_bytes));
434 }
435 }
436 bail!(
437 "no iroh-ssh keys found in {}, use --persist flag to create it",
438 ssh_dir.display()
439 )
440 }
441 }
442}