FEATURED · 精选文章

我用 Rust 写了一个 SSH 本地端口转发管理器:Via 的实现与取舍

发布时间 / 2026/8/23 8:53:25
来源 / 创域科博编辑部
栏目 / 资讯中心
我用 Rust 写了一个 SSH 本地端口转发管理器:Via 的实现与取舍 工作中访问内网的数据库、缓存和搜索服务我一直在跳板机后面开 SSH 本地端口转发再用一堆 bash 脚本统一启动这些隧道。脚本越攒越多散在各个目录哪条规则改过、哪个隧道断了全靠记忆和手动重跑。市面上有终端、有 SFTP 工具但没有一个专门用来统一管理和记录 SSH 本地转发的所以我写了 Via——一个 macOS 桌面端的 SSH 本地端口转发管理器。读完这篇文章能判断它适不适合自己的工作流。仓库地址https://github.com/colinrs/viaVia 在解决什么问题SSH 的-L参数能把本机端口经跳板机转发到内网地址Via 做的就是这个只是把它做成了一张可编辑的表格127.0.0.1:3306 → bastion.example.comSSH 会话 → rds.internal:3306一条规则一行本地端口、目标主机、目标端口三个字段加上状态和开关。本机连127.0.0.1:3306就等于连上只有跳板机才够得到的内网服务。规则按会话分组一个跳板机下面挂多条转发数据库、Redis、OpenSearch 各占一行。转发数据链路怎么实现核心逻辑在src-tauri/src/services/forwarder.rs里整个流程只有四步在127.0.0.1:local_port上 bind 一个TcpListener。每 accept 到一个本地连接通过已认证的 SSH 会话open_direct_tcpip打开一条到target_host:target_port的通道。在本端连接和 SSH 通道之间copy_bidirectional双向拷贝字节。字节流就透明地流到了内网服务。关键代码的骨架如下重点看 accept 循环和每个连接的处理方式letlistenerTcpListener::bind((Ipv4Addr::LOCALHOST,rule.local_port)).await?;lettasktokio::spawn(asyncmove{loop{tokio::select!{_task_cancel.cancelled()break,acceptedlistener.accept()matchaccepted{Ok((mutinbound,_)){// 每个本地连接单独开一条 direct-tcpip 通道tokio::spawn(asyncmove{ifletOk(mutoutbound)session.open_direct_tcpip(host,port).await{let_tokio::io::copy_bidirectional(mutinbound,mutoutbound).await;}});}Err(_)break,}}}});这段代码里每个进来的连接都单独 spawn 一个 task各开各的 direct-tcpip 通道互不影响一条规则失败不会影响同一会话里的其他规则。bind 地址固定是Ipv4Addr::LOCALHOST这决定了 Via 的一条安全边界隧道永远只监听回环不会暴露到局域网。bind 失败时把AddrInUse单独映射成ViaError::PortConflict前端就能直接显示「端口被占用」。技术选型前端是 Vue 3 TypeScript Vite配置、加密、SSH、监听、状态和重连这些逻辑都在 RustTauri 2后端。SSH 客户端用 russh本地配置存在单个 SQLite 文件里。对比 Electron选 Tauri 2 是因为后端要长期持有 TCP 监听、SSH 会话和重连调度这些放 Rust 里更直接、内存占用也更低。前端只负责展示和操作所有状态由 Rust 后端通过 Tauri 事件推过来。代码结构里能直接看到这个分工。前端不直接调后端中间隔着createViaStore它把 Tauri 的invoke/listen包成ViaBridge接口测试时换成 mock。前端对后端返回的数据做了防御式校验DTO 字段一变测试会先报错不会把错误状态悄悄塞进界面。凭据怎么存SSH 密码和私钥口令是敏感信息Via 的处理分两层用户设置主密码时用 Argon2 派生包装密钥把数据密钥用 XChaCha20-Poly1305 加密后写进 SQLite。首次初始化生成 10 个恢复码只显示一次忘记主密码时用一个未使用的恢复码重置。用户不设主密码时密码和私钥口令只留在内存不落盘重启后重新输入。导出配置时永远不含密码、私钥口令、私钥内容。导出的 JSON 只有分组、会话的非敏感字段和转发规则可以直接分享给同事导入这是当时最想要的「把配置分享给他人」的能力。主机密钥信任首次连一台跳板机Via 会要求确认主机密钥指纹之后指纹变了就阻断连接。这一步防中间人攻击思路和ssh的known_hosts一样。状态机与自动重连断线重连是最容易出问题的地方。Via 把重连逻辑放在前后两端必须保持同步Rust 后端TunnelManager持有五个 HashMap会话、规则、运行中的 Forwarder、手动停止的规则 id、每条规则的状态。传输层断掉时非手动停止的规则进入reconnecting按2^attempt退避重连退避间隔封顶 60 秒。前端收到reconnecting事件后用同样的退避策略重新调connect_sessionstart_enabled_rules。手动停止被两端都记着后端manually_stopped前端reconnectingSessions所以用户主动停掉的规则不会被自动重连拉起来。pubconstfnreconnect_delay_seconds(attempt:u32)-u64{matchattempt{0..51_u64attempt,// 1, 2, 4, 8, 16, 32 秒_60,}}性能转发数据面用 Criterion 在127.0.0.1回环上测过走的是 Forwarder 的实际数据路径accept 循环 copy_bidirectionalSSH 会话用 mock 实现峰值单向吞吐量 3.08 GiB/s16 MiB 批量中位数数据面往返延迟 32.5 µs1 字节 echo中位数Via 在数据拷贝这一环几乎没有额外开销延迟和吞吐主要受网络和 SSH 限制。benchmark 代码在src-tauri/benches/forwarder.rs跑make bench可以复现。取舍与边界Via 只做 Local Forwarding-RRemote和-DDynamic/SOCKS5都不做终端、SFTP、云端同步、跨平台也都不在 V1 范围。这个取舍的理由是把一个需求做窄才能在统一管理、状态展示和重连这些地方做得够细。V1 先把「管理和记录本地转发」这一件事做好。当前只官方支持 macOSWindows x64 和 Linux x64 会随 CI 构建产物发布但未测试。获取与使用还在用 bash 脚本维护 SSH 转发的可以试试 Via。源码、截图和 benchmark 都在仓库里GitHubhttps://github.com/colinrs/via本地跑起来只要两步makeinstallmakedevmake dev会启动 Vite 开发服务器和 Tauri 桌面窗口。发布产物是 macOS.app本地make package会生成 ad-hoc 签名的包对外分发前再换 Apple Developer ID 重新签名并公证。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻