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 = 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    let status = ssh_process.wait().await?;
199
200    // this kills the process (ok is just here for now compile errors)
201    exit_with_code(status);
202
203    Ok(())
204}
205
206#[cfg(unix)]
207pub(crate) fn exit_with_code(status: ExitStatus) {
208    use std::os::unix::process::ExitStatusExt;
209
210    if let Some(code) = status.code() {
211        std::process::exit(code);
212    }
213
214    // if ssh gets killed locally
215    if let Some(sig) = status.signal() {
216        unsafe {
217            libc::signal(sig, libc::SIG_DFL);
218            libc::kill(libc::getpid(), sig);
219        }
220        // fallback if kill fails (same as windows)
221        std::process::exit(128 + sig);
222    }
223
224    // fallback to 1 if don't know
225    std::process::exit(1);
226}
227
228#[cfg(not(unix))]
229pub(crate) fn exit_with_code(status: ExitStatus) {
230    std::process::exit(status.code().unwrap_or(1));
231}
232
233pub(crate) fn abs_key_dir(key_dir: Option<PathBuf>) -> Option<PathBuf> {
234    key_dir.map(|key_dir| {
235        if key_dir.is_absolute() {
236            key_dir
237        } else {
238            std::env::current_dir().unwrap_or_default().join(key_dir)
239        }
240    })
241}