最近遇到了一次很典型的 SSH 网络故障:

同一台公网服务器,在手机热点下可以正常 SSH,但在校园网环境下,无论怎么尝试都连不上。

最开始看到的现象非常容易让人误判:

  • ping 正常,延迟约 25 ms;
  • ssh -v 卡在 Connecting to ... port 22;
  • nc 探测 22 端口同样没有立即返回结果;
  • 服务器上的 sshd 正常运行;
  • UFW、iptables、云安全组都确认放行;
  • 换成手机热点后,SSH 立即恢复正常。

最终定位到:问题不在服务器,而是在校园网出口侧对 TCP 22 连接进行了过滤。

这篇文章记录整个排查过程,也整理一下几个容易混淆的网络诊断点。


一、故障现象

目标服务器:

1
103.36.221.122

在校园网环境下执行:

1
ssh -v -p 22 user@103.36.221.122

日志停在:

1
debug1: Connecting to 103.36.221.122 [103.36.221.122] port 22.

之后没有任何进一步输出。

与此同时:

1
ping 103.36.221.122

完全正常:

1
2
3
4
5
6
64 bytes from 103.36.221.122: icmp_seq=1 ttl=52 time=27.9 ms
64 bytes from 103.36.221.122: icmp_seq=2 ttl=52 time=26.2 ms
...

27 packets transmitted, 27 received, 0% packet loss
rtt min/avg/max/mdev = 24.670/27.461/37.792/3.666 ms

看起来网络没有问题。

于是很容易产生一个疑问:

既然服务器能 ping 通,为什么 SSH 反而连不上?

关键就在这里:ping 和 SSH 根本不是同一种网络测试。

ping 使用的是 ICMP,而 SSH 建立在 TCP 之上。

所以:

1
ICMP 正常 ≠ TCP 正常

更不能进一步推出:

1
ping 正常 ≠ SSH 端口一定可达

二、第一步:确认 IP 层连通性

先测试最基础的 IP 层:

1
ping 103.36.221.122

结果正常,说明至少可以确认:

  1. 目标主机能够对 ICMP Echo Request 做出响应;
  2. 基本 IP 路由是可达的;
  3. 当前链路不存在明显的整体断网问题。

但这里能得到的结论其实非常有限。

它只能说明:

ICMP 这条路径是通的。

至于 TCP 22 是否能够建立连接,还需要单独测试。


三、第二步:直接测试 TCP 22

使用 nc:

1
nc -zv 103.36.221.122 22

结果没有立即返回:

1
# 一直等待

这个现象比 ping 更有价值,因为现在测试的已经是目标 TCP 端口。

通常可以把 TCP 连接失败粗略分成几种典型情况:

现象 常见含义
succeeded/open TCP 三次握手成功,目标端口可以建立连接
Connection refused 收到了 RST,目标端口没有对应服务监听,或被主机主动拒绝
长时间等待后超时 在规定时间内没有得到有效响应,常见原因包括中间设备丢包、过滤或路径问题

这里最值得注意的是第三种。

“一直卡住”本身就是一个诊断信号。

它至少说明:

TCP 连接请求没有在预期时间内完成。

一种非常常见的情况,就是防火墙直接丢弃了 SYN 包,因此客户端只能不断等待,直到 TCP 超时。

但要注意,严格来说:

仅凭客户端卡住,不能百分之百断定就是某一台设备执行了 DROP。

也可能存在其他路径异常,因此接下来还需要继续排除服务器侧。


四、第三步:检查服务器上的 SSH 服务

这一步的目的很简单:

先证明服务器自己真的在正常提供 SSH。

通过服务器控制台、VNC、串口或者云厂商网页终端登录服务器。

1. 检查 sshd 是否监听 22

1
sudo ss -tlnp | grep ssh

输出:

1
2
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=22310,fd=3))
LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=22310,fd=4))

这里可以看到:

1
2
0.0.0.0:22
[::]:22

说明 SSH 服务不仅监听在 127.0.0.1,而是监听在 IPv4 / IPv6 的非回环地址上,可以接受外部连接。

这一步可以排除最基础的一类问题:

1
sshd 根本没启动

2. 检查服务器防火墙

如果服务器使用 UFW:

1
sudo ufw status

例如:

1
2
3
4
5
6
7
8
9
Status: active

To Action From
-- ------ ----
22/tcp ALLOW Anywhere
23333/tcp ALLOW Anywhere
24444/tcp ALLOW Anywhere
25565/tcp ALLOW Anywhere
22/tcp (v6) ALLOW Anywhere (v6)

可以看到 22/tcp 已经明确放行。

进一步检查底层 iptables 规则:

1
sudo iptables -L -n

其中可以看到类似:

1
ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22

这进一步说明服务器本机没有明显的 22 端口拦截规则。


3. 直接在服务器本机测试

1
nc -zv 127.0.0.1 22

结果:

1
Connection to 127.0.0.1 22 port [tcp/ssh] succeeded!

说明:

  • sshd 在运行;
  • SSH 服务正在监听;
  • 本机 TCP 连接可以建立;
  • 服务器本地配置基本正常。

到这里,服务器本身的问题已经被排除了一大部分。


五、第四步:检查云安全组

服务器本机正常,还不能马上断定网络没问题。

因为在很多云服务器环境中,请求实际上还需要经过云平台的安全组。

因此登录云厂商控制台,检查入站规则。

本次排查中:

  • 22/tcp 已放行;
  • 为了排除规则冲突,甚至临时放宽了安全组;
  • 修改后重新测试,问题仍然存在。

因此基本可以排除:

1
云安全组拦截

此时已经得到一个很有价值的结论:

1
2
3
4
5
6
7
8
9
客户端
↓
???
↓
云平台安全组
↓
服务器防火墙
↓
sshd

服务器这一侧从安全组到 sshd 基本全部正常。

那么问题就越来越像出在:

1
客户端 → 校园网出口 → Internet

这一段。


六、第五步:最关键的对照实验——换网络

网络问题里有一个非常有效的方法:

不要一直猜,直接换一个网络环境。

于是切换到手机热点,再执行:

1
ssh -p 22 user@103.36.221.122

结果:

1
成功连接。

这一结果非常关键。

因为目标服务器没有变化:

1
2
3
4
服务器没变
IP 没变
SSH 配置没变
云安全组没变

唯一发生变化的是:

1
校园网 → 手机热点

因此可以把问题范围迅速缩小到:

校园网出口路径。

结合之前的现象:

1
2
3
4
5
6
7
校园网
ping → 正常
TCP 22 → 无法建立

手机热点
ping → 正常
TCP 22 → 正常

最合理的解释就是:

校园网出口侧对外发起的 TCP 22 连接存在过滤或丢弃策略。

这也解释了最开始那个看起来很奇怪的现象:

1
2
ping 能通
SSH 却不能用

因为 ICMP 和 TCP 是完全不同的协议,网络设备可以对它们采用不同的策略。


七、问题到底发生在哪里?

把整个排查过程串起来,就会得到这样一条链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
┌──────────────┐
│ 本地电脑 │
└──────┬───────┘
│
│ ICMP
│ ───────────────→ 正常
│
│ TCP :22
│ ───────────────→ 无法建立
│
┌──────▼───────┐
│ 校园网出口 │
│ 可能存在过滤│
└──────┬───────┘
│
▼
Internet
│
▼
┌──────────────┐
│ 云安全组 │ 已确认放行
└──────┬───────┘
│
▼
┌──────────────┐
│ 服务器防火墙 │ 已确认放行
└──────┬───────┘
│
▼
┌──────────────┐
│ sshd │ 正常监听
└──────────────┘

这里需要强调一个措辞区别:

我们可以根据实验结果判断:

问题位于校园网这一侧的出口路径。

至于校园网具体是通过 ACL、端口过滤、防火墙策略还是其他网络设备实现拦截,仅凭这些测试结果无法进一步确定。

如果一定要确认具体是哪一台设备、哪条规则,可以继续使用 traceroute、tcpdump 等工具做更底层的抓包与路径分析。


八、最终解决方案:给 SSH 换一个端口

既然确认问题是:

1
TCP 22 无法从校园网正常连接

最直接的方法就是:

不要再使用 TCP 22。

可以给 SSH 增加一个其他的监听端口,例如:

1
51832

这里真正重要的不是“51832”这个数字,而是:

  1. 端口没有被其他服务占用;
  2. 服务端防火墙允许该端口;
  3. 云安全组允许该端口;
  4. 当前网络环境允许访问该端口。

通常选择一个较高且没有实际业务占用的端口即可。

需要特别说明:

49152–65535 属于 IANA 定义的动态/私有端口范围,并不意味着这些端口天然“更安全”或者“一定不会被占用”。

因此,检查端口是否真正空闲,比单纯选择一个大数字更加重要。


1. 先备份 SSH 配置

1
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak

这是一个很值得养成的习惯。

服务器上的网络配置尤其如此,因为 SSH 一旦配置错误,可能直接把自己锁在服务器外面。


2. 修改 sshd 配置

编辑:

1
sudo nano /etc/ssh/sshd_config

增加:

1
Port 51832

这次不要急着删除原来的:

1
Port 22

而是暂时保留:

1
2
Port 22
Port 51832

这样做的目的非常简单:

先让新端口和旧端口同时工作,确认新连接正常之后,再决定是否关闭 22。


3. 修改后先检查语法

不要直接重启。

先执行:

1
sudo sshd -t

如果没有任何输出,通常表示配置语法检查通过。

如果出现报错,先修复配置,不要继续重启 SSH 服务。


4. 放行防火墙

如果使用 UFW:

1
sudo ufw allow 51832/tcp

这一步很容易被忘记。

sshd 监听新端口,并不代表防火墙一定允许这个端口。


5. 重启 SSH

不同发行版的服务名称可能不同,例如有的系统使用 ssh,有的环境会使用 sshd。

例如:

1
sudo systemctl restart ssh

或者:

1
sudo systemctl restart sshd

以当前系统实际存在的服务名为准。


6. 确认新端口真的在监听

1
sudo ss -tlnp | grep 51832

例如看到:

1
LISTEN 0 128 0.0.0.0:51832 0.0.0.0:* users:(("sshd",pid=...,fd=...))

说明 sshd 已经成功监听新端口。


7. 从校园网重新测试

1
ssh -p 51832 user@103.36.221.122

这次连接成功进入认证阶段,说明 TCP 连接已经建立。

至此,问题得到解决。


九、过程中踩到的几个坑

这次排查过程中,还有几个非常典型的问题值得单独记录。

坑 1:防火墙放行 ≠ 端口空闲

一开始曾经考虑使用:

1
23333

因为服务器的 UFW 中已经允许这个端口。

但实际测试时:

1
2
kex_exchange_identification: Connection closed by remote host
Connection closed by 103.36.221.122 port 23333

进一步检查:

1
sudo ss -tlnp | grep -E '23333|24444|25565'

发现:

1
2
3
LISTEN 0 4096 0.0.0.0:25565 0.0.0.0:* users:(("java",pid=348140,fd=68))
LISTEN 0 511 *:24444 *:* users:(("node",pid=19234,fd=18))
LISTEN 0 511 *:23333 *:* users:(("node",pid=19273,fd=19))

也就是说:

1
2
3
23333 → Node 服务
24444 → Node 服务
25565 → Java 服务

这些端口根本不是 SSH。

这也解释了为什么会出现:

1
Connection closed by remote host

TCP 连接可能已经建立,但连接到该端口的实际上是另一个应用,而不是 sshd。

因此选端口时一定要检查:

1
sudo ss -tlnp | grep <port>

“防火墙允许这个端口”与“这个端口可以拿来运行 SSH”是两件完全不同的事情。


坑 2:不要把 Connection closed、refused 和 timeout 混为一谈

这几个错误背后的网络阶段并不一样。

Connection refused

例如:

1
ssh: connect to host 103.36.221.122 port 22: Connection refused

通常意味着 TCP 层已经收到对方的拒绝响应,例如 RST。

常见原因:

  • 端口没有服务监听;
  • 主机侧规则主动拒绝连接。

Connection closed by remote host

例如:

1
kex_exchange_identification: Connection closed by remote host

这意味着 TCP 连接至少已经进入建立后的阶段,然后连接被关闭。

如果目标端口实际上运行着 Node、Java 等其他服务,就可能出现这种情况。


长时间等待后超时

例如:

1
Connection timed out

说明客户端一直没有得到足够的响应来完成连接。

常见原因包括:

  • 防火墙丢弃数据包;
  • 中间设备进行过滤;
  • 路由异常;
  • 目标侧无响应。

所以:

1
2
3
refused
closed
timeout

并不是三个“差不多”的错误,它们对应的是不同阶段的问题。


十、另一个容易误判的地方:nc 卡住到底说明什么?

之前我一度把:

1
nc -zv 103.36.221.122 22

卡住直接理解成:

1
DROP

严格来说,这个结论太绝对了。

更加严谨的判断应该是:

TCP 连接在指定时间内没有完成,也没有立即收到拒绝响应。

其中一种典型原因确实是:

1
SYN → 防火墙直接 DROP → 客户端一直等待 → 最终 timeout

但还可能存在其他原因。

如果需要进一步确定,可以使用:

1
sudo tcpdump -i any -n port 22

观察 SYN 是否发出、有没有 SYN-ACK 返回。

例如:

1
2
客户端 → SYN →
客户端 ← SYN-ACK ←

说明服务器侧至少有返回。

如果只有:

1
2
3
客户端 → SYN →
客户端 → SYN →
客户端 → SYN →

始终没有收到对应响应,那么就可以进一步怀疑中间路径存在丢弃或过滤。

这时候排障就从:

1
“感觉端口有问题”

进入了:

1
“观察实际 TCP 报文”

的阶段。


十一、为什么换网络是一个非常有效的排障方法?

这次真正让我快速定位问题的,其实不是某条命令,而是:

手机热点对照实验。

网络故障最怕的是同时存在太多变量。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
客户端
↓
校园网
↓
出口网关
↓
运营商
↓
Internet
↓
云平台
↓
安全组
↓
服务器防火墙
↓
sshd

如果只盯着服务器排查,就可能在 ufw、iptables、sshd、云安全组里来回折腾很久。

而一旦换成:

1
2
3
4
5
6
7
8
9
10
11
客户端
↓
手机热点
↓
运营商
↓
Internet
↓
云平台
↓
服务器

服务器完全没变。

所以:

1
2
3
校园网失败
+
手机热点成功

本身就是一次非常强的“隔离变量”实验。

它直接把问题从:

1
服务器 / 云平台 / 网络路径

缩小到了:

1
校园网出口及其路径

这比不停修改服务器配置有效得多。


十二、以后遇到 SSH 连接问题,我会这样排查

现在回头看,这类问题其实可以形成一套比较固定的排查顺序。

第 1 层:IP 是否可达

1
ping <ip>

确认是不是连最基础的 IP 通信都不存在。

但注意:

1
ping 通 ≠ TCP 一定通

第 2 层:目标 TCP 端口是否可建立

1
nc -zv <ip> <port>

重点观察:

1
2
3
succeeded
refused
timeout

分别对应不同方向的排查。


第 3 层:服务器有没有服务监听

1
sudo ss -tlnp | grep <port>

确认:

1
2
3
4
端口存在吗?
是哪个进程占用?
真的是 sshd 吗?
监听地址正确吗?

第 4 层:服务器本机测试

例如:

1
nc -zv 127.0.0.1 22

证明服务本身至少能够接受本机连接。


第 5 层:检查服务器防火墙

例如:

1
2
sudo ufw status
sudo iptables -L -n

或者其他发行版对应的防火墙工具。


第 6 层:检查云安全组

特别是云服务器,不要只看操作系统里的防火墙。


第 7 层:更换网络

例如:

1
2
3
校园网 → 手机热点
公司网络 → 家庭网络
Wi-Fi → 手机流量

这是非常高性价比的排查手段。


第 8 层:必要时抓包

1
sudo tcpdump -i any -n port 22

进一步观察:

1
2
3
SYN 有没有出去?
SYN-ACK 有没有回来?
RST 是谁发的?

到了这个层面,基本就是在直接观察 TCP 的实际行为,而不是仅凭客户端错误信息猜测。


十三、最终结论

这次问题最后可以归纳成一句话:

服务器本身没有故障,SSH 服务、防火墙和云安全组均正常;真正的问题是校园网出口路径对 TCP 22 连接进行了过滤,使 SSH 无法完成 TCP 建连。

整个过程最值得记录的并不是“SSH 改成了 51832”,而是排障思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ping 正常
↓
不要急着认为网络正常
↓
单独测试 TCP 端口
↓
排除服务器 sshd
↓
排除服务器防火墙
↓
排除云安全组
↓
更换网络做对照实验
↓
定位到本地网络出口
↓
更换 SSH 监听端口

而这次经历里,我觉得最值得记住的其实是三个判断:

1. ping 通,不代表 TCP 服务可达

ICMP 和 TCP 是不同协议,网络设备完全可以对它们采用不同的策略。

2. 网络故障不要只盯着服务器

当服务器配置全部正常时,换一个网络环境做对照实验,往往比继续改服务器配置更有效。

3. SSH 换端口时,先确认端口真的空闲

不要因为:

1
UFW 允许

就认为:

1
这个端口可以直接拿来跑 SSH

一定要先检查:

1
sudo ss -tlnp | grep <port>

确认没有其他服务占用。


附:常用排障命令

测试 IP 连通性

1
ping <ip>

测试 TCP 端口

1
nc -zv <ip> <port>

也可以使用:

1
timeout 5 bash -c 'cat < /dev/null > /dev/tcp/<ip>/<port>'

查看监听端口和进程

1
sudo ss -tlnp

或者:

1
sudo ss -tlnp | grep <port>

查看 UFW

1
sudo ufw status

查看 iptables

1
sudo iptables -L -n

查看 SSH 配置

1
2
grep -i '^Port' /etc/ssh/sshd_config
sudo sshd -T | grep -i '^port'

检查 SSH 配置语法

1
sudo sshd -t

重启 SSH 服务

1
sudo systemctl restart ssh

或:

1
sudo systemctl restart sshd

具体取决于发行版的服务名称。

抓包观察 TCP 连接

1
sudo tcpdump -i any -n port 22

也可以进一步限定来源:

1
sudo tcpdump -i any -n host <你的IP> and port <端口>

这次排障让我比较直观地体会到一件事:

网络问题最重要的不是“多试几个命令”,而是不断缩小故障范围。

从 ICMP 到 TCP,从客户端到服务器,再从服务器切换到网络出口;每进行一次测试,实际上都应该是在排除一部分可能性。

当最后只剩下一个变量时,问题通常也就不难解决了。