SSH 连接被秒断?从握手失败到跳板机自动中转的完整排查路径

上周遇到了一个棘手的故障:我的 Mac 无法通过 SSH 连接生产环境的一台服务器 A(端口 20000),系统提示 Connection closed by remote host。奇怪的是,换成通过跳板机 B 去登录,又能正常连接。我用 telnet 测试服务器 A 的端口,结果是通的,防火墙规则也看不出问题,但 SSH 日志里没有任何连接记录。

这种问题最头疼的地方在于,表面上一切正常,连接却死活建不起来。更麻烦的是,日志里什么都没留下,完全不知道请求卡在了哪里。如果你也正在处理类似问题,下面分享的排查思路和方法或许能提供一些参考,它们都来自这次的实战经验。

这次故障的根本原因有些出乎意料,问题不在服务器配置或密钥权限,而是隐藏在网络链路中的一条访问控制策略。下面我将还原整个排查过程,并给出几个可行的解决方案。

确立排查的边界

SSH 连接失败的原因很多,盲目排查只会浪费时间。第一步应该是做对比测试,快速缩小问题范围。

在这个案例里,有三个关键线索:

  1. 本地的 Mac 电脑连不上,但跳板机 B 却能成功登录服务器 A。这说明服务器 A 的 SSH 服务本身运行正常。
  2. 在本地用 telnet A的IP 20000 命令可以连通,说明网络层和传输层没问题,TCP 三次握手也能顺利完成。
  3. 连接在 kex_exchange_identification 这一步被断开。这是 SSH 协议握手的第一个步骤,验证密钥的工作还没开始。

综合这三点,基本可以排除九成以上的常见原因,比如 SSH 服务挂了、端口被防火墙封了,或者密钥配置出错。问题肯定出在连接建立的早期阶段,并且只针对特定的来源 IP。

因此,定位问题边界的关键方法,是找到一个能稳定复现成功的对照组。如果 B 能连接,而你本地连不上,那差异就出在客户端或中间的网络链路上;反过来,要是所有客户端都无法连接,就该去排查服务器。很多时候问题不是修不好,而是排查顺序搞错了,白白浪费了时间。

服务器端排查清单

问题似乎不出在服务器的 SSH 服务上,但还是有必要把配置彻底过一遍,以免遗漏。

1. 防火墙规则检查

在服务器 A 上执行:

bash 复制代码
# 检查 firewalld 放行的端口和富规则
sudo firewall-cmd --list-all

# 检查底层 nftables 是否有针对性拦截
sudo nft list ruleset | grep "你的本地公网IP"

# 如果系统较旧,检查 iptables
sudo iptables -nL | grep "你的本地公网IP"

检查 ports 是否包含 20000/tcp,以及 rich rules 中是否有只允许特定 IP 访问的规则。此案例中,防火墙配置没有问题,20000 端口对全网开放。

2. SSH 服务配置审查

bash 复制代码
# 检查是否限制了登录来源
sudo grep -E "AllowUsers|AllowGroups|DenyUsers|DenyGroups" /etc/ssh/sshd_config

# 实时查看 SSH 认证日志
sudo tail -f /var/log/secure | grep sshd

如果配置文件里有 AllowUsers earic@B的IP 这类限制,来自本地 IP 的访问就会被拒绝。不过,连接若在握手阶段中断,通常不会留下日志,因为尚未进入认证步骤。

3. Fail2ban 封禁列表

bash 复制代码
# 查看 SSH 防护策略中被封禁的 IP
sudo fail2ban-client status sshd

# 如果发现自己 IP 被封,手动解封
sudo fail2ban-client set sshd unbanip 你的本地公网IP

Fail2ban 的拦截规则直接写入底层防火墙(iptables 或 nftables),且优先级很高。这会导致端口表面上看着通畅,但数据包一进来就会被立刻断开。此案例中,Fail2ban 的配置也正常。

客户端环境检查

服务器端排查无误后,再看本地客户端的情况。

1. 清理 known_hosts 缓存

如果服务器 A 重装了系统或换了密钥,本地缓存的旧指纹可能会导致连接异常:

bash 复制代码
# 在本地 Mac 上执行
ssh-keygen -R A服务器的IP
ssh-keygen -R [A服务器的IP]:20000

清理后重新连接,若系统提示输入 yes 确认新指纹,就说明是缓存的问题。但在本例中,命令提示 not found,表明客户端从未成功连接过服务器 A。

2. 私钥权限与格式验证

从 Windows 或 SecureCRT 迁移来的私钥,可能权限设置过大,或换行符格式(CRLF)不对:

bash 复制代码
# 修复私钥文件权限(必须是 600)
chmod 600 ~/.ssh/id_rsa

# 验证私钥格式是否正确
ssh-keygen -y -f ~/.ssh/id_rsa

如果第二条命令能正常输出以 ssh-rsa 开头的公钥字符串,说明私钥格式没问题。如果命令报错,则需要重新生成密钥对,或用 ssh-keygen 工具转换格式。

3. 使用详细日志定位问题

bash 复制代码
# 带三级调试参数连接,观察握手停在哪一步
ssh -vvv -i ~/.ssh/id_rsa -p 20000 root@A服务器的IP

注意观察输出内容,看 debug1: Connection established 后面紧跟着的是什么。如果紧接着是 kex_exchange_identification: Connection closed by remote host,则表明连接在协议握手的第一步就被对方(服务器或中间设备)主动断开。这时,SSH 服务甚至还没开始读取你的公钥。

这种情况基本可以排除密钥、权限和用户名的问题,应将排查重点转向网络链路。

网络链路诊断

既然服务器和客户端的配置都正常,那问题很可能出在从你本地到服务器 A 的网络链路上,有某个设备拦截了 20000 端口的 SSH 流量。

1. 端口探测实验

可以用 nc (netcat) 工具直接探测端口,看看应用层的响应:

bash 复制代码
# 在本地 Mac 上执行
nc -v A服务器的IP 20000

正常情况下,连接成功后服务器会发来 SSH 版本信息(比如 SSH-2.0-OpenSSH_8.7),连接也会一直挂起等待输入。

但这次的输出是:

css 复制代码
Connection to A服务器的IP port 20000 [tcp/*] succeeded!

连接随即退出,没有任何内容。这个现象说明 TCP 握手是成功的(所以才显示 succeeded),但应用层的数据传输被中间设备切断了。

作为对比,再用同样的命令连接跳板机 B 的 20000 端口:

bash 复制代码
nc -v B服务器的IP 20000

这次能看到 SSH-2.0-OpenSSH_8.7 并且连接也保持着。这说明问题确实在网络链路上。

2. 切换网络环境测试

试试把 Mac 断开当前的 Wi-Fi,改用手机的 4G/5G 热点,再重新连 SSH。

如果换了热点问题还在,说明拦截并非来自你的本地网络(比如公司防火墙),而是源于服务器 A 所在的机房、云厂商的安全组,或者是运营商的骨干网。

3. 常见的拦截来源

拦截通常来自两个地方。一个是云厂商的安全组,像阿里云、腾讯云这类平台都有独立于操作系统的硬防火墙,你需要检查服务器 A 的安全组规则,看 20000 端口是不是被设为"仅允许特定 IP 访问"。另一个是 ISP 的深度包检测(DPI),有些宽带运营商会识别并拦截非标准端口(比如 20000)上的加密流量,以阻止用户私搭代理或隧道。

机房级别的访问控制。有些托管机房会在网络边界部署硬件防火墙,只放行白名单 IP 或内网流量。

如果在生产环境也碰到"端口通但 SSH 连不上"的情况,可以按这个顺序排查:先确认服务器配置没问题,然后用nc测试应用层响应,最后通过切换网络环境来找到拦截点。很多所谓的"偶现"问题,其实并非真的随机发生,只是触发它所需的条件还没有全部满足。

跳板机自动中转方案

如果问题出在网络链路上,有几种解决方案。其中一种比较理想且适合生产环境的方法,是利用 OpenSSH 的 ProxyJump 功能,通过跳板机 B 自动中转连接到服务器 A。

为什么这是最优方案

  1. 只需一行命令即可直达目标服务器,不必先登录 B 再登录 A。
  2. 流量全程加密,符合安全合规要求。
  3. 此方法也适用于 scp、rsync、VS Code Remote SSH 等所有基于 SSH 的工具。
  4. 无需修改服务器配置,对服务器 A 和 B 均无侵入。

配置步骤

在本地 Mac 上编辑或创建 SSH 配置文件:

bash 复制代码
nano ~/.ssh/config

粘贴以下配置内容(注意替换 IP 地址和用户名):

css 复制代码
# 配置跳板机 B
Host vps-b
    HostName B机器的IP
    User B机器的用户名
    Port 22
    IdentityFile ~/.ssh/id_rsa

# 配置目标机 A
Host vps-a
    HostName A服务器的IP
    User earic
    Port 20000
    IdentityFile ~/.ssh/id_rsa_for_a
    ProxyJump vps-b

保存后,执行以下命令:

bash 复制代码
ssh vps-a

流量会自动通过 B 机加密中转到服务器 A。

底层原理

ProxyJump 的工作流程如下:

  1. 本地电脑先与 B 建立 SSH 连接。
  2. 然后通过 B 的 SSH 会话,再连接到服务器 A。
  3. 最后,将两段 SSH 隧道串联起来,这个过程对用户透明。

这种方式既能绕过本地到 A 的网络拦截,也符合许多企业的安全规范:生产服务器不直接暴露于公网,只通过堡垒机或跳板机访问。

适用场景

管理多台服务器的团队,最好将跳板机配置标准化。所有人都通过跳板机访问内网机器,可以简化日后的审计工作,还能避免给每台服务器配置公网IP所产生的安全风险。

其他两种方案

如果用不了跳板机,或者 B 机器不受你控制,还有另外两种办法。

方案二:端口转发映射

可以在服务器 A 上用防火墙的端口转发功能,把一个新端口(比如 32222)映射到 20000 端口:

bash 复制代码
# 在 A 上执行
sudo firewall-cmd --zone=public --add-port=32222/tcp --permanent
sudo firewall-cmd --zone=public --add-forward-port=port=32222:proto=tcp:toport=20000 --permanent
sudo firewall-cmd --reload

之后在本地就可以连接这个新端口:

bash 复制代码
ssh -i ~/.ssh/id_rsa -p 32222 earic@A服务器的IP

这样做的好处是不用修改 sshd_config,也不影响B机器。但如果新端口也被拦截(例如 ISP 对所有高位端口都做了 DPI 检测),这个方案就没用了。

方案三:切换回标准端口 22

运营商通常只识别非标准端口上的加密流量,对 22 端口一般会直接放行。可以先通过 B 登录到 A 修改配置:

bash 复制代码
sudo sed -i 's/^Port.*/Port 22/' /etc/ssh/sshd_config
sudo firewall-cmd --zone=public --add-port=22/tcp --permanent
sudo firewall-cmd --reload
sudo systemctl restart sshd

然后在本地重新连接:

bash 复制代码
ssh -i ~/.ssh/id_rsa earic@A服务器的IP

这算是一种彻底的解决办法,但如果有安全合规要求(比如禁止使用标准端口),就不适用了。

方案选择建议

具体怎么选,可以根据情况来定:如果手头有跳板机且要管理多台服务器,用方案一(ProxyJump)最方便。如果只是临时应急,不想改配置,就用方案二(端口转发)。要是确定了是运营商在拦截,而且没有合规限制,那方案三(改回22端口)最直接。

这三种方法各有各的适用场景,选择时需要想清楚代价和边界。一旦选错,不仅解决不了问题,还可能带来新的安全风险或运维负担。

排查清单与监控建议

这类问题的根源,往往不在表面配置,而是在网络链路的访问控制策略中。为防止再次出现,可以参考以下排查和监控措施。

排查清单

  1. 对照组:找一个能稳定连接的客户端作为对照,明确差异点。
  2. 服务器:检查服务器的防火墙(firewalld、iptables、nftables)、SSH 配置(sshd_config)和 Fail2ban 封禁列表。
  3. 客户端:清理 known_hosts 缓存,并验证私钥权限(确保为 600)及格式。
  4. 网络链路 :用 nc -v 测试应用层响应,或切换网络环境来定位拦截点。
  5. 云服务商:检查安全组规则,确认相关端口是否只对特定 IP 开放。

监控建议

  • 在跳板机上启用连接日志,记录用户、时间、来源 IP 及目标服务器。
  • 结合白名单使用 Fail2ban,自动封禁暴力破解行为,同时放行办公网段和跳板机的 IP。
  • 定期审查 SSH 配置,确保 PermitRootLoginPasswordAuthentication 等关键参数符合安全规范。

如果这篇文章说清了问题和思路,欢迎收藏备用。团队里有同事负责服务器运维的话,也请转给他参考。另外,如果你也踩过更诡异的 SSH 连接坑,不妨在评论区分享你的经历,也许正好能帮到其他人。

相关推荐
smallswan2 小时前
《Rust七十二变》开源了
开发语言·后端·rust·ai编程
想要成为糕糕手2 小时前
🚀 NestJS 完全入门指南: Module/Controller/Service讲解 + 实战 CRUD
后端·nestjs
开心就好20252 小时前
appuploader-cli 使用教程:在 Windows 上用命令行把 IPA 上传到 App Store
后端·ios
倾颜2 小时前
Node.js 为什么会慢?从 Event Loop、CPU、内存到并发控制聊性能问题
后端
用户921080262862 小时前
AI 应用平台为什么要拆分 Java 后端和 Python AI 服务
后端
绿智校园2 小时前
一套基座替代五类系统:DeepBasic Folar与传统BA/SCADA/IoT平台的架构对比
物联网·架构
星火10242 小时前
【LangChain4j系列08】Agentic AI 多智能体协作
人工智能·后端
凤山老林2 小时前
零停机数据库演进:Spring Boot 集成 Flyway 与平滑DDL变更策略
数据库·spring boot·后端
步行cgn2 小时前
MyBatis <sql> 标签详解:SQL 片段的定义与复用
后端