Skip to main content

iroh_ssh/
api.rs

1use std::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() -> anyhow::Result<()> {
20    let server_key = dot_ssh(&SecretKey::generate(&mut rand::rng()), false, false).ok();
21    let service_key = dot_ssh(&SecretKey::generate(&mut rand::rng()), false, true).ok();
22
23    if server_key.is_none() && service_key.is_none() {
24        println!(
25            "No keys found, run for server or service:\n  'iroh-ssh server --persist' or '-p' to create it"
26        );
27        println!();
28        println!("(if an iroh-ssh instance is currently running, it is using ephemeral keys)");
29        bail!("No keys found")
30    }
31
32    println!("iroh-ssh version {}", env!("CARGO_PKG_VERSION"));
33    println!("https://github.com/rustonbsd/iroh-ssh");
34    println!();
35
36    if server_key.is_none() && service_key.is_none() {
37        println!("run 'iroh-ssh server --persist' to start the server with persistent keys");
38        println!("run 'iroh-ssh server' to start the server with ephemeral keys");
39        println!(
40            "run 'iroh-ssh service install' to copy the binary, install the service and start the server (always uses persistent keys)"
41        );
42    }
43
44    if let Some(key) = server_key {
45        println!();
46        println!("Your server iroh-ssh endpoint id:");
47        println!(
48            "  iroh-ssh {}@{}",
49            whoami::username().unwrap_or("UNKNOWN_USER".to_string()),
50            key.clone().public()
51        );
52        println!();
53    }
54
55    if let Some(key) = service_key {
56        println!();
57        println!("Your service iroh-ssh endpoint id:");
58        println!(
59            "  iroh-ssh {}@{}",
60            whoami::username().unwrap_or("UNKNOWN_USER".to_string()),
61            key.clone().public()
62        );
63        println!();
64    }
65
66    Ok(())
67}
68
69pub mod service {
70    use crate::{ServiceParams, install_service, uninstall_service};
71
72    pub async fn install(
73        ssh_port: u16,
74        relay_url: Vec<String>,
75        extra_relay_url: Vec<String>,
76    ) -> anyhow::Result<()> {
77        if install_service(ServiceParams {
78            ssh_port,
79            relay_url,
80            extra_relay_url,
81        })
82        .await
83        .is_err()
84        {
85            anyhow::bail!("service install is only supported on linux and windows");
86        }
87        Ok(())
88    }
89
90    pub async fn uninstall() -> anyhow::Result<()> {
91        if uninstall_service().await.is_err() {
92            println!("service uninstall is only supported on linux or windows");
93            anyhow::bail!("service uninstall is only supported on linux or windows");
94        }
95        Ok(())
96    }
97}
98
99pub async fn server_mode(server_args: ServerArgs, service: bool) -> anyhow::Result<()> {
100    let mut iroh_ssh_builder = IrohSsh::builder()
101        .accept_incoming(true)
102        .accept_port(server_args.ssh_port)
103        .relay_urls(parse_relay_urls(&server_args.relay_url)?)
104        .extra_relay_urls(parse_relay_urls(&server_args.extra_relay_url)?);
105    if server_args.persist {
106        iroh_ssh_builder = iroh_ssh_builder.dot_ssh_integration(true, service);
107    }
108    let iroh_ssh = iroh_ssh_builder.build().await?;
109
110    println!("Connect to this this machine:");
111    println!(
112        "\n  iroh-ssh {}@{}\n",
113        whoami::username().unwrap_or("UNKNOWN_USER".to_string()),
114        iroh_ssh.endpoint_id()
115    );
116    if server_args.persist {
117        let distro_home = my_home()?.ok_or_else(|| anyhow::anyhow!("home directory not found"))?;
118        let ssh_dir = distro_home.join(".ssh");
119        println!("  (using persistent keys in {})", ssh_dir.display());
120    } else {
121        println!(
122            "  warning: (using ephemeral keys, run 'iroh-ssh server --persist' to create persistent keys)"
123        );
124    }
125    println!();
126    println!(
127        "client -> iroh-ssh -> direct connect -> iroh-ssh -> local ssh :{}",
128        server_args.ssh_port
129    );
130
131    println!("Waiting for incoming connections...");
132    println!("Press Ctrl+C to exit");
133    tokio::signal::ctrl_c().await?;
134    Ok(())
135}
136
137pub async fn proxy_mode(proxy_args: ProxyArgs) -> anyhow::Result<()> {
138    let iroh_ssh = IrohSsh::builder()
139        .accept_incoming(false)
140        .relay_urls(parse_relay_urls(&proxy_args.relay_url)?)
141        .extra_relay_urls(parse_relay_urls(&proxy_args.extra_relay_url)?)
142        .build()
143        .await?;
144    let endpoint_id = EndpointId::from_str(if proxy_args.endpoint_id.len() == 64 {
145        &proxy_args.endpoint_id
146    } else if proxy_args.endpoint_id.len() > 64 {
147        &proxy_args.endpoint_id[proxy_args.endpoint_id.len() - 64..]
148    } else {
149        return Err(anyhow::anyhow!("invalid endpoint id length"));
150    })?;
151    iroh_ssh.connect(endpoint_id).await
152}
153
154pub async fn client_mode(connect_args: ConnectArgs) -> anyhow::Result<()> {
155    let iroh_ssh = IrohSsh::builder()
156        .accept_incoming(false)
157        .relay_urls(parse_relay_urls(&connect_args.relay_url)?)
158        .extra_relay_urls(parse_relay_urls(&connect_args.extra_relay_url)?)
159        .build()
160        .await?;
161    let mut ssh_process = iroh_ssh
162        .start_ssh(
163            connect_args.target,
164            connect_args.ssh,
165            connect_args.remote_cmd,
166            &connect_args.relay_url,
167            &connect_args.extra_relay_url,
168        )
169        .await?;
170
171    ssh_process.wait().await?;
172
173    Ok(())
174}