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 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 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 let Some(sig) = status.signal() {
216 unsafe {
217 libc::signal(sig, libc::SIG_DFL);
218 libc::kill(libc::getpid(), sig);
219 }
220 std::process::exit(128 + sig);
222 }
223
224 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}