# rneter
[](https://crates.io/crates/rneter)
[](https://docs.rs/rneter)
[](https://opensource.org/licenses/MIT)
[English Documentation](README.md)
`rneter` 是一个用于管理网络设备 SSH 连接的 Rust 库,具有智能状态机处理功能。它提供了高级 API 用于连接网络设备(路由器、交换机等)、执行命令以及管理设备状态,并具备自动提示符检测和模式切换功能。
## 特性
- **连接池管理**:自动缓存和重用 SSH 连接以提高性能
- **状态机管理**:智能设备状态跟踪和自动状态转换
- **提示符检测**:自动识别和处理不同设备类型的提示符
- **模式切换**:在设备模式(用户模式、特权模式、配置模式等)之间无缝转换
- **SFTP 文件上传**:可向开启 SSH `sftp` 子系统的远端主机上传本地文件
- **内置 Copy Flow 模板**:可复用结构化模板来驱动 Cisco-like 设备上的交互式 `copy` 流程
- **最大兼容性**:支持广泛的 SSH 算法,包括用于旧设备的传统协议
- **异步/等待**:基于 Tokio 构建,提供高性能异步操作
- **错误处理**:全面的错误类型with详细上下文信息
## 安装
在你的 `Cargo.toml` 中添加:
```toml
[dependencies]
rneter = "0.3"
```
## 快速开始
```rust
use rneter::session::{ConnectionRequest, ExecutionContext, MANAGER, Command, CmdJob};
use rneter::templates;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 使用预定义的设备模板(例如:Cisco)
let handler = templates::cisco()?;
// 从管理器获取一个连接
let sender = MANAGER
.get_with_context(
ConnectionRequest::new(
"admin".to_string(),
"192.168.1.1".to_string(),
22,
"password".to_string(),
None,
handler,
),
ExecutionContext::default(),
)
.await?;
// 执行命令
let (tx, rx) = tokio::sync::oneshot::channel();
let cmd = CmdJob {
data: Command {
mode: "Enable".to_string(), // Cisco 模板使用 "Enable" 模式
command: "show version".to_string(),
timeout: Some(60),
..Command::default()
},
sys: None,
responder: tx,
};
sender.send(cmd).await?;
let output = rx.await??;
println!("命令执行成功: {}", output.success);
println!("输出: {}", output.content);
Ok(())
}
```
### 安全级别
`rneter` 现在支持安全默认值,并可在连接时自定义 SSH 安全级别:
```rust
use rneter::session::{
ConnectionRequest, ConnectionSecurityOptions, ExecutionContext, MANAGER,
};
use rneter::templates;
// 默认安全模式(known_hosts 校验 + 严格算法)
let _sender = MANAGER
.get_with_context(
ConnectionRequest::new(
"admin".to_string(),
"192.168.1.1".to_string(),
22,
"password".to_string(),
None,
templates::cisco()?,
),
ExecutionContext::default(),
)
.await?;
// 显式指定安全配置
let _sender = MANAGER
.get_with_context(
ConnectionRequest::new(
"admin".to_string(),
"192.168.1.1".to_string(),
22,
"password".to_string(),
None,
templates::cisco()?,
),
ExecutionContext::new()
.with_security_options(ConnectionSecurityOptions::legacy_compatible()),
)
.await?;
```
### 文件上传
如果远端主机启用了 SSH `sftp` 子系统,`rneter` 可以在同一条认证过的 SSH 连接上上传本地文件:
```rust
use rneter::session::{ConnectionRequest, ExecutionContext, FileUploadRequest, MANAGER};
use rneter::templates;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let handler = templates::linux()?;
MANAGER
.upload_file_with_context(
ConnectionRequest::new(
"user".to_string(),
"192.168.1.100".to_string(),
22,
"ssh_password".to_string(),
None,
handler,
),
FileUploadRequest::new(
"./artifacts/config.backup".to_string(),
"/tmp/config.backup".to_string(),
)
.with_timeout_secs(30)
.with_buffer_size(16 * 1024)
.with_progress_reporting(true),
ExecutionContext::default(),
)
.await?;
Ok(())
}
```
这条路径要求远端支持 SFTP。对于只支持 `copy scp:`、`copy tftp:` 这类 CLI 传输命令的网络设备,更适合先通过 `templates` 构建 transfer flow,再交给通用的 command-flow 执行 API。
### 网络设备 SCP/TFTP 传输
对于 Cisco-like CLI,`rneter` 现在提供了一个内置的 copy flow 模板。你只需要填运行时变量,把它渲染成 `CommandFlow`,再交给通用执行入口即可:
```rust
use rneter::session::{ConnectionRequest, ExecutionContext, MANAGER};
use rneter::templates::{self, cisco_like_copy_template, CommandFlowTemplateRuntime};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let flow = cisco_like_copy_template().to_command_flow(
&CommandFlowTemplateRuntime::new()
.with_default_mode("Enable")
.with_vars(json!({
"protocol": "scp",
"direction": "to_device",
"server_addr": "198.51.100.20",
"remote_path": "/pub/image.bin",
"device_path": "flash:/image.bin",
"transfer_username": "deploy",
"transfer_password": "secret",
})),
)?;
let result = MANAGER
.execute_command_flow_with_context(
ConnectionRequest::new(
"admin".to_string(),
"192.168.1.1".to_string(),
22,
"password".to_string(),
None,
templates::cisco()?,
),
flow,
ExecutionContext::default(),
)
.await?;
if let Some(last) = result.outputs.last() {
println!("传输输出: {}", last.content);
}
Ok(())
}
```
这个内置模板适配 `cisco`、`arista`、`chaitin`、`maipu` 和 `venustech` 这类 Cisco-like 提示风格。如果某个厂商的向导文案不同,就继续基于同一套 `CommandFlowTemplate` 自己再定义一个模板即可。
### 结构化命令流模板
如果你希望交互流程不要写死在 Rust 里,可以直接构建一个可复用的
`CommandFlowTemplate`。它保留了之前 TOML 设计里的核心结构:`vars`、`steps`、
`prompts`、`default_mode`,以及条件分支。
```rust
use rneter::templates::{
CommandFlowTemplate, CommandFlowTemplatePrompt, CommandFlowTemplateRuntime,
CommandFlowTemplateStep, CommandFlowTemplateText, CommandFlowTemplateVar,
};
use serde_json::json;
let template = CommandFlowTemplate::new(
"cisco_like_copy",
vec![CommandFlowTemplateStep::new(CommandFlowTemplateText::concat(vec![
CommandFlowTemplateText::literal("copy "),
CommandFlowTemplateText::var("protocol"),
CommandFlowTemplateText::literal(": "),
CommandFlowTemplateText::var("device_path"),
]))
.with_prompts(vec![CommandFlowTemplatePrompt::new(
vec![r"(?i)^Address or name of remote host.*\?\s*$".to_string()],
CommandFlowTemplateText::var("server_addr"),
)
.with_append_newline(true)
.with_record_input(true)])],
)
.with_default_mode("Enable")
.with_vars(vec![
CommandFlowTemplateVar::new("protocol")
.with_label("Transfer Protocol")
.with_description("Transfer protocol used by the device-side copy workflow.")
.with_required(true)
.with_options(["scp", "tftp"]),
CommandFlowTemplateVar::new("server_addr")
.with_label("Server Address")
.with_description("SCP/TFTP server reachable from the target device.")
.with_required(true),
CommandFlowTemplateVar::new("device_path")
.with_label("Device Path")
.with_description("Destination path on the target device.")
.with_required(true),
]);
let flow = template.to_command_flow(
&CommandFlowTemplateRuntime::new()
.with_default_mode("Enable")
.with_vars(json!({
"protocol": "scp",
"direction": "to_device",
"server_addr": "198.51.100.20",
"remote_path": "/pub/image.bin",
"device_path": "flash:/image.bin",
"transfer_username": "deploy",
"transfer_password": "secret",
})),
)?;
```
现在内置的 `cisco_like_copy_template()` 也是走这套结构化模板抽象,所以后面无论是
`http`、`ftp`,还是厂商自定义 copy 向导,都可以优先沉淀成同一套模板层,而不是继续往底层结构里塞特例字段。
### 自定义交互命令流程
如果设备上的流程需要多条命令,或者 prompt 文案并没有内置在模板里,可以直接构建 `CommandFlow`,并给每一步挂运行时 `PromptResponseRule`:
```rust
use rneter::session::{
Command, CommandFlow, CommandInteraction, ConnectionRequest, ExecutionContext, MANAGER,
PromptResponseRule,
};
use rneter::templates;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let flow = CommandFlow::new(vec![Command {
mode: "Enable".to_string(),
command: "copy http: flash:/image.bin".to_string(),
timeout: Some(600),
interaction: CommandInteraction::default()
.push_prompt(PromptResponseRule::new(
vec![r"(?i)^Address or name of remote host.*\?\s*$".to_string()],
"203.0.113.10\n".to_string(),
))
.push_prompt(PromptResponseRule::new(
vec![r"(?i)^Source (?:file ?name|filename).*\?\s*$".to_string()],
"/pub/image.bin\n".to_string(),
))
.push_prompt(
PromptResponseRule::new(
vec![r"(?i)^Destination (?:file ?name|filename).*\?\s*$".to_string()],
"\n".to_string(),
)
.with_record_input(true),
),
..Command::default()
}]);
let result = MANAGER
.execute_command_flow_with_context(
ConnectionRequest::new(
"admin".to_string(),
"192.168.1.1".to_string(),
22,
"password".to_string(),
None,
templates::cisco()?,
),
flow,
ExecutionContext::default(),
)
.await?;
if let Some(last) = result.outputs.last() {
println!("最后一步输出: {}", last.content);
}
Ok(())
}
```
运行时 prompt-response 规则会优先于模板里的静态输入规则生效,所以后续新增 `scp`、`tftp`、`http` 这类向导式 CLI 交互时,通常不需要再改底层模板定义。
### 会话录制与回放
```rust
use rneter::session::{
ConnectionRequest, ExecutionContext, MANAGER, SessionRecordLevel, SessionReplayer,
};
use rneter::templates;
let (sender, recorder) = MANAGER
.get_with_recording_level_and_context(
ConnectionRequest::new(
"admin".to_string(),
"192.168.1.1".to_string(),
22,
"password".to_string(),
None,
templates::cisco()?,
),
ExecutionContext::default(),
SessionRecordLevel::Full,
)
.await?;
// 实时订阅后续录制事件
let mut rx = recorder.subscribe();
tokio::spawn(async move {
while let Ok(entry) = rx.recv().await {
println!("实时事件: {:?}", entry.event);
}
});
// 或者仅记录关键事件(不记录原始 shell 分块)
let (_sender2, _recorder2) = MANAGER
.get_with_recording_level_and_context(
ConnectionRequest::new(
"admin".to_string(),
"192.168.1.1".to_string(),
22,
"password".to_string(),
None,
templates::cisco()?,
),
ExecutionContext::default(),
SessionRecordLevel::KeyEventsOnly,
)
.await?;
// ...通过 `sender` 发送 CmdJob...
// 导出为 JSONL
let jsonl = recorder.to_jsonl()?;
// 恢复并离线回放
let restored = rneter::session::SessionRecorder::from_jsonl(&jsonl)?;
let mut replayer = SessionReplayer::from_recorder(&restored);
let replayed_output = replayer.replay_next("show version")?;
println!("回放输出: {}", replayed_output.content);
// 无需真实 SSH 的离线命令流程测试
let script = vec![
rneter::session::Command {
mode: "Enable".to_string(),
command: "terminal length 0".to_string(),
timeout: None,
..rneter::session::Command::default()
},
rneter::session::Command {
mode: "Enable".to_string(),
command: "show version".to_string(),
timeout: None,
..rneter::session::Command::default()
},
];
let outputs = replayer.replay_script(&script)?;
assert_eq!(outputs.len(), 2);
```
### 事务化命令块下发
对于配置命令,可以按“块”执行并实现失败补偿回滚:
```rust
use rneter::session::{
Command, CommandBlockKind, CommandFlow, ConnectionRequest, ExecutionContext, MANAGER,
RollbackPolicy, SessionOperation, TxBlock, TxStep,
};
use rneter::templates::{self, cisco_like_copy_template, CommandFlowTemplateRuntime};
let block = TxBlock {
name: "addr-create".to_string(),
kind: CommandBlockKind::Config,
rollback_policy: RollbackPolicy::WholeResource {
rollback: Box::new(
Command {
mode: "Config".to_string(),
command: "no object network WEB01".to_string(),
timeout: Some(30),
..Command::default()
}
.into(),
),
trigger_step_index: 0,
},
steps: vec![
TxStep::new(Command {
mode: "Config".to_string(),
command: "object network WEB01".to_string(),
timeout: Some(30),
..Command::default()
}),
TxStep::new(CommandFlow::new(vec![
Command {
mode: "Config".to_string(),
command: "host 10.0.0.10".to_string(),
timeout: Some(30),
..Command::default()
},
Command {
mode: "Config".to_string(),
command: "description WEB01".to_string(),
timeout: Some(30),
..Command::default()
},
])),
],
fail_fast: true,
};
let result = MANAGER
.execute_tx_block_with_context(
ConnectionRequest::new(
"admin".to_string(),
"192.168.1.1".to_string(),
22,
"password".to_string(),
None,
templates::cisco()?,
),
block,
ExecutionContext::default(),
)
.await?;
println!(
"committed={}, rollback_succeeded={}",
result.committed, result.rollback_succeeded
);
```
现在 `TxStep::new(...)` 接受的是任意 `SessionOperation`,所以 workflow 里的一个步骤既可以是
单条命令,也可以是多步 `CommandFlow`,或者一个可复用的模板调用:
```rust
let copy_step = TxStep::new(SessionOperation::template(
cisco_like_copy_template(),
CommandFlowTemplateRuntime::new().with_vars(serde_json::json!({
"protocol": "scp",
"direction": "to_device",
"server_addr": "192.168.1.100",
"remote_path": "/srv/images/fw.bin",
"device_path": "flash:/fw.bin",
"transfer_username": "deploy",
"transfer_password": "secret",
})),
));
let summary = copy_step.run.summary()?;
println!(
"kind={} mode={} steps={} desc={}",
summary.kind, summary.mode, summary.step_count, summary.description
);
```
对于“地址对象 -> 服务对象 -> 策略”这类多块统一成败场景,可使用 workflow:
```rust
use rneter::session::{TxWorkflow, TxWorkflowResult};
let workflow = TxWorkflow {
name: "fw-policy-publish".to_string(),
blocks: vec![addr_block, svc_block, policy_block],
fail_fast: true,
};
let workflow_result: TxWorkflowResult = MANAGER
.execute_tx_workflow_with_context(
ConnectionRequest::new(
"admin".to_string(),
"192.168.1.1".to_string(),
22,
"password".to_string(),
None,
templates::cisco()?,
),
workflow,
ExecutionContext::default(),
)
.await?;
for block in &workflow_result.block_results {
for step in &block.step_results {
println!(
"step[{}] op={} execution={:?} rollback={:?}",
step.step_index,
step.operation_summary,
step.execution_state,
step.rollback_state
);
for child in &step.forward_operation_steps {
println!(
" forward_step[{}] op={} success={}",
child.step_index, child.operation_summary, child.success
);
}
for child in &step.rollback_operation_steps {
println!(
" rollback_step[{}] op={} success={}",
child.step_index, child.operation_summary, child.success
);
}
}
if let Some(block_rollback) = &block.block_rollback_operation_summary {
println!("block_rollback={block_rollback}");
for child in &block.block_rollback_steps {
println!(
" block_rollback_step[{}] op={} success={}",
child.step_index, child.operation_summary, child.success
);
}
}
}
```
也可以直接用模板策略自动构建事务块:
```rust
let cmds = vec![
"object network WEB01".to_string(),
"host 10.0.0.10".to_string(),
];
let block = templates::build_tx_block(
"cisco",
"addr-create",
"Config",
&cmds,
Some(30),
Some("no object network WEB01".to_string()), // 整体回滚
)?;
```
对于 CI 的离线测试,可以将 JSONL 录制文件放在 `tests/fixtures/` 下,
并在集成测试中回放(参考 `tests/replay_fixtures.rs`)。
将线上录制归一化为稳定 fixture:
```bash
cargo run --example normalize_fixture -- raw_session.jsonl tests/fixtures/session_new.jsonl
```
### 模板与状态机生态
你可以把内置模板当作注册表管理,并直接对状态图做诊断:
```rust
use rneter::templates;
let names = templates::available_templates();
assert!(names.contains(&"cisco"));
let _handler = templates::by_name("juniper")?; // 大小写不敏感
let report = templates::diagnose_template("cisco")?;
println!("是否存在问题: {}", report.has_issues());
println!("死路状态: {:?}", report.dead_end_states);
let catalog = templates::template_catalog();
println!("模板数量: {}", catalog.len());
let all_json = templates::diagnose_all_templates_json()?;
println!("全部诊断 JSON 字节数: {}", all_json.len());
```
也可以先导出内置模板配置,再按需扩展后重新构建:
```rust
use rneter::device::prompt_rule;
use rneter::templates;
let mut config = templates::by_name_config("cisco")?;
config
.prompt
.push(prompt_rule("CustomMode", &[r"^custom>\s*$"]));
let handler = config.build()?;
如果目标 Linux 主机登录 shell 是 `fish`,可以显式指定 shell 类型:
```rust
use rneter::device::DeviceShellFlavor;
use rneter::templates::{linux_with_config, LinuxTemplateConfig};
let handler = linux_with_config(LinuxTemplateConfig {
shell_flavor: DeviceShellFlavor::Fish,
..LinuxTemplateConfig::default()
})?;
```
新增的录制/回放能力:
- Prompt 前后态:每条 `command_output` 都记录 `prompt_before`/`prompt_after`
- 状态机 prompt 前后态:事件可记录 `fsm_prompt_before`/`fsm_prompt_after`
- 返回值带 prompt:命令执行与离线回放的 `Output` 现在包含 `prompt`
- 事务生命周期事件:`tx_block_started`、`tx_step_succeeded`、`tx_step_failed`、`tx_rollback_started`、`tx_rollback_step_succeeded`、`tx_rollback_step_failed`、`tx_block_finished`
- 兼容旧 schema:历史 `connection_established` 的 `prompt`/`state` 字段仍可读取
- fixture 测试工作流:`tests/fixtures/` 提供成功流/失败流/状态切换样本,`tests/replay_fixtures.rs` 提供快照与质量校验
`command_output` 事件结构示例:
```json
{
"kind": "command_output",
"command": "show version",
"mode": "Enable",
"prompt_before": "router#",
"prompt_after": "router#",
"fsm_prompt_before": "enable",
"fsm_prompt_after": "enable",
"success": true,
"content": "Version 1.0",
"all": "show version\nVersion 1.0\nrouter#"
}
```
事务生命周期事件示例:
```json
{
"kind": "tx_block_finished",
"block_name": "addr-create",
"committed": false,
"rollback_attempted": true,
"rollback_succeeded": true
}
```
## 架构
### 连接管理
`SshConnectionManager` 提供了通过 `MANAGER` 常量访问的单例连接池。它可以自动:
- 缓存连接 5 分钟的不活动时间
- 在连接失败时重新连接
- 管理最多 100 个并发连接
### 状态机
`DeviceHandler` 实现了一个有限状态机:
- 使用正则表达式模式跟踪当前设备状态
- 使用 BFS 算法查找状态之间的最优路径
- 处理自动状态转换
- 支持特定系统状态(例如不同的 VRF 或上下文)
#### 设计思路
这个状态机的设计基于网络设备自动化里的两个稳定事实:
1. 相比命令文本,Prompt 更适合判断当前模式。
2. 不同厂商/型号的模式切换路径不同,路径搜索必须数据驱动。
核心设计选择:
- 状态统一小写,并将 prompt 正则匹配结果映射到状态索引,保证快速定位。
- 将 prompt 检测(`read_prompt`)与状态更新(`read`)拆开,保证命令循环行为可预测。
- 将状态转换建模为有向图(`edges`),通过 BFS 找到最短可行切换路径。
- 将动态输入处理(`read_need_write`)与命令逻辑解耦,复用密码/确认类交互处理。
- 同时记录 CLI prompt 文本与 FSM prompt(状态名),便于在线诊断和离线回放断言。
这样设计的好处:
- 可移植性更好:设备差异主要通过配置表达,而不是硬编码分支。
- 稳定性更好:执行依赖 prompt/状态收敛,而不是脆弱的输出格式假设。
- 可测试性更好:可通过 record/replay 离线验证状态切换与 prompt 演化,不依赖真实 SSH。
#### 状态转换模型
```mermaid
flowchart LR
O["Output"] --> L["Login Prompt"]
L -->|enable| E["Enable Prompt"]
E -->|configure terminal| C["Config Prompt"]
C -->|exit| E
E -->|exit| L
E -->|show ...| E
C -->|show ... / set ...| C
```
#### 命令执行流程(带状态感知)
```mermaid
flowchart TD
A["接收命令(mode, command, timeout)"] --> B["读取当前 FSM prompt/state"]
B --> C["BFS 规划切换路径: trans_state_write(target_mode)"]
C --> D["按顺序执行切换命令"]
D --> E["执行目标命令"]
E --> F["读取流式输出 -> handler.read(line) 更新状态"]
F --> G{"匹配到 prompt?"}
G -->|否| F
G -->|是| H["构建 Output(success, content, all, prompt)"]
H --> I["记录事件: prompt_before/after + fsm_prompt_before/after"]
```
### 命令执行
命令通过基于异步通道的架构执行:
1. 向连接发送器提交一个 `CmdJob`
2. 库会在需要时自动转换到目标状态
3. 执行命令并等待提示符
4. 返回带有成功状态的输出
## 支持的设备类型
该库旨在与任何支持 SSH 的网络设备配合使用。特别适合:
- Cisco IOS/IOS-XE/IOS-XR 设备
- Juniper JunOS 设备
- Arista EOS 设备
- 华为 VRP 设备
- 通过 SSH 访问的通用 Linux/Unix 系统
## 配置
### SSH 算法支持
`rneter` 在 `config` 模块中包含全面的 SSH 算法支持:
- 密钥交换:Curve25519、DH 组、ECDH
- 加密:AES(CTR/CBC/GCM)、ChaCha20-Poly1305
- MAC:HMAC-SHA1/256/512 及 ETM 变体
- 主机密钥:Ed25519、ECDSA、RSA、DSA(用于旧设备)
这确保了与现代和传统网络设备的最大兼容性。
## 错误处理
该库通过 `ConnectError` 提供详细的错误类型:
- `UnreachableState`:无法从当前状态到达目标状态
- `TargetStateNotExistError`:请求的状态在配置中不存在
- `ChannelDisconnectError`:SSH 通道意外断开
- `ExecTimeout`:命令执行超时
- 等等...
对于 `execute_operation_with_context(...)` 这类 operation 级 API,失败时现在会返回
`SessionOperationExecutionError`,可通过 `partial_output()` 读取失败前已完成的子步骤结果。
## 文档
详细的 API 文档请访问 [docs.rs/rneter](https://docs.rs/rneter)。
## 许可证
本项目采用 MIT 许可证 - 详情请参阅 [LICENSE](LICENSE) 文件。
## 贡献
欢迎贡献!请随时提交 Pull Request。
## 作者
demohiiiii