rustnetconf 0.8.0

An async-first NETCONF 1.0/1.1 client library for Rust
Documentation
# 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