Skip to main content

iroh_ssh/
api.rs

1use std::{path::PathBuf, process::ExitStatus, str::FromStr as _};
2
3use anyhow::bail;
4use homedir::my_home;
5use iroh::{EndpointId, RelayUrl, SecretKey};
6
7use crate::{
8    IrohSsh,
9    cli::{ConnectArgs, ProxyArgs, ServerArgs},
10    dot_ssh,
11};
12
13fn parse_relay_urls(urls: &[String]) -> anyhow::Result<Vec<RelayUrl>> {
14    urls.iter()
15        .map(|s| RelayUrl::from_str(s).map_err(|e| anyhow::anyhow!("invalid relay URL '{s}': {e}")))
16        .collect()
17}
18
19pub async fn info_mode(key_dir: Option<PathBuf>) -> anyhow::Result<()> {
20    let server_key = dot_ssh(
21        &SecretKey::generate(&mut rand::rng()),
22        false,
23        false,
24        key_dir.as_deref(),
25    )
26    .ok();
27    let service_key = dot_ssh(
28        &SecretKey::generate(&mut rand::rng()),
29        false,
30        true,
31        key_dir.as_deref(),
32    )
33    .ok();
34
35    if server_key.is_none() && service_key.is_none() {
36        println!(
37            "No keys found, run for server or service:\n  'iroh-ssh server --persist' or '-p' to create it"
38        );
39        println!();
40        println!("(if an iroh-ssh instance is currently running, it is using ephemeral keys)");
41        bail!("No keys found")
42    }
43
44    println!("iroh-ssh version {}", env!("CARGO_PKG_VERSION"));
45    println!("https://github.com/rustonbsd/iroh-ssh");
46    println!();
47
48    if server_key.is_none() && service_key.is_none() {
49        println!("run 'iroh-ssh server --persist' to start the server with persistent keys");
50        println!("run 'iroh-ssh server' to start the server with ephemeral keys");
51        println!(
52            "run 'iroh-ssh service install' to copy the binary, install the service and start the server (always uses persistent keys)"
53        );
54    }
55
56    if let Some(key) = server_key {
57        println!();
58        println!("Your server iroh-ssh endpoint id:");
59        println!(
60            "  iroh-ssh {}@{}",
61            whoami::username().unwrap_or("UNKNOWN_USER".to_string()),
62            key.clone().public()
63        );
64        println!();
65    }
66
67    if let Some(key) = service_key {
68        println!();
69        println!("Your service iroh-ssh endpoint id:");
70        println!(
71            "  iroh-ssh {}@{}",
72            whoami::username().unwrap_or("UNKNOWN_USER".to_string()),
73            key.clone().public()
74        );
75        println!();
76    }
77
78    Ok(())
79}
80
81pub mod service {
82    use std::path::PathBuf;
83
84    use crate::{ServiceParams, api::abs_key_dir, install_service, uninstall_service};
85
86    pub async fn install(
87        ssh_port: u16,
88        key_dir: Option<PathBuf>,
89        relay_url: Vec<String>,
90        extra_relay_url: Vec<String>,
91    ) -> anyhow::Result<()> {
92        if install_service(ServiceParams {
93            ssh_port,
94            key_dir: abs_key_dir(key_dir),
95            relay_url,
96            extra_relay_url,
97        })
98        .await
99        .is_err()
100        {
101            anyhow::bail!("service install is only supported on linux and windows");
102        }
103        Ok(())
104    }
105
106    pub async fn uninstall() -> anyhow::Result<()> {
107        if uninstall_service().await.is_err() {
108            println!("service uninstall is only supported on linux or windows");
109            anyhow::bail!("service uninstall is only supported on linux or windows");
110        }
111        Ok(())
112    }
113}
114
115pub async fn server_mode(server_args: ServerArgs, service: bool) -> anyhow::Result<()> {
116    let mut iroh_ssh_builder = IrohSsh::builder()
117        .accept_incoming(true)
118        .accept_port(server_args.ssh_port)
119        .key_dir(server_args.key_dir.clone())
120        .relay_urls(parse_relay_urls(&server_args.relay_url)?)
121        .extra_relay_urls(parse_relay_urls(&server_args.extra_relay_url)?);
122    if server_args.persist {
123        iroh_ssh_builder = iroh_ssh_builder.dot_ssh_integration(true, service);
124    }
125    let iroh_ssh = iroh_ssh_builder.build().await?;
126
127    println!("Connect to this this machine:");
128    println!(
129        "\n  iroh-ssh {}@{}\n",
130        whoami::username().unwrap_or("UNKNOWN_USER".to_string()),
131        iroh_ssh.endpoint_id()
132    );
133    if server_args.persist {
134        let ssh_dir = match server_args.key_dir {
135            Some(dir) => dir,
136            None => {
137                let distro_home =
138                    my_home()?.ok_or_else(|| anyhow::anyhow!("home directory not found"))?;
139                distro_home.join(".ssh")
140            }
141        };
142        println!("  (using persistent keys in {})", ssh_dir.display());
143    } else {
144        println!(
145            "  warning: (using ephemeral keys, run 'iroh-ssh server --persist' to create persistent keys)"
146        );
147    }
148    println!();
149    println!(
150        "client -> iroh-ssh -> direct connect -> iroh-ssh -> local ssh :{}",
151        server_args.ssh_port
152    );
153
154    println!("Waiting for incoming connections...");
155    println!("Press Ctrl+C to exit");
156    tokio::signal::ctrl_c().await?;
157    Ok(())
158}
159
160pub async fn proxy_mode(proxy_args: ProxyArgs) -> anyhow::Result<()> {
161    let iroh_ssh = IrohSsh::builder()
162        .accept_incoming(false)
163        .relay_urls(parse_relay_urls(&proxy_args.relay_url)?)
164        .extra_relay_urls(parse_relay_urls(&proxy_args.extra_relay_url)?)
165        .build()
166        .await?;
167    let hostname = proxy_args
168        .endpoint_id
169        .split(":")
170        .next()
171        .ok_or_else(|| anyhow::anyhow!("failed to parse hostname"))?;
172    if hostname.len() == 64 && hostname.chars().all(|c| c.is_ascii_hexdigit()) {
173        let endpoint_id = EndpointId::from_str(hostname)?;
174        iroh_ssh.connect_pubkey(endpoint_id).await
175    } else {
176        // fallback to dns base (or ip) HostName connection (no iroh)
177        iroh_ssh.connect_tcpip(&proxy_args.endpoint_id).await
178    }
179}
180
181pub async fn client_mode(connect_args: ConnectArgs) -> anyhow::Result<()> {
182    let iroh_ssh = IrohSsh::builder()
183        .accept_incoming(false)
184        .relay_urls(parse_relay_urls(&connect_args.relay_url)?)
185        .extra_relay_urls(parse_relay_urls(&connect_args.extra_relay_url)?)
186        .build()
187        .await?;
188    let mut ssh_process = match iroh_ssh
189        .start_ssh(
190            connect_args.target,
191            connect_args.ssh,
192            connect_args.remote_cmd,
193            &connect_args.relay_url,
194            &connect_args.extra_relay_url,
195        )
196        .await
197    {
198        Ok(child) => child,
199        Err(err) => match err.kind() {
200            std::io::ErrorKind::NotFound => {
201                eprintln!(
202                    "SSH command not found, please make sure your system has an SSH client installed and and that the exact cmd \"ssh\" is in your PATH"
203                );
204                std::process::exit(1);
205            }
206            _ => {
207                eprintln!("Unknown failure when calling the SSH client: {err}");
208                std::process::exit(1);
209            }
210        },
211    };
212
213    let status = ssh_process.wait().await?;
214
215    // this kills the process (ok is just here for no compile errors)
216    exit_with_code(status);
217
218    Ok(())
219}
220
221#[cfg(unix)]
222pub(crate) fn exit_with_code(status: ExitStatus) {
223    use std::os::unix::process::ExitStatusExt;
224
225    if let Some(code) = status.code() {
226        std::process::exit(code);
227    }
228
229    // if ssh gets killed locally
230    if let Some(sig) = status.signal() {
231        unsafe {
232            libc::signal(sig, libc::SIG_DFL);
233            libc::kill(libc::getpid(), sig);
234        }
235        // fallback if kill fails (same as windows)
236        std::process::exit(128 + sig);
237    }
238
239    // fallback to 1 if don't know
240    std::process::exit(1);
241}
242
243#[cfg(not(unix))]
244pub(crate) fn exit_with_code(status: ExitStatus) {
245    std::process::exit(status.code().unwrap_or(1));
246}
247
248pub(crate) fn abs_key_dir(key_dir: Option<PathBuf>) -> Option<PathBuf> {
249    key_dir.map(|key_dir| {
250        if key_dir.is_absolute() {
251            key_dir
252        } else {
253            std::env::current_dir().unwrap_or_default().join(key_dir)
254        }
255    })
256}