So You Want to Build a Language VM - Part 23 - SSH Server: Part 1
Covers the first part of adding an SSH server to the Iridium VM
Intro
Right now we have two ways to interact with the Iridium VM:
Running
iridium <somefile>
The REPL But the Iridium VM is supposed to be more than a language interpreter; it should be a platform where you can deploy applications, check their status, failover, and lots more. This means we need a way for:
More than one person to interact with the running VM
A secure way to access the VM remotely
Multi-user Access
This one isn’t too bad to implement. We could allow multiple people to connect over a Unix pipe, but that would restrict it to being accessed only from that server. A better solution is to bind to a network interface and listen for network connections.
Permissions
More complicated is figuring out a permissions system. If I am running two applications, I may want to give a co-worker access to one of those, but not the other. How do we define users? How granular do we want access control to be? Should we integrate with Active Directory/LDAP?
All good questions! For now, we’re going to go with the model of all or nothing. If you can SSH to the VM, you can do anything.
Secure Remote Access
It is trivial to open a port and start accepting traffic. We could break on \n
, parse out commands, etc. Except for one minor detail…that would all happen in the clear; anything you type and send to the VM could be read by something between you and the VM.
To deal with that, we can use SSH. This is something a lot of developers are already using, and will make interacting with the Iridium VM similar to interacting with a general Linux server.
Important | Do not try to roll your own crypto solution! I see this over and over from newer coders. Use an existing library that has been examined and vetted. |
Thrussh
We’ll use a crate called thrussh
to add a SSH server to Iridium. You can find it here: https://docs.rs/thrussh/0.20.0/thrussh/. Add it to your dependencies. We’ll also need thrussh_keys, futures, and tokio. The lines to add to Cargo.toml
are:
thrussh = "0.20.0"
thrussh-keys = "0.9.5"
futures = "0.1.24"
tokio = "0.1.8"
Important | It relies on libsodium. This means you’ll need to install it. On OSX, it was brew install libsodium . |
You’ll also need to add externs
for them in lib.rs:
extern crate thrussh;
extern crate thrussh_keys;
extern crate futures;
extern crate tokio;
Implementation
This should be rather simple. We need to do the following:
Add CLI flag for turning on the SSH server feature
Add CLI flag to specify the port it will listen on
Add a REPL command to add an authorized key
Add an
iridium
sub-command to add an authorized keyCreate an SSH server in a background thread
Give each connection a REPL
…OK, maybe that isn’t so simple. But let’s give it a shot!
Step 1: Enable SSH CLI Flag
We’ll be doing the first three things in src/bin/cli.yml
and src/bin/iridium.rs
. Open up cli.yml
and let’s add the flag for turning on the SSH feature:
- ENABLE_SSH:
help: Enables the SSH server component of Iridium VM
required: false
takes_value: false
long: enable-ssh
Now if we compile (make dev
) and run iridium --help
, we should see:
$ iridium --help
iridium 0.0.23
Fletcher Haynes <fletcher@subnetzero.io>
Interpreter for the Iridium language
USAGE:
iridium [FLAGS] [OPTIONS] [INPUT_FILE]
FLAGS:
--enable-ssh Enables the SSH server component of Iridium VM
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-t, --threads <THREADS> Number of OS threads the VM will utilize
ARGS:
<INPUT_FILE> Path to the .iasm or .ir file to run
Now a port…
- SSH_PORT:
help: Which port Iridium should listen for SSH connections on
required: false
takes_value: true
long: ssh-port
short: p
And now for the subcommand. An example of running it would be:
iridium add-ssh-key /path/to/public/key
Add this to the cli.yml
file:
subcommands:
- add-ssh-key:
about: Adds a public key to the list of keys authorized to access this VM remotely
version: "0.0.1"
author: Fletcher Haynes <fletcher@subnetzero.io>
args:
- PUB_KEY_FILE:
help: Path to the file containing the public key
index: 1
required: true
Adding the Rust Code
Head over to src/bin/iridium.rs
. This is where we’ll put some code to handle some of the flags. In the main
function, add this simple if check:
let yaml = load_yaml!("cli.yml");
let matches = App::from_yaml(yaml).get_matches();
if matches.is_present("add-ssh-key") {
println!("User tried to add SSH key!");
// You may need to add `use std;` to the top
std::process::exit(0);
}
This will tell us if the sub-command was used. For now, we’ll print a message. If we compile and test, we should see:
$ iridium add-ssh-key test
User tried to add SSH key!
Next up, let’s check if the enable SSH flag was passed. Add this just below the check for add-ssh-key
:
if matches.is_present("enable-ssh") {
println!("User wants to enable SSH!");
if matches.is_present("ssh-port") {
println!("They'd like to use port {:#?}", matches.value_of("ssh-port"));
}
}
And now if we compile and run it:
$ iridium --enable-ssh -p 2333
User wants to enable SSH!
They'd like to use port None
Welcome to Iridium! Let's be productive!
>>> !quit
Farewell! Have a great day!
We have stubs now for the arguments.
SSH Server Implementation
Now we need to make an SSH server. This is going to be an important, separate sub-component of Iridium, so let’s put it in its own module: src/ssh.rs
.
Open up that file, and in it put:
use std;
use futures;
use thrussh_keys;
use std::sync::Arc;
use thrussh::*;
use thrussh::server::{Auth, Session};
use thrussh_keys::*;
As always, we’ll start with a basic struct. =)
#[derive(Clone, Debug)]
pub struct Server {
}
And now, we paste in a blob of futures code. I should note that I dislike the futures model of coding. Like, a lot. :angry_face:
But we’ll go through modifying this part in detail in a bit.
impl server::Server for Server {
type Handler = Self;
fn new(&self) -> Self {
self.clone()
}
}
impl server::Handler for Server {
type Error = std::io::Error;
type FutureAuth = futures::Finished<(Self, server::Auth), Self::Error>;
type FutureUnit = futures::Finished<(Self, server::Session), Self::Error>;
type FutureBool = futures::Finished<(Self, server::Session, bool), Self::Error>;
fn finished_auth(self, auth: Auth) -> Self::FutureAuth {
futures::finished((self, auth))
}
fn finished_bool(self, session: Session, b: bool) -> Self::FutureBool {
futures::finished((self, session, b))
}
fn finished(self, session: Session) -> Self::FutureUnit {
futures::finished((self, session))
}
fn auth_publickey(self, _: &str, _: &key::PublicKey) -> Self::FutureAuth {
futures::finished((self, server::Auth::Accept))
}
fn data(self, channel: ChannelId, data: &[u8], mut session: server::Session) -> Self::FutureUnit {
println!("data on channel {:?}: {:?}", channel, std::str::from_utf8(data));
session.data(channel, None, data);
futures::finished((self, session))
}
}
This is copy-pasted from https://docs.rs/thrussh/0.20.0/thrussh/, and we can modify it. I suggest reading their docs.
Before we start modifying the server, let’s get it running in a background thread and see if it can accept connections.
Starting the SSH server
Make a new function at the end of bin/iridium.rs
called start_ssh_server
that has:
fn start_ssh_server() {
let _t = std::thread::spawn(|| {
let mut config = thrussh::server::Config::default();
config.connection_timeout = Some(std::time::Duration::from_secs(600));
config.auth_rejection_time = std::time::Duration::from_secs(3);
let config = Arc::new(config);
let sh = iridium::ssh::Server{};
thrussh::server::run(config, "0.0.0.0:2223", sh);
});
}
And now, head back up to the beginning of main
, and in the check for ENABLE_SSH
, let’s call start_ssh_server
:
if matches.is_present("ENABLE_SSH") {
println!("User wants to enable SSH!");
if matches.is_present("SSH_PORT") {
println!("They'd like to use port {:#?}", matches.value_of("ssh-port"));
}
start_ssh_server()
}
Now let’s see if it even started a server:
$ iridium --enable-ssh
User wants to enable SSH!
Welcome to Iridium! Let's be productive!
$ ssh localhost -p 2223
ssh_exchange_identification: Connection closed by remote host
And in our Iridium REPL we can see:
>>> err Error(Timer(TooLong))
Woohoo! We are most of the way there.
End
This post is getting a bit long, so we’ll pick this up in the next one. We should be able to wrap up SSH access. See you then!
If you need some assistance with any of the topics in the tutorials, or just devops and application development in general, we offer consulting services. Check it out over here or click Services along the top.