伴随着服务器的逐渐增加,想要有效而方便地管理各个服务器的难度也逐渐提高。
现有方案与不足
需要方便的 SSH 客户端
在接触互联网的早年(大约是 2018 年左右),我使用的是网上搜到说还不错的 Xshell 产品(一开始用的是 5 ,后来升级成了 6 )。初期的使用感觉还不错,但后来遇到了它的后门事件,从此就没有再使用过了。
在寻找替代品的过程中,我短暂接触过 SecureCRT 的开心版。虽然它确实可以用,但用开心版的软件总感觉有点愧对于开发者的心血付出。所以仅仅是稍加试用了一小段时间,就没有再继续了。
后来,我在 GitHub 的学生包里发现了 Termius 的 Pro 权益,也因此使用了两年的 Termius 产品。它的一大好处就是跨平台,包括移动设备上——需要救急的话,不需要等电脑启动,直接在手机上就能开启会话。但随着学生包权益的过期,它的一些功能不再免费,我也就不再继续使用了。
再往后,我找到了 PuTTY 。这个软件很强大,并且是开源的,但操作稍微有一些反直觉。以及就是早年我的使用习惯不是很好,一个私钥带着到处跑,导致如何在设备间同步私钥成了一个头疼的问题。可惜的是, PuTTY 的私钥是自己的格式,需要用 PuTTYgen 生成,而不能直接使用 OpenSSH 的 PEM 格式私钥(虽然可以导入转化),对于从其他解决方案里导出的私钥来说,迁移数据也是一大麻烦事。另外就是 PuTTY 的数据是存在注册表里的,这让数据同步或备份变成了一个令人头疼的问题。这也就是为什么虽然我时至今日还是安装着它(偶尔开一次 telnet 什么的非常好用),但对于 SSH 的使用得其实已经不是很多了。
往后的时间里,我使用了 DNS 解析的方案(先是手动管理,后来还用了自动管理方案)帮我记录服务器的解析,我只需要去表格中查询 ID 的开头几个字符就行,剩下的都可以在连接一次之后交给命令行的历史记录自动补全。这样的使用虽说也并不算麻烦,但对于冷启动时候需要手动输入完整的地址也总是感觉有诸多不便。
如何有效地管理服务器
但这些还只是 SSH 连接的客户端。随着服务器的增多(并且分布于各个不同的提供商),如何有效管理它们也逐渐成了一个麻烦的话题。
状态管理倒其实并不麻烦。我有搭建 Grafana 面板,配合 Prometheus 做时序数据库,用 node_export 实时采集各个服务器的状态数据还是很方便的。设置好自定义的告警规则,当服务器状态异常时发送通知,也能及时通知到我。
为了有效地管理备注、资源和开销,有段时间我采用的是 Excel 的方式。这种方式虽然可行,但称不上方便—— Excel 的启动需要时间,而每次启动后看到的都是全部的内容,即使我需要的往往都只是其中的一小部分。
在将数据导入到自托管的 Cryptpad 实例后,我曾短暂地获得一些安宁;但 Cryptpad 似乎不是很喜欢我使用的部署方案,以至于时不时就会出现无法打开的问题;并且随着大萧条的到来缩减开支合并域名, Cryptpad 在迁移域名之后的加密密钥就变化了,导致我使用 SSO 没法登录先前的账号。种种原因之下,我最终又还是趁着旧域名释放之前恢复原始环境抢救出了服务器文档,然后又改回了本地管理。
在使用 Nebula 搭建虚拟网络的初期,我遭遇过很多次把自己锁在网络外面的情况。我需要使用救援手段绕过 SSH 打开到服务器的连接——这意味着我不得不去翻服务器启动时的电子邮件寻找 VNC 的地址和密码。几次折腾之后,我决定把这些内容也加入 Excel 表格;于是这些敏感信息也被合并,伴随着一次次的数据迁移和同步在各个设备上流转。
至于一些同时往服务器上执行多条指令的方案,我的做法是先把它们合并成一个自动化脚本,然后通过粘贴调用这个脚本的指令的方式来执行。很慢,很烦,并且可能会出现各种操作疏漏导致的意想不到的情况,但是能用。
想法成型
在我想法成型的初期,只是想要造一个方便查阅的工具:点击服务器名称来生成一个包含服务器信息的启动 SSH 的命令,再把命令粘贴到系统命令行里来手动调用 OpenSSH 之类的客户端。随着开发流程逐渐的推进,以及最关键的几个问题(例如会话窗口大小)的解决,现在它已经是算是一个初具全流程管理能力的有力帮手了。
品牌设计
名称方面,我依照惯常的命名习惯:因为是面向运维的工具,所以以 Ops 作为一个组成部分;寻找以 s 开头或是 o 结尾的和 Nya Candy 以及扩展意象相关的词,最终选择了 neko ,组合起来就成了 Nekops
。
LOGO 方面,我想到的意象是在机房工作的猫娘女仆,所以就用 Stable Diffusion 配 Q 版的模型随便跑了一张图。又不是拿来商用的项目,谁管我用什么。
选择框架
对于前端框架,因为我在工作中用习惯了 React ,所以就直接继续沿用下去了。对于组件库的选择,因为有接触到朋友推荐的 Mantine ,用了几次感觉非常方便,所以与其苦恼于如何用 TailwindCSS 手搓一个看得下去的样式,就还是选择了这个方案来省去设计的工夫。
让我感到较为苦恼的其实是打包方案的选择。 Electron 很稳定,很强大,一些成熟的工具使用的就是这个方案;但它过于重量级,每一个打包的应用程序都要内置一个 chromium 内核,这会让用户的设备上出现太多的电子垃圾;再瞅瞅如今价格一路高歌猛进的硬盘,我觉得这并不是一件好事。有听说过另一个相对来说也还算知名的解决方案 Tauri ,它能解决 Electron 的臃肿问题,但它底层使用的是 Rust ,而我并不会这种语言。与之相似,也有后端使用 Go 语言的 Wails ,但似乎是因为这是个人主推的项目,导致无论是知名度还是生态的丰富程度都有些不及 Tauri 。
在项目建设的早期,因为主要是前端上的工程,对于后端的需求并不高,所以我选择的是 Tauri 作为开发框架。而当遇到了那些关键问题的时候,我也曾想过是否应该转移到我更熟悉后端语言的 Wails 生态中去,这也正是项目的停滞期( 2024 年 8 月 - 2025 年 4 月 )。而当我最终想到了一个巧妙的办法绕过语言不通的问题后,就还是继续沿着先前的路线开发下去,直到现在。
开发时间线
伴随着这样的想法,我在 2023 年 12 月 1 日(其实应该算是前一天,因为这天是凌晨提交的)正式启动了项目的开发,在 2024 年 1 月 5 日封了第一个版本 0.0.1
,并在 2024 年 8 月 22 日伴随着 0.3.0
的封版将所有的代码以 AGPL-3.0 授权开源。
在这之后,因为遇到了难以解决的关键问题,此时因为这个问题导致软件的缺陷甚至连我自己都接受不了,所以陷入了一段沉寂期,并随着一些小修小补(我认为并不是很值得浪费流水线的时间来构建)都只是内部开发。在 2025 年 4 月 6 日,我突然想到了一个巧妙的方案,顺利解决了这个问题,这就带来了 0.4.0
版本的封版。之后随着各种诸如矩阵模式和国际化功能的加入,开发流程一路顺路推进,更多的新功能不断涌现。
在 0.9.0
,我再次将所有的开发代码开源,并更新了 GitHub 上的构建流水线,使得整套发布与更新流程都能在 GitHub 上完成。此时的 Nekops 虽不能说是一个功能完备的软件,但我已经可以完全信任地将所有的服务器都交由它去管理了。
关键的问题
开发过程中遇到的一个非常关键的问题就是,使用 OpenSSH 启动的会话需要有 tty 环境,使用 Tauri 的 Shell 插件里提供的方法只能加上 “-tt” 参数强制以 pseudo-terminal (pty) 启动;但这样启动的会话无法和 Xterm.js 建立绑定关系,只能以默认大小运行;当执行一些需要全屏幕运行的软件时,或是输入的文本行数过多时,就会出现显示错乱的问题。
我尝试了不少办法试图来解决这个问题。
使用 ANSI escape sequences
\e[8;{rows};{cols}t
这个办法在一个一般的客户端上有效,使用这个办法可以改变远端的会话大小;但在 pty 上它并没有用,消息没法被作为完整字符串传递处理,通过 echo 的方式输入时远端也没有给出任何响应。
使用 stty 命令
stty columns {cols} rows {rows}
这个办法确实能生效,但它输入的是明文命令,这使得它并不适合于自动调整尺寸的场合——如果拖动窗口的边缘改变大小,它会向服务端发送一连串的指令,造成刷屏污染的同时,在非待机模式下(例如正在使用文本编辑工具)还会导致正在处理的工作错乱。
所以我保留了这个办法,并将它作为一个运行非集成客户端时的手动大小调整方案:在使用系统命令行运行指令时,命令行标签页的右键菜单里可以选择它往命令行里输入一行大小设置的指令。
向进程发送
SIGWINCH
信号或使用 ioctl 发送TIOCSWINSZ
命令可惜的是,这些办法在 Windows 上没有用,更别说在其他平台上了。 Windows 并不使用 unix 的信号机制,进程标识符也是句柄而不是 pid ,这使得它没法像 unix 上那样管理。
很可惜的是,这些办法要么无效,要么远非完美。此时的我开始思考是否应该转移封装使用的平台,用我熟悉的 Go 来构建整个客户端,以集成一个 SSH 客户端并向其发送控制指令。
然后某一刻灵光乍现,我想到了 Tauri 的 Shell 插件支持 Sidecar :可以打包一个二进制文件伴随着主应用发布。思路如泉水般涌出:既然 OpenSSH 默认不支持基于指令的大小调整功能,那我写一个支持的再塞进去不就行了?
就这样,俺寻思之力帮助我解决了这个苦恼许久的难题。具体到代码实现上反而其实并不复杂,我这样不太灵活的小脑瓜也能磨出来,稍微多加一点调试也就让它运行起来了。
再往后基本就是一路顺畅,没有再遇到什么大的麻烦就尽情加功能了,偶尔遇到的小疑惑也都没有这样致命,基本稍加思考或查询就能解决。
后记
在我写这篇文章的时候,我已经完成了 Nekops 的初步开发,并整理了一个文档(感觉写得很不好,但反正是写了)以便如果您想要使用的话可以参考。本来还想要建设一个社区来方便访问不了 GitHub 的用户交流,但找来找去没找到合适的解决方案(论坛的话, Discourse 太重, Flarum 需要 PHP 环境不方便容器化管理, NodeBB 的容器在重建之后会丢掉所有的插件数据导致根本不能用于生产环境;即时通讯的话, Discord 是中心化的没法自己部署, Matrix 有点不太稳定),所以暂时就还是用 GitHub 提供的 Discussion 功能了。
实话说,我做这个项目除了想要解决服务器的管理问题以外,也是当作一个玩具项目练练手的,就像我所有以个人名义开发的项目那样。我没法保证它尽善尽美,但我会尽量去优化它,去解决它的问题和不足,让更多有过和我相似经历而因为种种原因没能自己亲自动手的人能获得更多一个可选的解决思路。
相关链接
项目相关
- 代码仓库 https://github.com/Candinya/nekops
- 项目文档 https://docs.nekops.app
- 项目周边 https://github.com/nekops-app
- 集成的 SSH 客户端 https://github.com/nekops-app/pipessh
- 文档仓库 https://github.com/nekops-app/docs
- 项目社区 https://community.nekops.app
参考资料
- 客户端性能比较 https://github.com/Elanis/web-to-desktop-framework-comparison
- 问题相关
- stty 尺寸调整命令 https://unix.stackexchange.com/questions/199737/stty-how-can-i-set-rows-columns-to-its-supported-maximum
- ANSI 控制界面尺寸 https://stackoverflow.com/questions/45775005/xterm-js-ws-ssh2-not-transmitting-terminal-resize-signals-sigwinch-to-ssh
- ioctl 的 TIOCSWINSZ 命令 https://stackoverflow.com/questions/6418678/resize-the-terminal-with-python
- SIGWINCH 信号相关 https://unix.stackexchange.com/questions/207782/how-are-terminal-length-and-width-forwarded-over-ssh-and-telnet