rustnetconf
A Rust network automation platform: async NETCONF client library, YANG code generation, vendor profiles, connection pooling, and a Terraform-like CLI for declarative network config management.
Built on tokio, russh, and rustls — pure Rust, no OpenSSL, no libssh2.
Workspace
| Crate | Description |
|---|---|
| rustnetconf | Async NETCONF 1.0/1.1 client library |
| rustnetconf-yang | YANG model code generation (compile-time config validation) |
| rustnetconf-cli | Terraform-like CLI tool (netconf binary) |
What's New in v0.11.0
Security remediation pass — addresses the seven findings from the internal audit (RNC-SEC-001..006 + CI hardening). Merged via PR #27.
Breaking changes:
ClientBuilderdefaultHostKeyVerificationis nowRejectAll(fail closed). Connections refuse to complete until the caller pins a fingerprint or explicitly opts in toAcceptAll.ProxyJumphops parsed from~/.ssh/configlikewise default toRejectAll.inventory.toml: devicepasswordandkey_passphrasefields now deserialize into aSecretStringnewtype.DebugprintsSecretString(***)and contents zeroize on drop.ClientBuilder::password/.key_passphrasenow acceptOption<Zeroizing<String>>(was plainOption<String>).
Security fixes:
- RNC-SEC-001 — SSH host-key verification fails closed by default in both the library and the CLI. New CLI flag
--insecure-accept-host-keyfor lab use; otherwisehost_key_fingerprintmust be set per device ininventory.toml. - RNC-SEC-002 — RUSTSEC-2023-0071 (rsa Marvin Attack timing side-channel) risk-accepted via
.cargo/audit.tomlwith reachability analysis and review date 2026-08-01. russh bumped 0.60.2 → 0.60.3. - RNC-SEC-003 — Inventory passwords use the new
SecretStringtype with redactedDebugand a customDeserializethat zeroizes on drop. - RNC-SEC-004 — State files always land at
0o600via atomic temp-file +rename(2), even when a pre-existing file had looser permissions..netconfand.netconf/stateare forced to0o700on every call. Stale temp files from a prior crash are cleaned up. - RNC-SEC-005 —
applyandrollbackguarantee candidate-lock cleanup on error. NewClient::release_candidate_lock_best_effort(discard-changes + unlock, swallowing errors) is invoked from extracted*_locked_regionhelpers. - RNC-SEC-006 — Desired XML is validated for well-formedness before any device connection or candidate lock. Errors name the offending file.
CI hardening:
- New
.github/workflows/ci.ymlruns build, test (workspace, all features), clippy with-D warnings, rustfmt, andcargo auditon every push and PR to main.
Quality fixes:
- YANG codegen: generated
use super::*;/use crate::serialize::*;imports now carry#[allow(unused_imports)]so modules without leaf references compile cleanly under-D warnings. - YANG codegen test: corrected
r#type→type_to match the field-sanitization the generator actually emits.
What's New in v0.10.0
Breaking changes:
HostKeyVerificationno longer implementsDefault— callers must explicitly choose a host key policySshAuth::PasswordandSshAuth::KeyFile { passphrase }now useZeroizing<String>instead ofString- User-provided XML content (RPC bodies, filters, configs) is now validated for well-formedness before sending
Security fixes:
- Shell injection via ProxyCommand
%h/%psubstitution — values are now shell-escaped - Credentials (passwords, passphrases) zeroized on drop via the
zeroizecrate - XML fragment validation prevents injection through malformed RPC content
- TLS
danger_accept_invalid_certsnow emits a detailed warning about the full scope of the bypass - CLI device names validated to prevent path traversal; state files written with
0600permissions
New features:
- Configurable RPC timeout (
.rpc_timeout(Duration)) — prevents indefinite blocking on unresponsive devices - Configurable read buffer size (
.max_read_buffer(bytes)) — defaults to 100 MB - IPv6 address support — bracket notation (
[::1]:830) and bare IPv6 addresses - Capability normalization — legacy Junos capability URIs are mapped to standard URIs during session establishment
Quality improvements:
- Connection pool health checks — dead connections are discarded on checkout and drop instead of being recycled
- Blocking
std::fs::read_to_stringin async context replaced withtokio::fs - Unnecessary
Arc<Mutex<>>removed fromSshTransport AtomicU64message counter replaced with plainu64(Session is&mut selfonly)- YANG codegen: full container/list XML serialization, complete Rust keyword list, hard error on module load failure
- CLI: plan summary fixed for non-JSON mode, diff engine compares all list elements
- Removed unused
futuresdependency andquick-xmlserialize feature ErrorTagimplementsstd::str::FromStr;Session::validate()checks:validatecapability- Dependency updates: russh 0.60.2, rustls 0.23.40, tokio 1.52.2, rustls-webpki 0.103.13
RFC Support
| RFC | Feature | Status |
|---|---|---|
| RFC 6241 | Network Configuration Protocol (NETCONF) | ✅ supported |
| RFC 6242 | NETCONF over SSH | ✅ supported |
| RFC 7589 | NETCONF over TLS | ✅ supported (feature flag tls) — needs physical SRX or non-vSRX for TLS test |
| RFC 5277 | Event Notifications | ✅ supported — tested on Junos 24.4 vSRX (subscription + capability; interleave limited by device) |
| RFC 5717 | Partial Lock RPC | 💡 planned |
| RFC 8071 | NETCONF Call Home | 💡 planned |
| RFC 6243 | With-defaults Capability | 💡 planned |
| RFC 6022 | YANG Module for NETCONF Monitoring | 💡 planned |
| RFC 8526 | NETCONF Extensions for NMDA | 💡 planned |
| RFC 6470 | NETCONF Base Notifications | 💡 planned |
| RFC 8040 | RESTCONF | 💡 planned |
CLI Tool — netconf
Declarative network config management. Write desired state as XML files, the CLI diffs against the device and applies changes with confirmed-commit safety.
Project Structure
my-network/
├── inventory.toml # Device connection details
├── desired/
│ └── spine-01/
│ ├── interfaces.xml # Desired interface config
│ └── system.xml # Desired system config
└── .netconf/state/ # Rollback snapshots (auto-managed)
inventory.toml
[]
= 60
[]
= "10.0.0.1:830"
= "admin"
= "~/.ssh/id_ed25519"
# vendor auto-detected from device hello
Secrets: inventory.toml may contain plaintext passwords. Prefer
key_file or SSH-agent auth where possible. If you must use inline
passwords, protect the file with chmod 600 inventory.toml and add it
to .gitignore. Passwords are stored in zeroizing memory and redacted
from Debug output, but the on-disk file itself is plaintext.
Library — Quick Start
[]
= { = "https://github.com/fastrevmd-lab/rustnetconf.git" }
= { = "1", = ["full"] }
For TLS transport (RFC 7589), enable the tls feature:
[]
= { = "https://github.com/fastrevmd-lab/rustnetconf.git", = ["tls"] }
Fetch running config
use ;
async
Edit config (full round trip)
use ;
async
Connect through a jump host (ProxyJump)
use ;
use ;
async
Connect using your ~/.ssh/config
use ;
async
Connect over TLS (RFC 7589)
Note: vSRX 24.4 has a known TLS handshake issue where the PKI engine cannot present a self-signed certificate chain. TLS testing requires a physical SRX, MX, or EX device with a CA-signed certificate. The code compiles and passes unit tests but has not been validated against a live TLS-capable device.
use ;
async
Event notifications (RFC 5277)
use ;
async
Note: Some devices (e.g., Junos vSRX 24.4) advertise
:interleavebut do not respond to RPCs on a session with an active subscription. On these devices, use a dedicated session for notifications and a separate session for RPCs. Notifications arriving during RPCs on interleave-capable devices are automatically buffered and available viadrain_notifications().
Connection pooling
use ;
use SshAuth;
use Datastore;
use Zeroizing;
let pool = builder
.max_connections
.add_device
.build;
let mut conn = pool.checkout.await?;
let config = conn.get_config.await?;
// connection auto-returned to pool on drop
Features
NETCONF Client
- Async-first — tokio-based, push config to 500 devices concurrently
- SSH + TLS transports — SSH (RFC 6242) by default, TLS (RFC 7589) via
tlsfeature flag - SSH bastion support —
ProxyJump(multi-hop),ProxyCommand(shell-escaped), and OpenSSH~/.ssh/configalias resolution - NETCONF 1.0 + 1.1 — EOM and chunked framing with auto-negotiation
- All core RPCs — get, get-config, edit-config, lock/unlock, commit, validate, close/kill-session, discard-changes
- Confirmed commit — auto-rollback safety net (RFC 6241 §8.4)
- Event notifications —
create-subscription, inline notification demux, buffered drain/recv API (RFC 5277) - RPC timeout — configurable per-session deadline prevents indefinite blocking on unresponsive devices
- XML fragment validation — user-provided RPC content is validated before insertion to prevent XML injection
- CommitUnknown detection — distinguishes "commit failed" from "maybe committed, connection lost"
- Stale lock recovery —
lock_or_kill_stale()kills crashed sessions holding locks - Framing mismatch detection — catches firmware bugs where devices send wrong framing
- IPv6 support — connect to devices using bracket notation (
[::1]:830) or bare IPv6 addresses
Vendor Profiles
- Auto-detection from device
<hello>capabilities - Junos — config wrapping, namespace normalization, discard-before-close
- Generic — standard RFC 6241 for any compliant device
- Extensible — implement
VendorProfiletrait for custom vendors
Connection Pool
- Tokio semaphore-based concurrency limiting
- Checkout with timeout (no blocking forever)
- Auto-checkin on drop with health check — dead connections are discarded, not recycled
- Connection reuse from idle pool
YANG Code Generation
- Build-time generation from
.yangmodel files via libyang2 - Typed Rust structs with serde Serialize/Deserialize
- Full XML serialization — leaves, containers, and lists
- Correct type mapping (string, bool, uint32, etc.)
- Complete Rust keyword escaping for YANG node names
- Bundled IETF models: ietf-interfaces, ietf-ip, ietf-yang-types, ietf-inet-types
Authentication
| Method | Transport | Builder API |
|---|---|---|
| Password | SSH | .password("secret") |
| Key file | SSH | .key_file("~/.ssh/id_ed25519") |
| SSH agent | SSH | .ssh_agent() |
| Server-only TLS | TLS | TlsConfig { ca_cert, .. } |
| Mutual TLS (mTLS) | TLS | TlsConfig { client_cert, client_key, .. } |
SSH Connection Options
| Option | Builder API | Notes |
|---|---|---|
| Direct TCP | (default) | No proxy |
ProxyJump (bastion chain) |
.jump_hosts(Vec<JumpHostConfig>) |
Each hop has its own credentials and host-key policy |
ProxyCommand |
.proxy_command("ssh -W %h:%p bastion") |
%h/%p shell-escaped and substituted; runs under sh -c |
~/.ssh/config alias |
Client::connect_via_ssh_config("alias")? |
Resolves HostName, Port, User, IdentityFile, ProxyJump, ProxyCommand, Include |
jump_hosts and proxy_command are mutually exclusive at connect time.
Error Handling
Layered errors matching the protocol stack:
match result
Supported Operations
| Operation | RFC 6241 | Status |
|---|---|---|
get |
§7.7 | Done |
get-config |
§7.1 | Done |
edit-config |
§7.2 | Done |
lock / unlock |
§7.4-7.5 | Done |
close-session |
§7.8 | Done |
kill-session |
§7.9 | Done |
commit |
§8.4 | Done |
confirmed-commit |
§8.4 | Done |
validate |
§8.6 | Done |
discard-changes |
§8.3 | Done |
Testing
197 tests across the workspace:
- Unit tests — framing, RPC serialization, capability parsing, vendor profiles, diff engine, inventory parsing, IPv6 address parsing, XML fragment validation, capability normalization
- Mock transport tests — session state machine, CommitUnknown detection, lock recovery
- Integration tests — 32 tests against a live Juniper vSRX including full edit-config round trips, vendor auto-detection, connection pooling, and concurrent sessions
Prerequisites
The rustnetconf-yang subcrate builds libyang2 from source via yang2's bundled feature, which requires cmake. Install it before running workspace-wide tests or clippy:
# Debian/Ubuntu
# macOS
# Fedora/RHEL
The core rustnetconf and rustnetconf-cli crates do not require cmake; cargo test -p rustnetconf works without it.
SKIP_INTEGRATION=1
Security
Known Issues
-
RSA timing sidechannel (RUSTSEC-2023-0071) — The
rsacrate (transitive dependency viarussh → internal-russh-forked-ssh-key → rsa) has a known timing sidechannel that could theoretically allow RSA key recovery. No upstream fix is available. Mitigation: Use Ed25519 or ECDSA keys instead of RSA for SSH authentication. -
Debug logs may contain file paths — When SSH key file loading fails, the key file path is included in
tracing::debug!output. This is not exposed at info/warn/error levels. Mitigation: Disable debug-level logging in production, or filterrustnetconf::transportlogs.
Security Features
- Credential zeroization — Passwords and key passphrases use
Zeroizing<String>(via thezeroizecrate) and are securely erased from memory on drop. - SSH host key verification —
HostKeyVerificationmust be set explicitly. TheClientBuilderdefault isRejectAll(fail closed): the SSH handshake fails until the caller pins a fingerprint viaFingerprint("SHA256:...")or explicitly opts in toAcceptAllfor lab use (logs atracing::warn!).ProxyJumphops parsed from~/.ssh/configlikewise default toRejectAlland must be individually configured. In the CLI, sethost_key_fingerprintper device ininventory.toml, or pass--insecure-accept-host-keyfor lab use only. - Shell-escaped ProxyCommand —
%hand%psubstitutions are shell-escaped to prevent command injection via malicious hostnames. - XML fragment validation — All user-provided RPC content is validated for well-formedness before insertion, preventing XML injection.
- XML attribute escaping — All message-id values are escaped to prevent XML attribute injection.
- TLS bypass warnings —
danger_accept_invalid_certsemits a detailed warning explaining that ALL certificate validation is bypassed (trust chain, signatures, hostname, and expiry). - Read buffer limits — Session read buffers default to 100 MB (configurable via
.max_read_buffer()) to prevent memory exhaustion. - RPC timeout — Configurable via
.rpc_timeout()to prevent indefinite blocking on unresponsive devices. - CLI input validation — Device names are validated to prevent path traversal; state files are written with
0600permissions on Unix. - Typed error hierarchy — Structured error types (
ChannelClosed,SessionExpired,MessageIdMismatch) enable precise error handling without string matching. - No unsafe code — The entire codebase uses safe Rust.
Known advisories
- RUSTSEC-2023-0071 (Marvin Attack,
rsacrate timing side-channel) is present in the dependency graph viarussh → internal-russh-forked-ssh-key → rsa 0.10.0-rc.16. No fixed upstream release is available yet. The advisory is risk-accepted with rationale in.cargo/audit.tomland CI re-checks on every run. It only matters when an RSA SSH key is used for authentication — Ed25519 and ECDSA paths are unaffected. Use the mitigation in the next section.
Security Best Practices
- Use Ed25519 SSH keys (not RSA) for device authentication (also mitigates RUSTSEC-2023-0071 above)
- Set
host_key_verification(HostKeyVerification::Fingerprint(...))in production — the default isRejectAll(fail closed), so the connection will refuse to complete until you choose a policy. For the CLI, sethost_key_fingerprint = "SHA256:..."per device ininventory.toml. - Set
.rpc_timeout(Duration::from_secs(30))to prevent hanging on unresponsive devices - Prefer SSH agent auth over inline passwords
- Store credentials in inventory.toml with restricted file permissions (
chmod 600) - Run the CLI on trusted management networks with direct device connectivity
- Use
confirmed-commit(the default fornetconf apply) so the device auto-reverts if something goes wrong - Disable debug-level logging in production environments
To report a security vulnerability, please open an issue on GitHub.
Dependencies
| Crate | Version | Purpose |
|---|---|---|
async-trait |
0.1 | Async trait support |
quick-xml |
0.37 | XML parsing (NETCONF RPC encode/decode) |
russh |
0.60 | SSH transport (pure Rust, no libssh2) |
thiserror |
2 | Error derive macros |
tokio |
1 | Async runtime |
tracing |
0.1 | Structured logging/tracing |
zeroize |
1 | Secure credential erasure on drop |
Optional (behind tls feature):
| Crate | Version | Purpose |
|---|---|---|
rustls |
0.23 | TLS transport (pure Rust, no OpenSSL) |
tokio-rustls |
0.26 | Async TLS stream adapter |
webpki-roots |
0.26 | Mozilla CA root certificates |
Dev-only:
| Crate | Version | Purpose |
|---|---|---|
tokio-test |
0.4 | Async test utilities |
tracing-subscriber |
0.3 | Log subscriber for tests |
tempfile |
3 | Temporary directories for tests |
License
MIT OR Apache-2.0
Contributing
Contributions welcome! See ARCHITECTURE.md for the codebase design and TODOS.md for tracked work items.