iroh_ssh/
api.rs

1use std::{path::PathBuf, str::FromStr as _};
2
3use anyhow::bail;
4use homedir::my_home;
5use iroh::{NodeId, SecretKey};
6
7use crate::{IrohSsh, dot_ssh};
8
9pub async fn info_mode() -> anyhow::Result<()> {
10    let server_key = dot_ssh(&SecretKey::generate(rand::rngs::OsRng), false, false).ok();
11    let service_key = dot_ssh(&SecretKey::generate(rand::rngs::OsRng), false, true).ok();
12
13    if server_key.is_none() && service_key.is_none() {
14        println!(
15            "No keys found, run for server or service:\n  'iroh-ssh server --persist' or '-p' to create it"
16        );
17        println!();
18        println!("(if an iroh-ssh instance is currently running, it is using ephemeral keys)");
19        bail!("No keys found")
20    }
21
22    println!("iroh-ssh version {}", env!("CARGO_PKG_VERSION"));
23    println!("https://github.com/rustonbsd/iroh-ssh");
24    println!();
25
26    if server_key.is_none() && service_key.is_none() {
27        println!("run 'iroh-ssh server --persist' to start the server with persistent keys");
28        println!("run 'iroh-ssh server' to start the server with ephemeral keys");
29        println!(
30            "run 'iroh-ssh service install' to copy the binary, install the service and start the server (always uses persistent keys)"
31        );
32    }
33
34    if let Some(key) = server_key {
35        println!();
36        println!("Your server iroh-ssh nodeid:");
37        println!("  iroh-ssh {}@{}", whoami::username(), key.clone().public());
38        println!();
39    }
40
41    if let Some(key) = service_key {
42        println!();
43        println!("Your service iroh-ssh nodeid:");
44        println!("  iroh-ssh {}@{}", whoami::username(), key.clone().public());
45        println!();
46    }
47
48    Ok(())
49}
50
51pub mod service {
52    use crate::{ServiceParams, install_service, uninstall_service};
53
54    pub async fn install(ssh_port: u16) -> anyhow::Result<()> {
55        if install_service(ServiceParams { ssh_port }).await.is_err() {
56            println!("service install is only supported on linux and windows");
57            anyhow::bail!("service install is only supported on linux and windows");
58        }
59        Ok(())
60    }
61
62    pub async fn uninstall() -> anyhow::Result<()> {
63        if uninstall_service().await.is_err() {
64            println!("service uninstall is only supported on linux or windows");
65            anyhow::bail!("service uninstall is only supported on linux or windows");
66        }
67        Ok(())
68    }
69}
70
71pub async fn server_mode(ssh_port: u16, persist: bool) -> anyhow::Result<()> {
72    let mut iroh_ssh_builder = IrohSsh::builder()
73        .accept_incoming(true)
74        .accept_port(ssh_port);
75    if persist {
76        iroh_ssh_builder = iroh_ssh_builder.dot_ssh_integration(true, false);
77    }
78    let iroh_ssh = iroh_ssh_builder.build().await?;
79
80    println!("Connect to this this machine:");
81    println!(
82        "\n  iroh-ssh {}@{}\n",
83        whoami::username(),
84        iroh_ssh.node_id()
85    );
86    if persist {
87        let distro_home = my_home()?.ok_or_else(|| anyhow::anyhow!("home directory not found"))?;
88        let ssh_dir = distro_home.join(".ssh");
89        println!("  (using persistent keys in {})", ssh_dir.display());
90    } else {
91        println!(
92            "  warning: (using ephemeral keys, run 'iroh-ssh server --persist' to create persistent keys)"
93        );
94    }
95    println!();
96    println!("client -> iroh-ssh -> direct connect -> iroh-ssh -> local ssh :{ssh_port}");
97
98    println!("Waiting for incoming connections...");
99    println!("Press Ctrl+C to exit");
100    tokio::signal::ctrl_c().await?;
101    Ok(())
102}
103
104pub struct ClientOptions {
105    pub target: String,
106    pub identity_file: Option<PathBuf>,
107    pub local_forward: Option<String>,
108    pub remote_forward: Option<String>,
109}
110
111pub async fn client_mode(
112    client_options: ClientOptions,
113    execute_command: Vec<String>,
114) -> anyhow::Result<()> {
115    let (ssh_user, iroh_node_id) = parse_iroh_target(&client_options.target)?;
116    let iroh_ssh = IrohSsh::builder().accept_incoming(false).build().await?;
117    let mut ssh_process = iroh_ssh
118        .connect(&ssh_user, iroh_node_id, client_options, execute_command)
119        .await?;
120
121    ssh_process.wait().await?;
122
123    Ok(())
124}
125
126fn parse_iroh_target(target: &str) -> anyhow::Result<(String, NodeId)> {
127    let (user, node_id_str) = target
128        .split_once('@')
129        .ok_or_else(|| anyhow::anyhow!("Invalid format, use user@node_id"))?;
130    let node_id = NodeId::from_str(node_id_str)?;
131    Ok((user.to_string(), node_id))
132}