1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
|
use core::net::{Ipv4Addr, SocketAddr};
use std::{
env,
fs::{self, File},
io::{self, Write},
path::{Path, PathBuf},
sync::Arc,
};
use anyhow::Context;
#[cfg(debug_assertions)]
use clap::ArgAction;
use clap::Parser;
use listenfd::ListenFd;
use oxish::{Config, DEFAULT_PROVIDER, DefaultStore, Server};
use proto::{
HostKeys,
named::{Named, PublicKeyAlgorithm},
};
use tokio::net::TcpListener;
use tracing::info;
use zeroize::Zeroizing;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
let provider = DEFAULT_PROVIDER;
let args = Args::parse();
let host_keys = if args.generate_host_key {
match File::create_new(&args.host_key_file) {
Ok(mut host_key_file) => {
let Ok((key, pkcs8)) = provider.generate_signing_key(&args.host_key_type) else {
anyhow::bail!("failed to generate host key");
};
let _pkcs8 = Zeroizing::new(pkcs8);
// FIXME ensure the host key is only readable by the ssh server user
let pem = proto::openssh::encode(&*key)?;
host_key_file.write_all(pem.as_bytes())?;
eprintln!("generated host key at {}", args.host_key_file);
return Ok(());
}
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {
anyhow::bail!("host key file `{}` already exists", args.host_key_file);
}
Err(err) => return Err(err.into()),
}
} else {
match HostKeys::from_dir(Path::new("/etc/ssh"), provider) {
Ok(host_keys) => {
info!(len = host_keys.len(), "loaded host keys from /etc/ssh");
host_keys
}
Err(error) => {
eprintln!("failed to load host keys from /etc/ssh: {error}");
let pem = Zeroizing::new(fs::read_to_string(&args.host_key_file).context(
format!("failed to read host key from {}", args.host_key_file),
)?);
HostKeys::from_openssh_v1(&pem, provider)?
}
}
};
let session_bin = match args.session_bin {
Some(path) => path,
None => {
let exe = env::current_exe()?;
let Some(dir) = exe.parent() else {
anyhow::bail!("cannot determine directory of current executable");
};
dir.join("oxish-session")
}
};
if !session_bin.is_file() {
anyhow::bail!("session binary `{}` not found", session_bin.display());
}
let listener = match (ListenFd::from_env().take_tcp_listener(0)?, args.port) {
(Some(listener), None) => {
listener.set_nonblocking(true)?;
TcpListener::from_std(listener)?
}
(None, Some(port)) => {
let addr = SocketAddr::from((Ipv4Addr::UNSPECIFIED, port));
TcpListener::bind(addr).await?
}
(Some(_), Some(_)) => anyhow::bail!("LISTEN_FDS and --port conflict with each other"),
(None, None) => anyhow::bail!("unless LISTEN_FDS is set, --port is required"),
};
info!(addr = %listener.local_addr()?, "listening for connections");
#[cfg_attr(not(debug_assertions), expect(unused_mut))]
let mut config = Config::default();
#[cfg(debug_assertions)]
{
config.spawn = args.spawn;
}
Arc::new(
Server::new(
DefaultStore::new(provider)?,
host_keys,
session_bin,
provider,
)?
.with_config(config),
)
.run(listener)
.await
}
#[derive(Debug, Parser)]
struct Args {
#[clap(short, long)]
port: Option<u16>,
#[clap(long, default_value = "ssh_host_ed25519_key")]
host_key_file: String,
#[clap(long)]
generate_host_key: bool,
#[clap(long, value_parser = host_key_type, default_value = "ssh-ed25519")]
host_key_type: PublicKeyAlgorithm<'static>,
/// Path to the `oxish-session` binary (defaults to a sibling of this executable)
#[clap(long)]
session_bin: Option<PathBuf>,
#[cfg(debug_assertions)]
#[clap(long, action = ArgAction::Set, default_value_t = true)]
spawn: bool,
}
fn host_key_type(name: &str) -> Result<PublicKeyAlgorithm<'static>, String> {
match PublicKeyAlgorithm::typed(name) {
PublicKeyAlgorithm::Unknown(_) => Err(format!("unsupported host key type `{name}`")),
algorithm => Ok(algorithm.to_owned()),
}
}
|