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}