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:

  1. Running iridium <somefile>

  2. 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:

  3. More than one person to interact with the running VM

  4. 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:

  1. Add CLI flag for turning on the SSH server feature

  2. Add CLI flag to specify the port it will listen on

  3. Add a REPL command to add an authorized key

  4. Add an iridium sub-command to add an authorized key

  5. Create an SSH server in a background thread

  6. 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!
and in another terminal if we do:
$ 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.