1use crate::{Builder, Inner, IrohSsh, cli::SshOpts};
2use std::{ffi::OsString, process::Stdio};
3
4use anyhow::bail;
5use ed25519_dalek::SECRET_KEY_LENGTH;
6use homedir::my_home;
7use iroh::{
8 endpoint::Connection, protocol::{ProtocolHandler, Router}, Endpoint, EndpointId, SecretKey
9};
10use tokio::{
11 net::TcpStream,
12 process::{Child, Command},
13};
14
15impl Builder {
16 pub fn new() -> Self {
17 Self {
18 secret_key: SecretKey::generate(&mut rand::rng()).to_bytes(),
19 accept_incoming: false,
20 accept_port: None,
21 }
22 }
23
24 pub fn accept_incoming(mut self, accept_incoming: bool) -> Self {
25 self.accept_incoming = accept_incoming;
26 self
27 }
28
29 pub fn accept_port(mut self, accept_port: u16) -> Self {
30 self.accept_port = Some(accept_port);
31 self
32 }
33
34 pub fn secret_key(mut self, secret_key: &[u8; SECRET_KEY_LENGTH]) -> Self {
35 self.secret_key = *secret_key;
36 self
37 }
38
39 pub fn dot_ssh_integration(mut self, persist: bool, service: bool) -> Self {
40 tracing::info!(
41 "dot_ssh_integration: persist={}, service={}",
42 persist,
43 service
44 );
45
46 match dot_ssh(&SecretKey::from_bytes(&self.secret_key), persist, service) {
47 Ok(secret_key) => {
48 tracing::info!("dot_ssh_integration: Successfully loaded/created SSH keys");
49 self.secret_key = secret_key.to_bytes();
50 }
51 Err(e) => {
52 tracing::error!(
53 "dot_ssh_integration: Failed to load/create SSH keys: {:#}",
54 e
55 );
56 eprintln!(
57 "Warning: Failed to load/create persistent SSH keys: {e:#}"
58 );
59 eprintln!("Continuing with ephemeral keys...");
60 }
61 }
62 self
63 }
64
65 pub async fn build(&mut self) -> anyhow::Result<IrohSsh> {
66 let secret_key = SecretKey::from_bytes(&self.secret_key);
68 let endpoint = Endpoint::builder()
69 .secret_key(secret_key)
70 .bind()
71 .await?;
72
73 let mut iroh_ssh = IrohSsh {
74 public_key: *endpoint.id().as_bytes(),
75 secret_key: self.secret_key,
76 inner: None,
77 ssh_port: self.accept_port.unwrap_or(22),
78 };
79
80 let router = if self.accept_incoming {
81 Router::builder(endpoint.clone()).accept(IrohSsh::ALPN(), iroh_ssh.clone())
82 } else {
83 Router::builder(endpoint.clone())
84 }
85 .spawn();
86
87 iroh_ssh.add_inner(endpoint, router);
88
89 Ok(iroh_ssh)
90 }
91}
92
93impl Default for Builder {
94 fn default() -> Self {
95 Self::new()
96 }
97}
98
99impl IrohSsh {
100 pub fn builder() -> Builder {
101 Builder::new()
102 }
103
104 #[allow(non_snake_case)]
105 pub fn ALPN() -> Vec<u8> {
106 b"/iroh/ssh".to_vec()
107 }
108
109 fn add_inner(&mut self, endpoint: Endpoint, router: Router) {
110 self.inner = Some(Inner { endpoint, router });
111 }
112
113 pub async fn start_ssh(
114 &self,
115 target: String,
116 ssh_opts: SshOpts,
117 remote_cmd: Vec<OsString>,
118 ) -> anyhow::Result<Child> {
119 let c_exe = std::env::current_exe()?;
120 let cmd = &mut Command::new("ssh");
121
122 cmd.arg("-o")
123 .arg(format!("ProxyCommand={} proxy %h", c_exe.display()));
124
125 if let Some(p) = ssh_opts.port {
126 cmd.arg("-p").arg(p.to_string());
127 }
128 if let Some(id) = &ssh_opts.identity_file {
129 cmd.arg("-i").arg(id);
130 }
131 for l in &ssh_opts.local_forward {
132 cmd.arg("-L").arg(l);
133 }
134 for r in &ssh_opts.remote_forward {
135 cmd.arg("-R").arg(r);
136 }
137 for o in &ssh_opts.options {
138 cmd.arg("-o").arg(o);
139 }
140 if ssh_opts.agent {
141 cmd.arg("-A");
142 }
143 if ssh_opts.no_agent {
144 cmd.arg("-a");
145 }
146 if ssh_opts.x11_trusted {
147 cmd.arg("-Y");
148 } else if ssh_opts.x11 {
149 cmd.arg("-X");
150 }
151 if ssh_opts.no_cmd {
152 cmd.arg("-N");
153 }
154 if ssh_opts.force_tty {
155 cmd.arg("-t");
156 }
157 if ssh_opts.no_tty {
158 cmd.arg("-T");
159 }
160 for _ in 0..ssh_opts.verbose {
161 cmd.arg("-v");
162 }
163 if ssh_opts.quiet {
164 cmd.arg("-q");
165 }
166
167 cmd.arg(target);
168
169 if !remote_cmd.is_empty() {
170 cmd.args(remote_cmd.iter());
171 }
172
173 let ssh_process = cmd
174 .stdin(Stdio::inherit())
175 .stdout(Stdio::inherit())
176 .stderr(Stdio::inherit())
177 .spawn()?;
178
179 Ok(ssh_process)
180 }
181
182 pub async fn connect(&self, endpoint_id: EndpointId) -> anyhow::Result<()> {
183 let inner = self.inner.as_ref().expect("inner not set");
184 let conn = inner.endpoint.connect(endpoint_id, &IrohSsh::ALPN()).await?;
185 let (mut iroh_send, mut iroh_recv) = conn.open_bi().await?;
186 let (mut local_read, mut local_write) = (tokio::io::stdin(), tokio::io::stdout());
187 let a_to_b = async move { tokio::io::copy(&mut local_read, &mut iroh_send).await };
188 let b_to_a = async move { tokio::io::copy(&mut iroh_recv, &mut local_write).await };
189
190 tokio::select! {
191 result = a_to_b => {
192 let _ = result;
193 },
194 result = b_to_a => {
195 let _ = result;
196 },
197 };
198 Ok(())
199 }
200
201 pub fn node_id(&self) -> EndpointId {
202 self.inner
203 .as_ref()
204 .expect("inner not set")
205 .endpoint
206 .id()
207 }
208}
209
210impl ProtocolHandler for IrohSsh {
211 async fn accept(&self, connection: Connection) -> Result<(), iroh::protocol::AcceptError> {
212 let endpoint_id = connection.remote_id()?;
213
214 match connection.accept_bi().await {
215 Ok((mut iroh_send, mut iroh_recv)) => {
216 println!("Accepted bidirectional stream from {endpoint_id}");
217
218 match TcpStream::connect(format!("127.0.0.1:{}", self.ssh_port)).await {
219 Ok(mut ssh_stream) => {
220 println!("Connected to local SSH server on port {}", self.ssh_port);
221
222 let (mut local_read, mut local_write) = ssh_stream.split();
223
224 let a_to_b =
225 async move { tokio::io::copy(&mut local_read, &mut iroh_send).await };
226 let b_to_a =
227 async move { tokio::io::copy(&mut iroh_recv, &mut local_write).await };
228
229 tokio::select! {
230 result = a_to_b => {
231 println!("SSH->Iroh stream ended: {result:?}");
232 },
233 result = b_to_a => {
234 println!("Iroh->SSH stream ended: {result:?}");
235 },
236 };
237 }
238 Err(e) => {
239 println!("Failed to connect to SSH server: {e}");
240 }
241 }
242 }
243 Err(e) => {
244 println!("Failed to accept bidirectional stream: {e}");
245 }
246 }
247
248 Ok(())
249 }
250}
251
252pub fn dot_ssh(
253 default_secret_key: &SecretKey,
254 persist: bool,
255 _service: bool,
256) -> anyhow::Result<SecretKey> {
257 tracing::info!(
258 "dot_ssh: Function called, persist={}, service={}",
259 persist,
260 _service
261 );
262
263 let distro_home = my_home()?.ok_or_else(|| anyhow::anyhow!("home directory not found"))?;
264 #[allow(unused_mut)]
265 let mut ssh_dir = distro_home.join(".ssh");
266
267 #[cfg(target_os = "linux")]
270 if _service {
271 ssh_dir = std::path::PathBuf::from("/root/.ssh");
272 }
273
274 #[cfg(target_os = "windows")]
276 if _service {
277 ssh_dir = std::path::PathBuf::from(crate::service::WindowsService::SERVICE_SSH_DIR);
278 tracing::info!("dot_ssh: Using service SSH dir: {}", ssh_dir.display());
279
280 if !ssh_dir.exists() {
282 tracing::info!("dot_ssh: Service SSH dir doesn't exist, creating it");
283 std::fs::create_dir_all(&ssh_dir)?;
284 }
285 }
286
287 let pub_key = ssh_dir.join("irohssh_ed25519.pub");
288 let priv_key = ssh_dir.join("irohssh_ed25519");
289
290 tracing::debug!("dot_ssh: ssh_dir exists = {}", ssh_dir.exists());
291 tracing::debug!("dot_ssh: pub_key path = {}", pub_key.display());
292 tracing::debug!("dot_ssh: priv_key path = {}", priv_key.display());
293
294 match (ssh_dir.exists(), persist) {
295 (false, false) => {
296 tracing::error!(
297 "dot_ssh: ssh_dir does not exist and persist=false: {}",
298 ssh_dir.display()
299 );
300 bail!(
301 "no .ssh folder found in {}, use --persist flag to create it",
302 distro_home.display()
303 )
304 }
305 (false, true) => {
306 tracing::info!("dot_ssh: Creating ssh_dir: {}", ssh_dir.display());
307 std::fs::create_dir_all(&ssh_dir)?;
308 println!("[INFO] created .ssh folder: {}", ssh_dir.display());
309 dot_ssh(default_secret_key, persist, _service)
310 }
311 (true, true) => {
312 tracing::info!("dot_ssh: Branch (true, true) - directory exists, persist enabled");
313 tracing::debug!("dot_ssh: pub_key.exists() = {}", pub_key.exists());
314 tracing::debug!("dot_ssh: priv_key.exists() = {}", priv_key.exists());
315
316 if pub_key.exists() && priv_key.exists() {
318 tracing::info!("dot_ssh: Keys exist, reading them");
319 if let Ok(secret_key) = std::fs::read(priv_key.clone()) {
321 let mut sk_bytes = [0u8; SECRET_KEY_LENGTH];
322 sk_bytes.copy_from_slice(z32::decode(secret_key.as_slice())?.as_slice());
323 Ok(SecretKey::from_bytes(&sk_bytes))
324 } else {
325 bail!("failed to read secret key from {}", priv_key.display())
326 }
327 } else {
328 tracing::info!("dot_ssh: Keys don't exist, creating new keys");
329 tracing::debug!("dot_ssh: Writing to pub_key: {}", pub_key.display());
330 tracing::debug!("dot_ssh: Writing to priv_key: {}", priv_key.display());
331
332 let secret_key = default_secret_key.clone();
333 let public_key = secret_key.public();
334
335 match std::fs::write(&pub_key, z32::encode(public_key.as_bytes())) {
336 Ok(_) => {
337 tracing::info!("dot_ssh: Successfully wrote pub_key");
338 }
339 Err(e) => {
340 tracing::error!(
341 "dot_ssh: Failed to write pub_key: {} (error kind: {:?})",
342 e,
343 e.kind()
344 );
345 return Err(e.into());
346 }
347 }
348
349 match std::fs::write(&priv_key, z32::encode(&secret_key.to_bytes())) {
350 Ok(_) => {
351 tracing::info!("dot_ssh: Successfully wrote priv_key");
352 }
353 Err(e) => {
354 tracing::error!(
355 "dot_ssh: Failed to write priv_key: {} (error kind: {:?})",
356 e,
357 e.kind()
358 );
359 return Err(e.into());
360 }
361 }
362
363 Ok(secret_key)
364 }
365 }
366 (true, false) => {
367 if pub_key.exists() && priv_key.exists() {
369 if let Ok(secret_key) = std::fs::read(priv_key.clone()) {
371 let mut sk_bytes = [0u8; SECRET_KEY_LENGTH];
372 sk_bytes.copy_from_slice(z32::decode(secret_key.as_slice())?.as_slice());
373 return Ok(SecretKey::from_bytes(&sk_bytes));
374 }
375 }
376 bail!(
377 "no iroh-ssh keys found in {}, use --persist flag to create it",
378 ssh_dir.display()
379 )
380 }
381 }
382}