SSH 卡死修复

问题

最近遇到个挺邪门的事,我在mac上使用 ssh user@192.168.1.22 连接服务器,输入 yes 确认指纹,然后就卡住了。这个问题导致我最近无法执行日常工作,甚至怀疑是单位网络炸了(虽然真的有问题)。

思路

我第一反应是网络问题。ping 了一下,通的。又怀疑服务器挂了,但换了台Windows的机器就连得好好的。那就奇怪了。同一个局域网,其他机器能连就 mac 不能连,总不能是macOS系统的问题吧。倒是也有可能是终端软件的问题,因为我用的是 Ghostty,这个终端会注入 TERM=xterm-ghostty,但我也试过系统的 Terminal.app,还是一样会卡住,所以和终端软件没关系。

于是老老实实上 ssh -v 看日志。输出刷了一大串,最后停在两行:

1
2
debug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic,password
debug1: Next authentication method: publickey

然后,没了。正常情况下一两秒内应该出现 Offering public key...,但我这儿永远等不到下一行。卡在“选好了用公钥”和“真正拿出公钥”之间。

我当时猜是 ssh-agent 的问题。因为 SSH 客户端在这一步会去问 ssh-agent 要私钥,如果 agent 没反应,客户端就会一直等。但我不敢确定,就先试了个绕开的办法:

1
ssh -o IdentityAgent=none -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 user@192.168.1.22

秒连。行,破案了,就是 ssh-agent 的锅。

原因

那这个 ssh-agent 到底是什么呢?

说白了就是个替你拿私钥签名的后台小管家。它是个登录后常驻后台的小进程,专门替你保管私钥。你用 ssh-add 把私钥交给它,之后所有 SSH 连接都找它要签名,省得每次输 passphrase,也不用每个程序自己去读磁盘上的私钥文件。它和 SSH 客户端之间通过一个 Unix socket 通信,路径放在环境变量 SSH_AUTH_SOCK 里。客户端说“我要用某个 key 签个名”,agent 签完把结果还回来。平时你根本感觉不到它存在——直到它挂了。麻烦的地方在于,这是个请求-响应模型,客户端发完请求就等,没有超时重试,也感知不到对面是死是活。agent 一崩,客户端就傻等,表现出来就是“卡住”。

但它为什么炸?我用 launchctl print gui/$(id -u)/com.openssh.ssh-agent 看了一眼,其中:

1
2
runs = 14255
last exit code = 255

一万四千多次启动,每次都退出码 255。也就是说这玩意儿在后台崩溃了一万四千多次,launchd 一直在拉它,它一直在死,死循环。而我每次 SSH 连接,客户端恰好去问这个正在反复去世的进程要密钥,然后就卡住了。

手动跑了一下 /usr/bin/ssh-agent,错误终于说人话了:

1
2
3
agent_cleanup_stale: stat "/Users/x/.ssh/agent/.": Permission denied
unix_listener_tmp: bind "...": Permission denied
main: Couldn't prepare agent socket

~/.ssh/agent 这个目录的权限不对。ssh-agent 启动时要做两件事:进去清理旧的 socket 文件,然后创建一个新的监听 socket。两个操作都被 Permission denied 挡了,它只能退出。至于权限为什么坏了,我猜是之前某次 sudo 操作或者备份恢复的时候被改掉的,因为前两天我确实重置过我mac的 .ssh目录的权限。

修复

修复本身不复杂,就是改所有者、改权限、清掉旧文件、重启服务:

1
2
3
4
sudo chown -R $(id -u):$(id -g) ~/.ssh/agent
chmod 700 ~/.ssh ~/.ssh/agent
rm -f ~/.ssh/agent/*
launchctl kickstart -k gui/$(id -u)/com.openssh.ssh-agent

然后 ssh-add -l 能立刻返回了,再连服务器也正常了。

总结

回头想想,这事坑就坑在它不报错。SSH 客户端不知道 ssh-agent 已经死了,它只是发了个请求,然后等一个永远不会来的回复。你看到的现象是“卡住”,但真正的问题在另一个进程里,而且那个进程已经崩溃了一万多次,只是没人告诉你。

所以如果你也遇到 SSH 输完 yes 就卡死、不报错不超时的情况,别急着怀疑网络和服务器。先试试 ssh -o IdentityAgent=none 能不能连,能连的话,八成就是 ssh-agent 在后台偷偷去世了。launchctl print 看一眼 runs 和 last exit code,基本就水落石出了。

折腾完这一通,我决定以后对 ~/.ssh 目录的权限多留个心眼。这玩意儿平时不起眼,坏了是真能让你怀疑人生。