1

Rust SSH

 2 years ago
source link: https://saidvandeklundert.net/learn/2021-08-06-rust-ssh-cli-tool/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Rust SSH

Learning Rust by building a CLI tool

Posted on August 6, 2021

Creating small CLI tools is a fun way to get more familiar with a programming language. If you are coming from an infrastructure background, a CLI tool that you can use to send commands to devices/servers might be considered a neat starting point getting into Rust. This is how I started off learning Python, by writing small things that were usefull in a context that I was familiar with. Back then, I used argparse, getpass and netmiko. Starting with Rust is pretty daunting, and I have found that using similar tactic used to learn Python can also be applied when learning Rust.

In this article, I explore a tiny bit of Rust by building a CLI tool that will let you log into a router or a server and issue a command over SSH. I hope the examples here can offer you a nice running start exploring Rust.

Running a CLI command over SSH

In case you do not have Rust installed, one easy way of going about things is to grab the rust Docker container:

docker pull rust
docker run --name='ru' --hostname='ru' -di rust /bin/sh
docker exec -it ru bash
apt-get update
apt-get install vi

The Rust image is 1.22GB 😳

After this, we use cargo new to start a new project:

cargo new ssh
cd ssh/
vi Cargo.toml

We put the following in our Cargo.toml:

[package]
name = "ssh_example"
version = "0.1.0"
edition = "2018"

[dependencies]
ssh2 = "0.9"

This let’s us pull in the ssh2-rs, which offers Rust bindings to libssh2, the ssh client library written in C.

Now we put in our first example script. We edit the main.rs file in the src directory:

vi src/main.rs

Then we put in the following:

use ssh2::Session;
use std::io::prelude::*;
use std::net::TcpStream;

fn main() {
    let tcp = TcpStream::connect("192.168.1.1:22").unwrap();
    let mut sess = Session::new().unwrap();
    sess.set_tcp_stream(tcp);
    sess.handshake().unwrap();
    sess.userauth_password("username", "password")
        .unwrap();
    let mut channel = sess.channel_session().unwrap();
    channel.exec("show version").unwrap();
    let mut s = String::new();
    channel.read_to_string(&mut s).unwrap();
    println!("{}", s);
    channel.wait_close().ok();
    println!("{}", channel.exit_status().unwrap());
}

This will use SSH to connect to a system configured with IP address 192.168.1.1 and issue the show version command.

After putting in the file, we can use cargo run to run the code:

root@ru:/ssh# cargo run 

    Finished dev [unoptimized + debuginfo] target(s) in 0.83s
     Running `target/debug/ssh`
Arista DCS-7050TX-64-R
Hardware version:    01.01
Serial number:       KLF80800800
System MAC address:  001c.84ec.cb72

Software image version: 4.20.12M
Architecture:           i386
Internal build version: 4.20.12M-11527863.42012M
Internal build ID:      3a8329a8-af7b-4a7e-ae1c-cad006c5540d

Uptime:                 114 weeks, 1 days, 7 hours and 2 minutes
Total memory:           3818208 kB
Free memory:            2258276 kB


0

Collecting user input

We can add some CLI options to running this script by using structopt. First, change our Cargo.toml to the following:

[package]
name = "ssh_example"
version = "0.1.0"
edition = "2018"

[dependencies]
ssh2 = "0.9"
structopt = "0.3.21"

This brings in the structopt crate and let’s us define some arguments for our CLI script. In case you are familiar with Python, think argparse.

Let’s first create a script that only collects the arguments:

use structopt::StructOpt;

#[derive(Debug, StructOpt)]
#[structopt(name = "structopt example", about = "using structop")]
struct Args {
    #[structopt(short = "h", long)]
    host: String,
    #[structopt(short = "c", long)]
    command: String,
    #[structopt(short = "u", long)]
    username: String,
}

fn main() {
    println!("Structopt example");
    let args = Args::from_args();
    println!(
        "{:?}\nHost: {}\nCommand: {}\nUsername: {}",
        args, args.host, args.command, args.username
    );
}

We can run the above like so:

root@ru:/ssh# cargo run -- -h 192.168.1.50  -c 'show version' -u said
   Compiling ssh_example v0.1.0 (/ssh)
    Finished dev [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/ssh_example -h 192.168.1.50 -c 'show version' -u said`
Structopt example
Args { host: "192.168.1.50", command: "show version", username: "said" }
Host: 192.168.1.50
Command: show version
Username: said

Looks good.

Though, we would also need to collect the password. For that, we include the rpassword crate by adding the following to our Cargo.toml:

rpassword = "5.0"

The rpassword crate allows us to ask users for a password without echoing it to screen. To collect the arguments as well as the password, we now have the following:

extern crate rpassword;
use rpassword::read_password;
use structopt::StructOpt;

#[derive(Debug, StructOpt)]
#[structopt(name = "structopt example", about = "using structop")]
struct Args {
    #[structopt(short = "h", long)]
    host: String,
    #[structopt(short = "c", long)]
    command: String,
    #[structopt(short = "u", long)]
    username: String,
}

fn main() {
    println!("Enter your password: ");
    let password = read_password().unwrap();

    println!("The password is: '{}'", password);
    println!("Structopt example");
    let args = Args::from_args();
    println!(
        "{:?}\nHost: {}\nCommand: {}\nUsername: {}",
        args, args.host, args.command, args.username
    );
}

When we run this, we see the following:

root@ru:/ssh# cargo run -- -h 192.168.1.50  -c 'show version' -u said
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/ssh_example -h 192.168.1.50 -c 'show version' -u said`
Enter your password: 

The password is: 'mypassw0rd'
Structopt example
Args { host: "192.168.1.50", command: "show version", username: "said" }
Host: 192.168.1.50
Command: show version
Username: said

Great! Now we can move on and combine the two.

Tying things together

We create the following Cargo.toml:

[package]
name = "ssh_example"
version = "0.1.0"
edition = "2018"

[dependencies]
ssh2 = "0.9"
structopt = "0.3.21"
rpassword = "5.0"

Then we update the SSH script:

use ssh2::Session;
use std::io::prelude::*;
use std::net::TcpStream;
extern crate rpassword;
use rpassword::read_password;
use structopt::StructOpt;

#[derive(Debug, StructOpt)]
#[structopt(name = "structopt example", about = "using structop")]
struct Args {
    #[structopt(short = "h", long)]
    host: String,
    #[structopt(short = "c", long)]
    command: String,
    #[structopt(short = "u", long)]
    username: String,
}

fn main() {
    println!("Enter your password: ");
    let password = read_password().unwrap();
    let args = Args::from_args();
    println!(
        "Running command {} against host {}:\n",
        args.command, args.host
    );
    let tcp = TcpStream::connect(args.host + ":22").unwrap();
    let mut sess = Session::new().unwrap();
    sess.set_tcp_stream(tcp);
    sess.handshake().unwrap();
    sess.userauth_password(&args.username, &password).unwrap();
    let mut channel = sess.channel_session().unwrap();
    channel.exec(&args.command).unwrap();
    let mut command_output = String::new();
    channel.read_to_string(&mut command_output).unwrap();
    println!("{}", command_output);
    channel.wait_close().ok();
    println!("{}", channel.exit_status().unwrap());
}

Running the above gives us the following:

root@ru:/ssh# cargo run -- -h 192.168.1.50  -c 'show version' -u said

    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/ssh_example -h 192.168.1.50 -c 'show version' -u said`
Enter your password: 

Running command show version against host 192.168.1.50:

Arista DCS-7050TX-64-R
Hardware version:    01.01
Serial number:       KLF80800800
System MAC address:  001c.84ec.cb72

Software image version: 4.20.12M
Architecture:           i386
Internal build version: 4.20.12M-11527863.42012M
Internal build ID:      3a8329a8-af7b-4a7e-ae1c-cad006c5540d

Uptime:                 114 weeks, 1 days, 7 hours and 2 minutes
Total memory:           3818208 kB
Free memory:            2258276 kB


0

Instead of targeting a router, we can also play around targeting Linux servers:

root@ru:/ssh# cargo run -- -h 10.0.0.1  -c 'ls -ltr' -u said           

    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/ssh_example -h 10.0.0.1 -c 'ls -ltr' -u said`
Enter your password: 

Running command ls -ltr against host 10.0.0.1:

total 25032
-rw-r--r--.  1 said UnixUsers 25627989 May  3 11:11 Python-3.9.5.tgz
drwxr-xr-x. 16 said UnixUsers     4096 Jun  8 06:38 Python-3.9.5

0

Wrapping up

We created a small CLI tool that allows us to send a command over SSH to a server or a router. In the examples, I send a command to Linux server and an Arista switch. I did not get into all the specifics and this is not something that is ready for production. For one, I do not properly handle the Result, instead I used unwrap() everywhere. But the main point in this article was to give you a running start sending CLI commands to systems over SSH. I hope this gives you a nice starting point.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK