问题背景

一台 Windows + WSL2 服务器同时运行了 Web 服务、SSH、Docker 和本地代理。服务器所在网段可以正常访问这些服务,但另一个内网网段的机器始终连接超时。

最初看起来像 Windows 防火墙没有放行端口。实际排查后发现,问题由两层配置叠加造成:

  • Windows 防火墙需要允许可信内网访问 WSL 中的 TCP 服务。
  • WSL 里的 Docker 网桥与真实内网网段重叠,导致返回数据走错网卡。

第二点更隐蔽。即使端口已经监听、防火墙规则也正确,只要回程路由错误,客户端看到的仍然只是超时。


先确认服务真的在监听

不要一开始就改防火墙。先在 WSL 中确认服务监听地址:

1
ss -lntp

如果服务只监听 127.0.0.1,其他机器无法直接连接。需要对内网提供服务时,应让它监听 0.0.0.0:<PORT>,或者通过反向代理暴露。

然后分别从服务器本机和客户端测试:

1
2
3
4
5
# 在服务器本机测试
curl -I http://127.0.0.1:<PORT>

# 在内网客户端测试
curl -I --connect-timeout 5 http://<SERVER_IP>:<PORT>

本机成功而远程超时,说明应用大概率正常,问题位于防火墙、转发或路由层。


Windows 防火墙应该按信任边界配置

如果服务器运行的端口很多,逐个维护端口规则很容易漏。对于确定可信的 Private 网络,可以建立一条统一的 TCP 入站规则:

1
2
3
4
5
6
7
8
New-NetFirewallRule `
-DisplayName "Trusted Private TCP Services" `
-Direction Inbound `
-Action Allow `
-Profile Private `
-Protocol TCP `
-LocalPort Any `
-RemoteAddress Any

这条规则的含义是:

  • 仅对 Windows 识别为 Private 的网络生效。
  • 允许访问本机全部 TCP 端口。
  • Public 网络不受影响,仍执行默认阻止策略。
  • UDP 不统一开放,需要时再单独添加规则。

先确认当前网络配置文件,避免把不可信网络误设为 Private:

1
Get-NetConnectionProfile

如果只运行少量固定服务,按端口放行仍然更稳妥。全 TCP 放行适合端口较多、网络边界清楚、客户端均可信的内网环境。


防火墙放行后仍然超时,就检查路由

这次排障中,防火墙规则生效后,同网段客户端已经可以访问,但另一个网段仍然超时。此时继续添加端口规则没有意义,需要检查 WSL 的路由表:

1
2
ip -4 addr
ip -4 route

重点查看 Docker 创建的网桥:

1
2
docker network ls
docker network inspect bridge

典型冲突如下:

1
2
真实客户端网段:<CLIENT_SUBNET>
Docker 网桥网段:覆盖了 <CLIENT_SUBNET> 的更大网段

例如,真实客户端位于某个 /24 网段,而 Docker 恰好创建了覆盖它的 /16 网桥。Linux 根据路由表选择出口时,会把返回客户端的数据送进 Docker 网桥,而不是发往真正的局域网网关。

请求能够到达服务器,但响应回不到客户端。客户端最终只看到连接超时。

可以用下面的命令直接确认内核准备走哪条路:

1
ip route get <CLIENT_IP>

如果输出中的设备是 docker0 或其他 Docker bridge,而不是实际连接局域网的网卡,就找到了根因。


用更精确的静态路由修复回程路径

为真实客户端网段添加一条更精确的路由:

1
sudo ip route replace <CLIENT_SUBNET> via <LAN_GATEWAY> dev <LAN_INTERFACE>

例如,Docker 占用了较大的 /16,真实内网使用其中一个 /24。新增的 /24 路由比 /16 更精确,因此内核会优先把数据交给真实局域网网卡。

再次检查:

1
ip route get <CLIENT_IP>

确认输出中的网关和网卡正确后,再从客户端测试 SSH 和 Web 服务:

1
2
nc -vz <SERVER_IP> 22
curl -I --connect-timeout 5 http://<SERVER_IP>:<PORT>

如果可以调整 Docker 网络,长期更干净的方案是重新规划 Docker address pool,避免它与公司内网、家庭局域网、VPN 和 Tailscale 网段重叠。不过修改已有 Docker 网段可能影响正在运行的容器,因此临时增加精确路由通常风险更低。


让静态路由在 WSL 重启后自动恢复

手动执行 ip route 只在当前 WSL 实例中有效。WSL 重启后路由会消失,可以创建 systemd 服务自动恢复:

1
2
3
4
5
6
7
8
9
10
11
12
13
# /etc/systemd/system/wsl-lan-route.service
[Unit]
Description=Restore LAN route in WSL
After=network-online.target docker.service
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/sbin/ip route replace <CLIENT_SUBNET> via <LAN_GATEWAY> dev <LAN_INTERFACE>
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

启用并立即启动:

1
2
3
sudo systemctl daemon-reload
sudo systemctl enable --now wsl-lan-route.service
systemctl status wsl-lan-route.service

最后重启 WSL 再验证一次,确保修复不是暂时生效:

1
wsl --shutdown

重新进入 WSL 后检查:

1
2
3
ip route get <CLIENT_IP>
systemctl is-enabled wsl-lan-route.service
systemctl is-active wsl-lan-route.service

通过 SSH 安全复用服务器本地代理

网络恢复后,不能直接访问外网的内网机器还可以通过 SSH 隧道复用服务器上的本地 HTTP 代理。无需把代理端口直接暴露到整个局域网:

1
2
3
4
5
6
ssh -fNT \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=60 \
-o ServerAliveCountMax=3 \
-L <LOCAL_PROXY_PORT>:127.0.0.1:<REMOTE_PROXY_PORT> \
<USER>@<SERVER_IP>

然后在客户端设置代理环境变量:

1
2
3
4
5
export ALL_PROXY=http://127.0.0.1:<LOCAL_PROXY_PORT>
export http_proxy=http://127.0.0.1:<LOCAL_PROXY_PORT>
export https_proxy=http://127.0.0.1:<LOCAL_PROXY_PORT>
export HTTP_PROXY=http://127.0.0.1:<LOCAL_PROXY_PORT>
export HTTPS_PROXY=http://127.0.0.1:<LOCAL_PROXY_PORT>

验证时不要使用 ping,因为 HTTP 代理不会转发 ICMP。应该使用真实的 HTTP 请求:

1
2
curl -I https://github.com
curl -I https://www.google.com

返回 200 或正常的 301302 跳转,说明代理链路已经工作。

这种方式有两个好处:代理服务仍然只监听服务器的 127.0.0.1,同时认证和传输都由 SSH 负责,比直接向内网开放代理端口更稳妥。


这次排障得到的经验

排查跨网段访问超时时,可以按下面的顺序进行:

  1. 确认应用监听的是 0.0.0.0,并在服务器本机访问成功。
  2. 确认 Windows 当前网络属于预期的 Private 配置文件,入站规则已经匹配。
  3. 分别从同网段和跨网段客户端测试,判断问题是否只发生在特定来源网段。
  4. 在 WSL 中执行 ip route get <CLIENT_IP>,检查回包是否被 Docker 网桥截走。
  5. 使用更精确的静态路由修正回程路径,并通过 systemd 持久化。
  6. 最后再验证 SSH、Web 服务和 SSH 代理,不要只看端口是否处于监听状态。

这类故障最容易误判为“防火墙还没放开”。端口监听和防火墙只解决请求能否进入服务器,双向通信还要求返回路径正确。只要回程走错网卡,前面的配置看起来都正常,客户端仍然会一直超时。


扩展阅读