Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a9e12fc
style: cargo fmt
r-vdp Mar 17, 2026
15c4736
Add --key-dir flag to configure iroh-ssh key location
r-vdp Mar 17, 2026
c69c12f
add: exit code passthrough
rustonbsd Mar 27, 2026
1d33712
add: ci win exit code test
rustonbsd Mar 27, 2026
be4ea5e
fix: exit code test in win + compare to ssh exit code on unix (like i…
rustonbsd Mar 27, 2026
003eb60
add: ssh passthrough for non ed25519 pub keys (dns resolve via ssh)
rustonbsd Mar 27, 2026
a64a81d
fix: actions version updated v4 -> v5 + powershell ci expected non-ze…
rustonbsd Mar 27, 2026
ce2f717
fix: updated to latest version for all actions + iroh-ssh version bump
rustonbsd Mar 27, 2026
af24269
release: v0.2.10
rustonbsd Mar 27, 2026
43c7dd6
fix: ssh config hostname parsing moved behind ProxyCommand
rustonbsd Mar 31, 2026
2019bb3
add: ssh_config file cicd tests
rustonbsd Mar 31, 2026
7841e8d
fix: mac and win cicd failures
rustonbsd Mar 31, 2026
c0429d7
fix: cicd timeout extended + windows @BaseArgs in array literal
rustonbsd Mar 31, 2026
9159d4a
Add --key-dir flag to configure iroh-ssh key location
r-vdp Mar 17, 2026
ed5f5dc
Merge branch 'main' into rvdp/key_dir
rustonbsd Apr 1, 2026
6f68024
change: [RELAYARGS] to [SERVERARGS]
rustonbsd Apr 1, 2026
33e861a
add: --key-dir cicd tests (lin/mac and win)
rustonbsd Apr 1, 2026
432b75f
fix: sshd only on port 22
rustonbsd Apr 1, 2026
abaf266
fix: abs --key-dir service path
rustonbsd Apr 2, 2026
62fb175
add: service cicd test
rustonbsd Apr 2, 2026
e1ddb52
fix: unified ssh connection success detection
rustonbsd Apr 2, 2026
71f998c
fix: grep service not first match
rustonbsd Apr 2, 2026
8099a8f
fix: win service pub key match
rustonbsd Apr 2, 2026
f8fcca5
update: nix tag hash v0.2.10
rustonbsd Apr 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
644 changes: 616 additions & 28 deletions .github/workflows/build.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion nix/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ rustPlatform.buildRustPackage (finalAttrs: {
owner = "rustonbsd";
repo = "iroh-ssh";
tag = finalAttrs.version;
hash = "sha256-n4kNzhQBasl7vAWAJysNwHsmsf8F8leXP0jBoB5hHhU=";
hash = "sha256-LXLXKrJ2nJzlW8eNhXS9tr9oGp8RlwJPIojqDIdVZf0=";
};

cargoHash = "sha256-/cq/rOzrQ4t0qvdaqM3JhRn8IMncx7jWYDjdYmLCYvc=";
Expand Down
2 changes: 1 addition & 1 deletion service/install_linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Description=SSH over Iroh
[Service]
Type=simple
WorkingDirectory=~
ExecStart=/bin/bash -c 'iroh-ssh server -p --ssh-port [SSHPORT][RELAYARGS]'
ExecStart=/bin/bash -c 'iroh-ssh server -p --ssh-port [SSHPORT][SERVERARGS]'
Restart=on-failure
RestartSec=3s

Expand Down
53 changes: 45 additions & 8 deletions src/api.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{process::ExitStatus, str::FromStr as _};
use std::{path::PathBuf, process::ExitStatus, str::FromStr as _};

use anyhow::bail;
use homedir::my_home;
Expand All @@ -16,9 +16,21 @@ fn parse_relay_urls(urls: &[String]) -> anyhow::Result<Vec<RelayUrl>> {
.collect()
}

pub async fn info_mode() -> anyhow::Result<()> {
let server_key = dot_ssh(&SecretKey::generate(&mut rand::rng()), false, false).ok();
let service_key = dot_ssh(&SecretKey::generate(&mut rand::rng()), false, true).ok();
pub async fn info_mode(key_dir: Option<PathBuf>) -> anyhow::Result<()> {
let server_key = dot_ssh(
&SecretKey::generate(&mut rand::rng()),
false,
false,
key_dir.as_deref(),
)
.ok();
let service_key = dot_ssh(
&SecretKey::generate(&mut rand::rng()),
false,
true,
key_dir.as_deref(),
)
.ok();

if server_key.is_none() && service_key.is_none() {
println!(
Expand Down Expand Up @@ -67,15 +79,19 @@ pub async fn info_mode() -> anyhow::Result<()> {
}

pub mod service {
use crate::{ServiceParams, install_service, uninstall_service};
use std::path::PathBuf;

use crate::{ServiceParams, api::abs_key_dir, install_service, uninstall_service};

pub async fn install(
ssh_port: u16,
key_dir: Option<PathBuf>,
relay_url: Vec<String>,
extra_relay_url: Vec<String>,
) -> anyhow::Result<()> {
if install_service(ServiceParams {
ssh_port,
key_dir: abs_key_dir(key_dir),
relay_url,
extra_relay_url,
})
Expand All @@ -100,6 +116,7 @@ pub async fn server_mode(server_args: ServerArgs, service: bool) -> anyhow::Resu
let mut iroh_ssh_builder = IrohSsh::builder()
.accept_incoming(true)
.accept_port(server_args.ssh_port)
.key_dir(server_args.key_dir.clone())
.relay_urls(parse_relay_urls(&server_args.relay_url)?)
.extra_relay_urls(parse_relay_urls(&server_args.extra_relay_url)?);
if server_args.persist {
Expand All @@ -114,8 +131,14 @@ pub async fn server_mode(server_args: ServerArgs, service: bool) -> anyhow::Resu
iroh_ssh.endpoint_id()
);
if server_args.persist {
let distro_home = my_home()?.ok_or_else(|| anyhow::anyhow!("home directory not found"))?;
let ssh_dir = distro_home.join(".ssh");
let ssh_dir = match server_args.key_dir {
Some(dir) => dir,
None => {
let distro_home =
my_home()?.ok_or_else(|| anyhow::anyhow!("home directory not found"))?;
distro_home.join(".ssh")
}
};
println!(" (using persistent keys in {})", ssh_dir.display());
} else {
println!(
Expand All @@ -141,7 +164,11 @@ pub async fn proxy_mode(proxy_args: ProxyArgs) -> anyhow::Result<()> {
.extra_relay_urls(parse_relay_urls(&proxy_args.extra_relay_url)?)
.build()
.await?;
let hostname = proxy_args.endpoint_id.split(":").next().ok_or_else(|| anyhow::anyhow!("failed to parse hostname"))?;
let hostname = proxy_args
.endpoint_id
.split(":")
.next()
.ok_or_else(|| anyhow::anyhow!("failed to parse hostname"))?;
if hostname.len() == 64 && hostname.chars().all(|c| c.is_ascii_hexdigit()) {
let endpoint_id = EndpointId::from_str(hostname)?;
iroh_ssh.connect_pubkey(endpoint_id).await
Expand Down Expand Up @@ -202,3 +229,13 @@ pub(crate) fn exit_with_code(status: ExitStatus) {
pub(crate) fn exit_with_code(status: ExitStatus) {
std::process::exit(status.code().unwrap_or(1));
}

pub(crate) fn abs_key_dir(key_dir: Option<PathBuf>) -> Option<PathBuf> {
key_dir.map(|key_dir| {
if key_dir.is_absolute() {
key_dir
} else {
std::env::current_dir().unwrap_or_default().join(key_dir)
}
})
}
20 changes: 18 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use clap::{ArgAction, Args, Parser, Subcommand};
const TARGET_HELP: &str = "Target in the form user@ENDPOINT_ID";
const RELAY_URL_HELP: &str = "Use only these relay servers, replacing the defaults (repeatable)";
const EXTRA_RELAY_URL_HELP: &str = "Add relay servers alongside the defaults (repeatable)";
const KEY_DIR_HELP: &str = "Directory for iroh-ssh identity keys (default: ~/.ssh)";

#[derive(Parser, Debug)]
#[command(name = "iroh-ssh", about = "ssh without ip")]
Expand All @@ -28,7 +29,7 @@ pub struct Cli {
pub remote_cmd: Option<Vec<OsString>>,
}

#[derive(Subcommand,Debug)]
#[derive(Subcommand, Debug)]
pub enum Cmd {
Connect(ConnectArgs),
#[command(hide = true)]
Expand All @@ -38,7 +39,7 @@ pub enum Cmd {
#[command(subcommand)]
op: ServiceCmd,
},
Info,
Info(InfoArgs),
#[command(hide = true)]
Proxy(ProxyArgs),
#[command(hide = true)]
Expand Down Expand Up @@ -161,19 +162,31 @@ pub struct ServerArgs {
#[arg(short, long, default_value_t = false)]
pub persist: bool,

#[arg(long, value_name = "DIR", help = KEY_DIR_HELP)]
pub key_dir: Option<PathBuf>,

#[arg(long, value_name = "URL", help = RELAY_URL_HELP, action = ArgAction::Append)]
pub relay_url: Vec<String>,

#[arg(long, value_name = "URL", help = EXTRA_RELAY_URL_HELP, action = ArgAction::Append)]
pub extra_relay_url: Vec<String>,
}

#[derive(Args, Clone, Debug)]
pub struct InfoArgs {
#[arg(long, value_name = "DIR", help = KEY_DIR_HELP)]
pub key_dir: Option<PathBuf>,
}

#[derive(Subcommand, Clone, Debug)]
pub enum ServiceCmd {
Install {
#[arg(long, default_value = "22")]
ssh_port: u16,

#[arg(long, value_name = "DIR", help = KEY_DIR_HELP)]
key_dir: Option<PathBuf>,

#[arg(long, value_name = "URL", help = RELAY_URL_HELP, action = ArgAction::Append)]
relay_url: Vec<String>,

Expand All @@ -188,6 +201,9 @@ pub struct ServiceArgs {
#[arg(long, default_value = "22")]
pub ssh_port: u16,

#[arg(long, value_name = "DIR", help = KEY_DIR_HELP)]
pub key_dir: Option<PathBuf>,

#[arg(long, value_name = "URL", help = RELAY_URL_HELP, action = ArgAction::Append)]
pub relay_url: Vec<String>,

Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ mod cli;
mod service;
mod ssh;

use std::path::PathBuf;

use ed25519_dalek::{PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH};
use iroh::{Endpoint, RelayUrl, protocol::Router};

Expand Down Expand Up @@ -35,6 +37,7 @@ pub struct Builder {
secret_key: [u8; SECRET_KEY_LENGTH],
accept_incoming: bool,
accept_port: Option<u16>,
key_dir: Option<PathBuf>,
relay_urls: Vec<RelayUrl>,
extra_relay_urls: Vec<RelayUrl>,
}
23 changes: 18 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,35 @@ async fn main() -> anyhow::Result<()> {
Some(Cmd::Service { op }) => {
if !self_runas::is_elevated() {
self_runas::admin()?;
return Ok(())
return Ok(());
} else {
match op {
ServiceCmd::Install { ssh_port, relay_url, extra_relay_url } => api::service::install(ssh_port, relay_url, extra_relay_url).await,
ServiceCmd::Install {
ssh_port,
key_dir,
relay_url,
extra_relay_url,
} => api::service::install(ssh_port, key_dir, relay_url, extra_relay_url).await,
ServiceCmd::Uninstall => api::service::uninstall().await,
}
}
}
Some(Cmd::Info) => api::info_mode().await,
Some(Cmd::Info(args)) => api::info_mode(args.key_dir).await,
Some(Cmd::Version) => {
println!("iroh-ssh version {}", env!("CARGO_PKG_VERSION"));
Ok(())
},
}
Some(Cmd::Proxy(args)) => api::proxy_mode(args).await,
#[cfg(target_os = "windows")]
Some(Cmd::RunService(args)) => iroh_ssh::run_service(args.ssh_port, args.relay_url, args.extra_relay_url).await,
Some(Cmd::RunService(args)) => {
iroh_ssh::run_service(
args.ssh_port,
args.key_dir,
args.relay_url,
args.extra_relay_url,
)
.await
}
#[cfg(not(target_os = "windows"))]
Some(Cmd::RunService(_)) => {
bail!("service runtime is only available on windows");
Expand Down
11 changes: 7 additions & 4 deletions src/service/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,15 @@ impl LinuxService {
fn init_install_script(service_params: ServiceParams) -> anyhow::Result<std::path::PathBuf> {
use std::io::Write as _;

let mut relay_args = String::new();
let mut server_args = String::new();
if let Some(ref dir) = service_params.key_dir {
server_args.push_str(&format!(" --key-dir {}", dir.display()));
}
for url in &service_params.relay_url {
relay_args.push_str(&format!(" --relay-url {url}"));
server_args.push_str(&format!(" --relay-url {url}"));
}
for url in &service_params.extra_relay_url {
relay_args.push_str(&format!(" --extra-relay-url {url}"));
server_args.push_str(&format!(" --extra-relay-url {url}"));
}

let mut temp_sh = tempfile::Builder::new()
Expand All @@ -59,7 +62,7 @@ impl LinuxService {
temp_sh.write_all(
LinuxService::INSTALL_SH_BYTES
.replace("[SSHPORT]", &service_params.ssh_port.to_string())
.replace("[RELAYARGS]", &relay_args)
.replace("[SERVERARGS]", &server_args)
.replace(
"[BINARYPATH]",
std::env::current_exe()?
Expand Down
4 changes: 4 additions & 0 deletions src/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ pub(crate) use crate::service::windows::WindowsService;
#[cfg(target_os = "windows")]
pub async fn run_service(
ssh_port: u16,
key_dir: Option<std::path::PathBuf>,
relay_url: Vec<String>,
extra_relay_url: Vec<String>,
) -> anyhow::Result<()> {
WindowsService::run_service(ServiceParams {
ssh_port,
key_dir: crate::api::abs_key_dir(key_dir),
relay_url,
extra_relay_url,
})
Expand All @@ -25,6 +27,7 @@ pub async fn run_service(
#[cfg(not(target_os = "windows"))]
pub async fn run_service(
_ssh_port: u16,
_key_dir: Option<std::path::PathBuf>,
_relay_url: Vec<String>,
_extra_relay_url: Vec<String>,
) -> anyhow::Result<()> {
Expand All @@ -34,6 +37,7 @@ pub async fn run_service(
#[derive(Debug, Clone)]
pub struct ServiceParams {
pub ssh_port: u16,
pub key_dir: Option<std::path::PathBuf>,
pub relay_url: Vec<String>,
pub extra_relay_url: Vec<String>,
}
Expand Down
14 changes: 14 additions & 0 deletions src/service/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ static SERVICE_RELAY_URLS: OnceLock<Vec<String>> = OnceLock::new();
#[cfg(target_os = "windows")]
static SERVICE_EXTRA_RELAY_URLS: OnceLock<Vec<String>> = OnceLock::new();

#[cfg(target_os = "windows")]
static SERVICE_KEY_DIR: OnceLock<Option<PathBuf>> = OnceLock::new();

#[cfg(target_os = "windows")]
impl Service for WindowsService {
async fn install(service_params: ServiceParams) -> anyhow::Result<()> {
Expand Down Expand Up @@ -104,6 +107,7 @@ impl WindowsService {
})
.ok_or_else(|| anyhow!("service port already initialized with different value"))?;

let _ = SERVICE_KEY_DIR.set(service_params.key_dir);
let _ = SERVICE_RELAY_URLS.set(service_params.relay_url);
let _ = SERVICE_EXTRA_RELAY_URLS.set(service_params.extra_relay_url);

Expand All @@ -126,6 +130,10 @@ impl WindowsService {
SERVICE_EXTRA_RELAY_URLS.get().cloned().unwrap_or_default()
}

fn service_key_dir() -> Option<PathBuf> {
SERVICE_KEY_DIR.get().cloned().flatten()
}

pub const SERVICE_NAME: &'static str = "iroh-ssh";
pub const SERVICE_DISPLAY_NAME: &'static str = "iroh-ssh";
pub const SERVICE_DESCRIPTION: &'static str = "SSH to any machine without ip";
Expand Down Expand Up @@ -228,6 +236,10 @@ impl WindowsService {
OsString::from("--ssh-port"),
OsString::from(service_params.ssh_port.to_string()),
];
if let Some(ref dir) = service_params.key_dir {
args.push(OsString::from("--key-dir"));
args.push(OsString::from(dir));
}
for url in &service_params.relay_url {
args.push(OsString::from("--relay-url"));
args.push(OsString::from(url));
Expand Down Expand Up @@ -566,6 +578,7 @@ mod service_runtime {
tracing::info!("run_service_worker: Starting");

let ssh_port = WindowsService::service_port().map_err(anyhow_to_win_error)?;
let key_dir = WindowsService::service_key_dir();
let relay_url = WindowsService::service_relay_urls();
let extra_relay_url = WindowsService::service_extra_relay_urls();

Expand Down Expand Up @@ -612,6 +625,7 @@ mod service_runtime {
ServerArgs {
ssh_port,
persist: true,
key_dir,
relay_url,
extra_relay_url,
},
Expand Down
4 changes: 2 additions & 2 deletions src/service/windows/firewall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ Write-Host 'Removed HTTPS rule'
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
// Don't fail on cleanup - just log the error
// Don't fail on cleanup (just log the error)
tracing::warn!(
"Failed to remove some firewall rules (may not exist).\nStdout: {}\nStderr: {}",
stdout,
Expand All @@ -130,4 +130,4 @@ Write-Host 'Removed HTTPS rule'
}

Ok(())
}
}
Loading