在 WSL 2 中使用 KeeAgent
更新
已经换成更加全能好用的 https://github.com/buptczq/WinCryptSSHAgent。
莽撞
最近看到 Windows 10 版本 2004 即将发布的新闻,又有网友说其中最大亮点就是 WSL 2 了。我作为 WSL 的重度使用用户不禁心痒痒的想升级试试。
恰好有时间,就急冲冲地加入了 Windows Insider 的 Slow ring,重启完电脑就收到了 2004 的更新。
然后就一边下载更新,一边找文档看 WSL 2 的相关说明,顺手还勾上了“停止获取预览版本”,以免之后无法及时退出😎。在 WSL 2 的相关信息其中有几点值得特别拿出来说一下:
WSL 1 是微软费时费力的在 Windows 上重新实现了一套 Linux API,并翻译到 NT kernel 来运行。(永远怀念天国的 coLinux)
由于这种特殊的实现访问,导致WSL 1 难以提升性能和存在兼容性问题。为此,微软重新设计了 WSL 2 的架构,改成基于微软自家的 Hyper-V 虚拟化技术实现,直接就解决了繁琐的兼容性问题,然后还能享受到现代虚拟化技术的高性能。
不过 WSL 2 也不是没有缺点。因为其使用 VM 实现,从而导致了与 host 之间的交互性还没有 WSL 1 好。比如和 host 的文件交互性能会下降,无法直接访问 localhost 监听的端口了(从 18945 开始可以在 Windows 通过 localhost 访问 WSL 端口)。
因此 WSL 1 也没有被官方声明废弃,1 和 2 不仅可以共存,还能随时切换版本。
然后我还发现,这次的 Windows 大版本更新也是在下载之后直接就安装了,重启之后的安装时间好像变短了,不知道是不是错觉。
悲剧
等待系统安装完成,就开始按照官方文档启用 "Virtual Machine Platform" 组件,然后执行转换命令:
wsl --set-version <Distro> 2
经过大概10多分钟的漫长等待,得到了一个 8G 多的 vhdx 文件。打开微软的暖心终端 Windows Terminal,跑了几个常用的工具,再加上一点点心理作用,确实感受到了性能的大幅提升。然而与性能提升一起出现的,是我的 SSH 连不上了🤦♂️。
挣扎
这里要说一下我的 SSH key 管理方案:我的 SSH key 都存放在 KeePass 里,使用 KeeAgent 插件来提供 SSH agent。这个插件是支持 msysgit/cygwin socket file 的,实质上是开放了一个 TCP 端口,因而可以在 WSL 使用 msysgit2unix-socket.py 脚本通过访问 TCP 端口的形式将 msysgit socket 转换成 unix socket 给 SSH 使用。
首先给 ssh 加上 -v
参数,看到日志中有说明是与 SSH agent 的通讯失败。
debug1: pubkey_prepare: ssh_fetch_identitylist: communication with agent failed
打开 python 脚本一看,很容易就能发现脚本中写死了 localhost 作为 host。然而由于 WSL 2 的架构变化,现阶段还无法在 WSL 2 中使用 localhost 访问 Windows 上监听的端口。
参考官方说明,给脚本加上从 /etc/resolv.conf
中读取 IP 的逻辑。改完保存执行,却发现还是不能正常通讯。
这个时候,我突然意识到之前写的地址是 localhost,而 KeeAgent 从安全角度讲,很有可能只监听了 127.0.0.1
。从 Hyper-V 的虚拟交换机访问过去就不是本机了,应该是因为这个原因而不通的。
于是打开 ProcessExplorer,查看 KeePass 的 TCP/IP 信息,可以看到确实只监听了 127.0.0.1:1121
。
刚好 Windows 自带端口转发功能,用管理员权限打开 PowerShell,执行
netsh interface portproxy add v4tov4 1121 127.0.0.1
再次连接 SSH,发现问题还是没解决。又尝试用 nc 连接 host-ip:1121
, 发现根本就连接不上转发的端口。这个时候,我开始感觉到事情不太对劲了。
先怀疑是防火墙问题,禁用了防火墙,也还是不行。没辙,开始在网上一番搜索,找到了这个 issue:Windows Defender Firewall blocks access from WSL2。原来 Hyper-V 所生成的交换机网络是归属在 Public 下的,然而我之前只禁用了使用中的 Private…
于是把 Public 的防火墙也禁用掉,nc 可以连上了,我不禁感觉到胜利在向我招手。
然而现实是残酷的,SSH 依旧还是连不上。看来要从脚本这边入手了,先注释掉 daemonize 方法,加上日志输出。发现脚本连上端口之后,虽然有发出数据,但就再也没有回复。又用 Wireshark 抓包确认了下,确认在发出数据之后 KeeAgent 端就没有了响应,也没有主动关闭 socket。
于是开始看 msysgit socket 的协议格式 和 KeeAgent 中 MsysSocket .cs 的实现,来分析协议中哪里出现了问题。然后就发现在 msysgit 中有一段 EventWaitHandle 相关的通讯逻辑。但 EventWaitHandle
是 Windows 系统中的实现呀,WSL 中没法使用,之前是怎么正常工作的???cygwin 的协议实现倒是比较简单,只需要校验 socket file 中的 GUID,传一下 pid、uid、gid 就可以了,在 TCP 中即可完成。我此时看到脚本里是用的已经被 Python 3.6 废弃的 asyncore
,不禁产生了自己用 asyncio
写个 cygwin socket 协议的想法,而没再追究为啥之前的 msysgit socket 不能用的问题(留下了坑)。
还好 Python 的标准库实现提供了非常全面和方便的功能,而不像 nodejs 那样基本什么功能都要引个库来实现 (meme)。我很快便完成了修改,跑起来测试,结果还是收不到数据。在连上端口之后,发送完 GUID 便石沉大海,没有任何反馈。
又核对了一下 CygwinSocket.cs 中的逻辑,没发现自己的实现有什么问题。不禁开始想给 KeeAgent 加上日志输出,看看逻辑哪里不对,是不是有什么报错。又跑去看 KeePass 的 plugin 开发文档中有没有什么便捷的调试手段,意识到要用 Visual Studio 加上 KeeAgent 的源码来开发,感觉怪麻烦的。
转念一想,KeePass 是用 .NET 写的,作为一个开源软件,也不会有什么混淆加壳之类的骚操作,我是不是可以动态调试一下?立即打开 dnSpy,直接 attach 上 KeePass,不仅不需要下载源码,都不用加载 pdb 就已经能看到反编译的源码和下断点。
立马在 AcceptConnection 的地方下断点,发现 GUID 的比较就没通过。诧异之际,发现 KeeAgent 中存放的 GUID 和文件中的字节顺序有一点不太一样,是不是 GUID 的字节序和字符串并不一致?研究了一番,原来 GUID 的字节序不是按照 RFC 4122 中定义的 UUID layout 来按顺序摆放,Python 的 uuid 库中虽然也有个 uuid.RESERVED_MICROSOFT 的 variant flag,但并没有办法直接传入使用。
没办法,那就自己手动交换一下字节的位置吧。修改完代码,测试,终于能够连上 SSH 了。
醒悟
在写这篇博客的时候,我不禁又开始思考,为什么之前的 msysgit 脚本能正常工作?
又用 dnSpy 调试了一下 KeePass,发现在 KeeAgent 的实现里,当尝试打开 client 的 EventWaitHandle
时,会报错 WaitHandleCannotBeOpenedException
,然后会 catch 并忽略掉(source),接着走后面的逻辑。
于是我修改了下脚本,改成使用 msysgit socket,去掉发送 GUID 等逻辑,测试确实也能正常工作了。
至于之前为什么自己加完端口转发,再测试的时候没法工作,可能是我错误的访问了 cygwin 的端口?现在已经无从得知了。
折腾
现在整套流程虽然能正常工作了,但还是有未完成的事项。
因为 KeeAgent 监听的端口是随机的,意味着每次启动,端口都可能变化,所以我们需要自动更新端口映射。KeePass 自带的 Trigger 功能就派上了用场。
为此,我又写了个 ForwardUnixSocket 小工具,可以传入 socket file 以将端口暴露在 0.0.0.0
上。它的使用命令如下:
ForwardUnixSocket.exe C:\path\to\socket\file.sock
执行之后 Windows 防火墙会弹出询问对话框,一定注意要勾上“公用网络”,再点击允许,不然 WSL 2 会没法访问。
接下来打开 KeePass 的 Triggers 设置,添加两个 trigger。一个是在程序初始化时运行 ForwardUnixSocket 程序;另一个是在程序退出时执行 taskkill /f /im ForwardUnixSocket.exe
杀掉转发程序,不然 KeePass 会一直等待 ForwardUnixSocket 而无法退出。两个 trigger 的 Window style 也都改成 Hidden,毕竟谁也不想看显示着白字的黑漆漆窗口嘛(
附言
这里附上我修改版本的 socket2unix-socket.py,同时支持 cygwin 和 msysgit 的 socket file 实现。
配合上面的 ForwardUnixSocket 使用时,建议使用 cygwin 格式的 socket file。因为会将端口公开,而 cygwin 至少还有个 GUID 的校验规则,相比而言更安全一点。
完结
撒花🎉🎉🎉
季更博主来看看咸鱼大佬 .