ferrissh
An async SSH CLI scraper library for network device automation in Rust.
Warning: This library is EXTREMELY experimental and under active development. The API is subject to change without notice. Use in production at your own risk.
Ferrissh provides a high-level async API for interacting with network devices over SSH, heavily inspired by Python's scrapli and netmiko libraries.
Features
- Async/Await - Built on Tokio and russh for efficient async SSH connections
- Multi-Vendor Support - Linux, Juniper JUNOS, Arista EOS, Nokia SR OS, Arrcus ArcOS
- Privilege Management - Automatic navigation between privilege levels
- Config Sessions - RAII-guarded config sessions with commit, abort, diff, validate, and confirmed commit
- ConfD Support - Generic ConfD config session shared by both C-style and J-style CLI vendors
- Interactive Commands - Handle prompts requiring user input (confirmations, passwords)
- Configuration Mode - Automatic privilege escalation for config commands
- Credential Protection - Passwords and passphrases wrapped in
SecretString(viasecrecy), redacted from Debug output - Multi-Channel - Multiple independent PTY shells on a single SSH connection via
Session+Channel - Zero-Copy Responses -
Payloadtype backed by reference-countedByteswith in-place buffer normalization. Cheap clones. - Pattern Matching - Efficient tail-search buffer matching (scrapli-style optimization)
- Data-Driven Platforms - Platforms are pure data (prompts, privilege graphs, failure patterns) with optional extension traits for configuration sessions
Installation
Add to your Cargo.toml:
[]
= "0.3"
= { = "1", = ["full"] }
Quick Start
use ;
async
Built in Platforms
| Platform | Enum Variant | Privilege Levels | Config Session |
|---|---|---|---|
| Linux/Unix | Platform::Linux |
user ($), root (#) |
- |
| Juniper JUNOS | Platform::JuniperJunos |
exec (>), configuration (#), shell (%) |
JuniperConfigSession |
| Arista EOS | Platform::AristaEos |
exec (>), privileged (#), configuration ((config)#) |
AristaConfigSession |
| Nokia SR OS | Platform::NokiaSros |
exec (#), configuration ((ex)[]#), MD-CLI and Classic CLI |
NokiaSrosConfigSession |
| Arrcus ArcOS | Platform::ArrcusArcOs |
exec (#), configuration ((config)#), ConfD C-style CLI |
ConfDConfigSession |
Config Session Management
Config sessions are RAII-guarded transactions that hold &mut Channel, preventing concurrent use at compile time. The commit() and abort() methods consume the session by value, enforcing single-use.
Ferrissh uses extension traits to express what each vendor's config session supports:
| Trait | Description | Vendors |
|---|---|---|
ConfigSession |
Core trait: send_command, commit, abort, detach |
All |
Diffable |
View uncommitted changes (diff()) |
Juniper, Arista, Nokia, ConfD |
Validatable |
Validate config before commit (validate()) |
Juniper, Arista, Nokia, ConfD |
ConfirmableCommit |
Auto-rollback commit (commit_confirmed(timeout)) |
Juniper, Arista, ConfD |
NamedSession |
Named/isolated config sessions (session_name()) |
Arista |
Each vendor implements only the traits it supports — the type system prevents calling features the vendor doesn't have.
ConfD Support
Ferrissh includes a generic ConfDConfigSession that works with any vendor using Tail-f/Cisco ConfD as their management framework. The config session commands (commit, revert, validate, compare running-config, commit confirmed) are identical for both C-style and J-style ConfD CLIs — only the prompts and navigation commands differ, which are defined in each vendor's platform definition.
Arrcus ArcOS uses this generic ConfD session directly. Future ConfD-based vendors can reuse it with just a platform definition — no new config session code needed.
Usage Examples
Basic Commands
use ;
let mut driver = new
.username
.password
.platform
.build?;
driver.open.await?;
// Single command
let response = driver.send_command.await?;
println!;
// Multiple commands
let responses = driver.send_commands.await?;
for response in responses
driver.close.await?;
SSH Key Authentication
use PathBuf;
let driver = new
.username
.private_key
.platform
.build?;
Configuration Mode
Automatically enter and exit configuration mode:
// send_config handles privilege escalation automatically
let responses = driver.send_config.await?;
// Check for errors
for response in &responses
Config Sessions (RAII-guarded)
Config sessions provide RAII-guarded access to device configuration with commit, abort, diff, validate, and confirmed commit support:
use ;
use JuniperConfigSession;
let mut driver = connect_juniper.await;
// Create a config session (enters config mode)
let mut session = new.await?;
// Make changes
session.send_command.await?;
// Review pending changes
let diff = session.diff.await?;
println!;
// Validate before committing
let result = session.validate.await?;
if result.valid else
Multi-Channel (Multiple Shells on One Connection)
Open multiple independent PTY shells on a single authenticated SSH connection:
use ;
let mut driver = new
.username
.password
.platform
.build?;
driver.open.await?;
// Open a second channel on the same SSH connection
let mut ch2 = driver.open_channel.await?;
// Each channel has its own shell, privilege state, and prompt detection
let = try_join!?;
ch2.close.await?;
driver.close.await?;
For full control, use SessionBuilder to create a session and open channels directly:
use ;
let session = new
.username
.password
.platform
.connect.await?;
let mut ch1 = session.open_channel.await?;
let mut ch2 = session.open_channel.await?;
let = try_join!?;
ch1.close.await?;
ch2.close.await?;
session.close.await?;
Interactive Commands
Handle commands that require confirmation or input:
use ;
// Using the builder (fluent API)
let events = new
.send
.expect?
.send
.expect?
.build;
let result = driver.send_interactive.await?;
if !result.is_success
// With hidden input (passwords)
let events = new
.send
.expect?
.send_hidden // Won't appear in logs
.expect?
.build;
Privilege Level Management
// Check current privilege
if let Some = driver.current_privilege
// Navigate to a specific privilege level
driver.acquire_privilege.await?;
// Do configuration work...
driver.send_command.await?;
// Return to operational mode
driver.acquire_privilege.await?;
Parsing Output with TextFSM
For structured data extraction from CLI output, ferrissh works well with textfsm-rust - a Rust implementation of Google's TextFSM.
As dictionaries
use ;
use Template;
// Define a TextFSM template for parsing `df -h` output
const DF_TEMPLATE: &str = r#"
Value Filesystem (\S+)
Value Size (\S+)
Value Used (\S+)
Value Available (\S+)
Value UsePercent (\d+)
Value MountedOn (\S+)
Start
^Filesystem -> Continue
^${Filesystem}\s+${Size}\s+${Used}\s+${Available}\s+${UsePercent}%\s+${MountedOn} -> Record
"#;
// Run command and parse output
let response = driver.send_command.await?;
let template = parse_str?;
let mut parser = template.parser;
let records = parser.parse_text_to_dicts?;
// Access structured data
for record in records
Into typed structs (serde)
With the serde feature enabled, parse directly into strongly-typed Rust structs:
[]
= { = "0.3", = ["serde"] }
use Deserialize;
use Template;
let template = parse_str?;
let mut parser = template.parser;
let disks: = parser.parse_text_into?;
for disk in &disks
See the textfsm_parsing example for a complete demonstration with templates for Linux and Juniper commands.
Response Payload (Zero-Copy)
Command responses use the Payload type — a zero-copy wrapper around reference-counted Bytes. It implements Deref<Target = str>, so it works anywhere a &str is expected:
let response = driver.send_command.await?;
// All &str methods work via deref coercion
println!; // Display
assert!; // str::contains
for line in response.result.lines
let trimmed: &str = response.result.trim; // str::trim
// Cloning is cheap (reference count increment, no data copy)
let cloned = response.result.clone;
// Convert to owned String when needed
let owned: String = response.result.into_string;
The in-place normalization pipeline (linefeed normalization, echo stripping, prompt removal) operates directly on the buffer with SIMD-accelerated byte search via memchr, avoiding intermediate String allocations.
Adding a Custom Platform
use ;
use Arc;
// Define privilege levels with prompt patterns
let exec = new?;
let config = new?
.with_parent
.with_escalate
.with_deescalate;
// Create platform definition — pure data, no driver code needed
let platform = new
.with_privilege
.with_privilege
.with_default_privilege
.with_failure_pattern
.with_on_open_command
.with_behavior;
// Use with driver
let driver = new
.custom_platform
.username
.password
.build?;
Running the Examples
The ferrissh/examples/ directory contains several examples demonstrating different features. All examples support both password and SSH key authentication.
Common Options
| Option | Description |
|---|---|
--host <HOST> |
Target hostname or IP (default: localhost) |
--port <PORT> |
SSH port (default: 22) |
--user <USER> |
Username (default: $USER) |
--password <PASS> |
Password authentication |
--key <PATH> |
Path to SSH private key |
--timeout <SECS> |
Connection timeout (default: 30) |
--help |
Show help message |
basic_ls - Linux Commands
Basic example connecting to a Linux host and running commands.
# With password
# With SSH key
juniper - Juniper JUNOS
Demonstrates connecting to Juniper devices and running operational/configuration commands.
# Basic operational commands
# Include configuration mode demo
nokia_sros - Nokia SR OS
Demonstrates connecting to Nokia SR OS devices with auto-detection of MD-CLI vs Classic CLI.
arista_eos - Arista EOS
Demonstrates connecting to Arista EOS switches and running operational commands.
config_session - Config Sessions
Demonstrates RAII-guarded config sessions with diff, validate, commit, and abort.
# Juniper config session
# Nokia SR OS config session
multi_channel - Multiple Shells on One Connection
Demonstrates opening multiple PTY channels on a single SSH connection, both from a driver and from a session directly.
SSH_HOST=myserver SSH_USER=admin SSH_PASS=secret
interactive - Interactive Commands
Shows how to handle commands that require user input or confirmation prompts.
textfsm_parsing - Structured Output Parsing
Demonstrates using textfsm-rust to parse CLI output into structured data. Includes templates for common Linux and Juniper commands.
# Parse Linux commands (uname, df, ps)
# Parse Juniper commands (show version, show interfaces terse)
Sample output:
[
{
"filesystem": "/dev/nvme1n1p4",
"size": "853G",
"used": "278G",
"available": "532G",
"usepercent": "35",
"mountedon": "/"
}
]
Filesystems with >50% usage:
/sys/firmware/efi/efivars - 52% used (64K of 128K)
Debug Logging
Enable debug logging to see detailed SSH and parsing information:
RUST_LOG=debug
Log levels: error, warn, info, debug, trace
Security
- Credential protection - Passwords and key passphrases are stored as
SecretString(from thesecrecycrate), which zeroizes memory on drop.Debugformatting onAuthMethodandSshConfigredacts all secrets. - Input validation - Arista config session names are validated against injection (alphanumeric, hyphens, underscores only, max 63 chars). Builder inputs (host, port) are validated before connection.
- RAII safety - Config session guards only mark themselves as consumed after all operations succeed, ensuring
Dropwarnings fire on partial failures.
Planned Features
Platform Support
- Juniper JUNOS
- Nokia SR OS
- Arista EOS
- Arrcus ArcOS
Config Sessions
- RAII-guarded config sessions (commit/abort/detach)
- Diff support
- Validate support
- Confirmed commit support
- Generic ConfD config session (shared by C-style and J-style vendors)
Connection Management
- SSH keepalive configuration
- Connection health checks (
is_alive()) - Multi-channel support (multiple PTY shells per connection)
Macros & Compile-Time Safety
- Proc macro for defining custom platforms declaratively
- Compile-time privilege graph validation
- Compile-time regex verification for prompt patterns
Transport
- Feature-gated
async_ssh2_litebackend
API
- Streaming output API
Dependencies
| Crate | Purpose |
|---|---|
russh |
SSH client library |
ssh-key |
SSH key handling |
tokio |
Async runtime |
bytes |
Zero-copy buffer management (BytesMut/Bytes) |
memchr |
SIMD-accelerated byte search for in-place normalization |
regex |
Pattern matching |
thiserror |
Error handling |
log |
Logging facade |
secrecy |
Credential protection (SecretString with zeroize) |
serde |
Serialization/deserialization |
indexmap |
Deterministic-order maps |
strip-ansi-escapes |
ANSI escape code removal |
License
Licensed under either of
at your option.
Acknowledgments
The architecture and design of ferrissh is heavily influenced by scrapli — the data-driven platform definitions, privilege level graph, and pattern buffer optimization are all concepts from scrapli. If you're working in Python, check it out.