Skip to content

Latest commit

 

History

History
184 lines (164 loc) · 8.91 KB

File metadata and controls

184 lines (164 loc) · 8.91 KB

Architecture — rustnetconf

This document describes the internal architecture of rustnetconf. It serves as the implementation guide and contributor reference.

Design docs:

  • v0.1: ~/.gstack/projects/rustnetconf/mharman-main-design-20260319-130037.md
  • v0.2: ~/.gstack/projects/rustnetconf/mharman-main-design-20260319-170927.md

System Overview

┌─────────────────────────────────────────────┐
│              DevicePool                      │  Async connection pool
│  Semaphore(max) + HashMap<name, Vec<Client>>│  checkout() → PoolGuard
└──────────────┬──────────────────────────────┘
               │
┌──────────────▼──────────────────────────────┐
│         Client (thin wrapper)                │  Ergonomic API, builder patterns
│  .connect() .edit_config() .lock()           │  .vendor_profile() for explicit vendor
│  .vendor_name() → "junos" / "generic"        │  NO protocol state
└──────────────┬──────────────────────────────┘
               │
┌──────────────▼──────────────────────────────┐
│      Session (owns ALL state)                │  msg-id counter, capabilities,
│                                              │  framing strategy, vendor profile,
│  ┌─────────┐ ┌───────────┐ ┌──────────────┐ │  RPC dispatch
│  │ msg-id  │ │capabilities│ │vendor_profile│ │
│  │ counter │ │ base:1.0   │ │ JunosVendor  │ │
│  └─────────┘ │ :candidate │ │ GenericVendor│ │
│              └───────────┘ └──────────────┘ │
│  Framing switch + vendor detect after hello │
└──────┬──────────────────────┬───────────────┘
       │                      │
┌──────▼──────┐    ┌──────────▼────────┐
│ RPC Layer   │    │  Framing Layer    │
│ operations  │    │  ┌─────┐ ┌─────┐ │
│ filter      │    │  │ EOM │ │Chunk│ │
│ (serialize/ │    │  └─────┘ └─────┘ │
│  parse XML) │    │  Selected by      │
└─────────────┘    │  Session at hello │
                   └──────────┬────────┘
                              │
                   ┌──────────▼────────┐
                   │ Transport (trait)  │  AsyncRead + AsyncWrite
                   │  SshTransport     │  byte-stream interface
                   │  MockTransport    │  (for tests)
                   └──────────┬────────┘
                              │
                          ┌───▼───┐
                          │Device │
                          └───────┘

Module Structure

src/
├── lib.rs              # Public API re-exports
├── client.rs           # Client — thin ergonomic wrapper over Session
├── session.rs          # Session — owns msg-id, capabilities, vendor, framing, RPC dispatch
├── transport/
│   ├── mod.rs          # Transport trait + MockTransport (for tests)
│   └── ssh.rs          # SshTransport — russh-based SSH implementation
├── rpc/
│   ├── mod.rs          # RPC types, response parsing (all 7 rpc-error fields)
│   ├── operations.rs   # XML generation: get, get-config, edit-config, lock, unlock,
│   │                   # commit, confirmed-commit, discard-changes, validate, etc.
│   └── filter.rs       # Subtree and XPath filter builders
├── framing/
│   ├── mod.rs          # Framing trait
│   ├── eom.rs          # NETCONF 1.0 end-of-message framing (]]>]]>)
│   └── chunked.rs      # NETCONF 1.1 chunked framing + mismatch detection
├── vendor/
│   ├── mod.rs          # VendorProfile trait + auto-detection
│   ├── junos.rs        # Juniper Junos — config wrapping, capability normalization
│   └── generic.rs      # Default RFC 6241 behavior (passthrough)
├── pool/
│   └── mod.rs          # DevicePool + PoolGuard — async connection pooling
├── error.rs            # Layered error hierarchy (Transport/Framing/Rpc/Protocol)
├── capability.rs       # Capability URIs, negotiation, well-known constants
└── types.rs            # Datastore, DefaultOperation, TestOption, ErrorSeverity, etc.

Key Design Decisions

1. Transport Trait = Byte Stream

The Transport trait provides raw byte read/write. Framing sits above transport:

  • Framing logic written once, shared by all transports (DRY)
  • Independently testable without SSH
  • Future transports (TLS, RESTCONF) plug in without reimplementing framing

2. Thin Client + Fat Session

Client is an ergonomic wrapper. Session owns all protocol state:

  • message_id: AtomicU32 — incremented per RPC
  • capabilities: Capabilities — device capabilities from <hello>
  • framer: Box<dyn Framer> — selected during hello exchange
  • vendor_profile: Box<dyn VendorProfile> — auto-detected or explicit
  • pending_commit: bool — for CommitUnknown detection

3. Session-Managed Framing Switch

Hello exchange:
  Client sends <hello> with EOM framing (always)
       │
       ▼
  Device responds with <hello> + capabilities
       │
       ▼
  Session parses capabilities:
    Both advertise :base:1.1? → switch to chunked
    Otherwise                 → stay on EOM
       │
       ▼
  Auto-detect vendor from capabilities:
    Junos capability URI? → JunosVendor
    Otherwise             → GenericVendor

4. Vendor Profiles

trait VendorProfile
├── wrap_config()           — add vendor-specific XML wrapping for edit-config
├── unwrap_config()         — strip vendor wrapper from get-config responses
├── normalize_capability()  — normalize legacy/vendor URIs to standard form
└── close_sequence()        — Standard or DiscardThenClose

Built-in:
├── GenericVendor  — passthrough, standard RFC 6241
├── JunosVendor    — auto-detected via http://xml.juniper.net/netconf/junos/1.0
│   Wraps bare config in <configuration>, strips Junos attributes on read,
│   normalizes legacy urn:ietf:params:xml:ns:netconf: URIs,
│   discards uncommitted changes before session close
└── [IosXeVendor]  — planned, deferred until Cisco test device available

5. Layered Error Hierarchy

NetconfError
├── Transport(TransportError)
│   ├── Connect, Auth, Channel, Io, Ssh
├── Framing(FramingError)
│   ├── Invalid, Incomplete, Mismatch (firmware bug detection)
├── Rpc(RpcError)
│   ├── ServerError{7 fields}, Timeout, CommitUnknown, ParseError, MessageIdMismatch
└── Protocol(ProtocolError)
    ├── CapabilityMissing, SessionClosed, HelloFailed, Xml

6. Connection Pool

DevicePool
├── Semaphore(max_connections)  — global concurrency limit
├── devices: HashMap<name, DeviceConfig>
└── connections: HashMap<name, Vec<Client>>  — idle pool

checkout("spine-01") → PoolGuard
  - Acquires semaphore permit (with timeout)
  - Reuses idle connection or creates new one
  - PoolGuard derefs to Client
  - Auto-returns to pool on drop (if healthy)
  - Discard broken connections

7. SSH Authentication

Three methods via russh (pure Rust, no OpenSSL):

  • Password, Key file (Ed25519/RSA/ECDSA), SSH agent

Dependencies

Crate Purpose Why this one
tokio Async runtime Industry standard, required for russh
russh SSH client Pure Rust, no OpenSSL/libssh2 dependency
quick-xml XML parsing Streaming parser, low memory footprint
thiserror Error types Ergonomic derive macros for error enums
tracing Logging Structured, async-aware, composable
futures Async utilities join_all for concurrent pool operations

Testing Strategy

Three layers:

  1. Unit tests — pure logic: framing, RPC serialization, capability parsing, vendor wrapping
  2. Mock Transport — session and vendor integration with canned NETCONF exchanges
  3. vSRX Integration — real Juniper vSRX for end-to-end validation including vendor auto-detection and pool operations