Mac 上 SSH 总是超时断开?用 keepalive 保持连接

要解决 Mac SSH 超时断开,在 ~/.ssh/config 的 Host * 下加上 ServerAliveInterval 60 和 ServerAliveCountMax 3。这样 Mac 的 SSH 客户端会先发一个很小的 keepalive 包,赶在 Wi-Fi 路由器、公司防火墙或远端服务器的空闲计时器关掉连接之前。不过它撑不过睡眠,也撑不过 Wi-Fi 切换。下面帮你判断,你碰到的是四种超时中的哪一种。
在 ~/.ssh/config 里设置全局 SSH keepalive
打开(或新建)~/.ssh/config,加一个 Host * 块,写两条指令:ServerAliveInterval 60 和 ServerAliveCountMax 3。在终端应用里跑 shell 的工具,包括 MXterm Pro,用的都是 PATH 里的 ssh,通常是 macOS 自带的 OpenSSH。
之后客户端每 60 秒向服务器发一个空的“你还在吗”包。连续 3 个(共 180 秒)没有回应,SSH 就干净地关闭连接,不会卡在一个已死的 socket 上。
把 Host * 这个通配块放在文件最底部。OpenSSH 对每个选项采用“先取到的值生效”。如果你已经为单台服务器写了专门的 Host 块(比如跳板机,或带独立设置的工作 VPN 机器),放在它们上面的 Host * 会在读到专门块之前,先把 ServerAliveInterval 占掉。
60 秒间隔加 3 次重试,最多要三分钟才能发现连接已死,作为起点比较合理。如果公司防火墙或公共 Wi-Fi 在一分钟左右就清理空闲连接,改成 ServerAliveInterval 30 和 ServerAliveCountMax 2。这样一分钟而不是三分钟就能发现掉线,代价是每 30 秒多发一个很小的包。
要确认设置生效,让会话空闲的时间超过以前会断开它的那段时间,再看它还在不在。下方 FAQ 里有观察 keepalive 包本身的方法。
macOS 的 SSH 客户端默认已开启 TCPKeepAlive yes,但那是 TCP 层的检查,要发现一个真正死掉的 socket,最久可能要两小时。防火墙或 NAT 设备仍在转发数据包、却认定你的空闲连接已经结束时,它完全不起作用。ServerAliveInterval 工作在 SSH 协议层,所以这个问题要靠它解决。
你碰到的是哪种超时(以及为什么 MacBook 更容易中招)
不是所有 SSH 断开都出于同一个原因,该怎么修,取决于你碰到的是哪一种。
空闲防火墙或 NAT 超时:你和服务器之间的某个设备(公司防火墙、家用路由器、VPN 网关)认为几分钟没有流量的连接已经死了,于是丢掉它的状态。只要 ServerAliveInterval 比该设备的超时窗口短,就能直接解决。
服务器端空闲超时:远端 sshd 被配置为踢掉空闲客户端,或者登录 shell 自带 TMOUT。遇到 sshd 这种情况,本地的 ServerAliveInterval 有帮助,因为在服务器看来,你的客户端不再空闲。
网络切换:切换 Wi-Fi、连上 VPN,或在家和热点之间来回,会在会话中途改变你的 IP。SSH 的 TCP 连接撑不过这种变化,任何 keepalive 设置都解决不了 IP 变化。只有专为此设计的协议(Mosh)可以,代价是牺牲一些终端功能来换取这种韧性。
睡眠:MacBook 独有的一种。合上盖子或 Mac 进入睡眠后,它不再运行你的 SSH 客户端,这段时间里不会发出任何 keepalive 包。唤醒时,服务器往往已经放弃了这条连接,SSH 会报超时或 broken pipe。
如果会话必须撑过合盖,就把远端任务放在 tmux/screen 里跑,这样任务就不在乎本地连接掉没掉(FAQ 里有更多说明)。
有条件的话,服务器端也改一下
如果远端机器归你管,也可以从那一侧防止 SSH 超时断开。在 /etc/ssh/sshd_config 里加上 ClientAliveInterval 60 和 ClientAliveCountMax 3,然后重启 sshd。这是客户端设置在服务器侧的对应项,能阻止服务器成为先挂断你的那一方。
如果服务器不归你管(共享主机、客户的虚拟机、别人管理的云服务器),也不用发愁。你自己 ~/.ssh/config 里的 ServerAliveInterval 独立生效,因为 keepalive 流量由你的客户端发出,远端 sshd 不需要配合。
会话超时时,MXterm Pro 内部发生了什么
MXterm Pro 里的每个终端分屏都是真正的 PTY,运行你平时的登录 shell,基于 Tauri,用 Rust 的 portable-pty 构建。它不是拿浏览器标签页冒充的终端。
你在其中打开的 SSH 连接,用的是 Mac 上本来就有的那个 OpenSSH 客户端。在普通分屏里,ssh 读取 ~/.ssh/config 的方式和在 Terminal.app 或 iTerm2 里完全一样,所以本文的 keepalive 配置原样适用。
从已保存的主机打开的分屏不一样。MXterm Pro 会在命令行上用 -o ServerAliveInterval=30 -o ServerAliveCountMax=3 启动 ssh,所以大约 90 秒就能发现连接已死。OpenSSH 取最先拿到的值,而命令行选项排在配置文件之前。因此你 ~/.ssh/config 里这两个选项的值,对这个分屏不生效。
会话掉线时你看到什么,取决于它是怎么打开的。在普通分屏里自己输入 ssh,退出的是 ssh 这个进程,而不是下面的 shell。你会回到同一个分屏里 Mac 本机的命令提示符,已经打印的内容仍留在屏幕上。重新连接,重敲命令或按 ↑ 即可。
从已保存的主机打开的分屏行为不同。它直接启动 ssh,没有 shell 可退回。会话掉线通常以 255 退出,此时分屏会打印一行自己的暗色提示,以 SSH connection failed or dropped (exit 255) 开头,以 press Enter to reconnect 结尾。按 Enter 重新打开连接。
其他退出码,分屏会打印 Disconnected (exit N),后面跟着 press Enter to reconnect。
SSH Host Manager (⌘⇧H) 帮得上忙的是掉线前后,而不是掉线的那一刻。通过已保存的主机连接,会用上 OpenSSH 自带的连接复用(ControlMaster=auto、ControlPersist=10m)。之后为已连接的主机打开 SFTP 文件面板,会沿用这条连接并跳过握手。共享连接在其上最后一个会话关闭后,还会保持打开 10 分钟。关于已保存的主机,更多内容见 SSH 和 SFTP 文档。
如果你修改已保存主机的连接设置,或删除该主机,共享连接会更早关闭。遇到拒绝登录或强制修改密码的服务器时,文件面板里有重置连接按钮。
管理的服务器不止两三台的话,看一下套餐:默认情况下,Free 套餐在这个管理器里最多保存 5 台 SSH 主机,付费的 Pro 套餐取消了这个上限。
长时间 SSH 会话:MXterm Pro 与 iTerm2、Warp、Termius、MobaXterm 对比
Termius 支持 Mosh,它能撑过 IP 变化和睡眠唤醒,这是普通 SSH 在结构上做不到的。更完整的对比见 Mac 上好用的 SSH 客户端。
MXterm Pro 在同一个工作区里并排运行多个 SSH 会话和 AI 编程 Agent(Claude Code、Codex、Gemini CLI)。在创建工作区页面勾选 worktree 隔离后(默认关闭,仅限 git 仓库),每个 Agent 分屏都有自己的 git worktree,降低冲突概率,即一个 Agent 的改动撞上另一个 Agent 的改动。它不能替代上面的 keepalive 设置,所以不管用哪个终端应用,都要设置。
多分屏 SSH、Command Recipes(Cmd+J)和多 Agent 的 Control Room (原 War Room):先免费用。
常见问题
ServerAliveInterval 是客户端设置,写在你 Mac 自己的 ~/.ssh/config 里,让你的机器去 ping 服务器。ClientAliveInterval 是服务器端的对应项,写在远端机器的 /etc/ssh/sshd_config 里,让服务器去 ping 你的客户端。keepalive 只设置一侧就能生效。两侧都设也无害,不确定是哪一端在超时时反而有用。
应该,凡是要撑过真正断线(而不只是空闲超时)的任务都该这样。ServerAliveInterval 只能防止连接因为看起来空闲而被掐掉。如果 MacBook 睡眠、Wi-Fi 掉线或合上了盖子,它救不了任务。放在远端服务器 tmux 或 screen 里的构建或迁移,不管你本地连接怎样都会继续跑。恢复上线后用 tmux attach 重新接上。
多半是因为 MacBook 睡眠了,而不是 keepalive 失效。睡眠中的 Mac 不运行 SSH 客户端,也就不会发出 keepalive 包。你一唤醒,SSH 就会报告掉线。
最简单的办法是让会话空闲的时间超过以前会断开它的那段时间,确认它还在。想看数据包本身,在第二个窗口运行 sudo tcpdump -i en0 host <server-ip> and port 22。留意即使会话空闲,也每隔 ServerAliveInterval 秒就有小包发出。从已保存的主机打开的分屏则改为每 30 秒发一次。