
🔥个人主页:爱和冰阔乐
📚专栏传送门:《数据结构与算法》 、C++
🐶学习方向:C++方向学习爱好者
⭐人生格言:得知坦然 ,失之淡然

🏠博主简介

文章目录
前言
实验室电脑、工控机、家庭 NAS 和边缘设备经常有一个共同问题:
设备能访问互联网
外面却不能主动连接设备
原因可能是:
- 没有公网 IPv4;
- 在公司或校园 NAT 后面;
- 路由器不归自己管理;
- 运营商使用 CGNAT;
- 防火墙不允许入站;
- 设备地址经常变化。
这时给内网设备安装远控软件当然可以,但如果需求只是:
在外面通过 SSH 维护一台 Linux 机器
反向 SSH 隧道更轻。
它的思路不是让公网服务器主动打进内网,而是让内网机器先连接一台有公网地址的服务器,并在服务器上反向开放一个端口。
本文会完成一条可长期运行的链路:
笔记本
↓ SSH
公网跳板机:22022
↓ 已建立的反向隧道
内网设备:22
同时处理几个真正容易出问题的地方:
- 为什么
ssh -R成功却连不上; - 端口绑定在
127.0.0.1还是0.0.0.0; - 网络抖动后怎样自动重连;
- systemd 的
Restart=always为什么仍可能假在线; - 如何限制专用账号,避免跳板机变成任意转发器;
- 怎样判断故障发生在内网设备、隧道、跳板机还是客户端。

一、反向隧道到底做了什么
1.1 普通本地转发
本地转发常见写法:
ssh -L 8080:internal-web:80 user@jump-server
含义:
本机 8080
通过 jump-server
访问 internal-web:80
1.2 反向转发
反向转发:
ssh -R 22022:127.0.0.1:22 [email protected]
这条命令由内网设备执行。
含义:
在远端服务器监听 22022
连接进入 22022 后
通过当前 SSH 会话转发到内网设备的 127.0.0.1:22
注意两个 127.0.0.1 的位置不同:
远端监听地址:默认通常只在跳板机回环地址监听
目标 127.0.0.1:22:相对于发起 ssh -R 的内网设备
1.3 数据流
从外部笔记本连接时:
ssh client
→ jump-server:22022
→ reverse tunnel
→ edge-device:127.0.0.1:22
→ edge-device sshd
如果跳板机端口正常监听,但设备本机 sshd 没启动,隧道依然无法完成最终连接。

图里的红色箭头被 NAT 墙挡住,说明外部不能直接拨入;橙色绳索由内网设备主动抛向跳板机,外部笔记本再沿现有链路进入。反向隧道改变的是连接建立方向。
二、准备三台角色
本文使用下面的名字:
| 角色 | 示例地址 | 用途 |
|---|---|---|
| 内网设备 | edge-box | 被维护的机器 |
| 公网跳板机 | server.example.com | 接收反向隧道 |
| 外部客户端 | laptop | 发起维护连接 |
跳板机至少需要:
- 可从公网访问的 SSH 端口;
- 一个专用 Linux 用户;
- 允许 TCP 转发;
- 防火墙允许需要的监听方式。
内网设备需要:
- 能主动访问跳板机;
- 本机 SSH 服务可用;
- 保存专用私钥;
- systemd。
三、先验证最小命令
3.1 检查内网设备 SSH
在内网设备执行:
systemctl status ssh
不同发行版服务名可能是:
systemctl status sshd
确认本机可连接:
ssh 127.0.0.1
如果这一步失败,先修本机 SSH,不要急着排查隧道。
3.2 创建专用密钥
在内网设备执行:
install -d -m 700 ~/.ssh
ssh-keygen \
-t ed25519 \
-f ~/.ssh/reverse_tunnel_ed25519 \
-C "edge-box reverse tunnel"
无人值守服务需要避免交互式密码。
如果私钥设置口令,就要额外配置 agent 或凭据解锁方案。不要在 systemd 单元中明文写私钥口令。
3.3 创建跳板机专用用户
在跳板机执行:
sudo useradd \
--create-home \
--shell /usr/sbin/nologin \
tunnel
某些系统的 nologin 路径不同:
command -v nologin
创建授权目录:
sudo install \
-d \
-m 700 \
-o tunnel \
-g tunnel \
/home/tunnel/.ssh
把内网设备的公钥写入:
sudo tee /home/tunnel/.ssh/authorized_keys
再修权限:
sudo chown tunnel:tunnel \
/home/tunnel/.ssh/authorized_keys
sudo chmod 600 \
/home/tunnel/.ssh/authorized_keys
3.4 前台启动隧道
内网设备执行:
ssh \
-NT \
-i ~/.ssh/reverse_tunnel_ed25519 \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-R 22022:127.0.0.1:22 \
[email protected]
参数说明:
| 参数 | 作用 |
|---|---|
-N | 不执行远端命令 |
-T | 不分配伪终端 |
-R | 建立远端端口转发 |
ExitOnForwardFailure=yes | 远端端口绑定失败时退出 |
ServerAliveInterval=30 | 定期发送保活请求 |
ServerAliveCountMax=3 | 多次无响应后断开 |
如果第一次连接询问主机指纹,先人工核对并接受。不要为了省事长期使用:
StrictHostKeyChecking=no
四、先从跳板机本机测试
4.1 检查监听
在跳板机执行:
ss -lntp | grep 22022
可能看到:
LISTEN 0 128 127.0.0.1:22022 0.0.0.0:*
LISTEN 0 128 [::1]:22022 [::]:*
这说明端口默认只绑定跳板机回环地址。
4.2 通过回环地址连接
在跳板机执行:
ssh \
-p 22022 \
[email protected]
这里的 edgeuser 是内网设备上的登录用户,不是跳板机的 tunnel 用户。
如果成功,说明:
内网设备 → 跳板机 SSH 会话正常
远端监听正常
隧道到内网设备 22 端口正常
内网设备 sshd 正常
4.3 为什么外部客户端仍连不上
因为监听地址是:
127.0.0.1:22022
公网网卡没有监听。
这不是错误,反而是更安全的默认方式。
外部客户端可以先 SSH 登录跳板机,再使用 ProxyJump 或二次转发,不必把 22022 暴露到公网。

链路中只有跳板机原有 SSH 入口暴露公网。22022 留在回环地址,由 ProxyJump 在跳板机内部访问,减少新增端口暴露。
五、推荐方式:端口只绑定回环地址
5.1 使用 ProxyJump
在外部笔记本的 ~/.ssh/config:
Host public-jump
HostName server.example.com
User admin
IdentityFile ~/.ssh/jump_ed25519
Host edge-box
HostName 127.0.0.1
Port 22022
User edgeuser
IdentityFile ~/.ssh/edge_admin_ed25519
ProxyJump public-jump
连接:
ssh edge-box
这里有两组密钥:
laptop → jump-server
laptop → edge-box(通过隧道)
不要把隧道服务密钥同时当管理员登录密钥。
5.2 为什么这种方式更稳
跳板机公网只开放原本的 SSH 端口。
反向端口:
只允许跳板机本机访问
攻击面更小,也不需要为每台内网设备额外开放一个公网端口。
5.3 使用显式 ProxyCommand
旧版客户端也可以:
Host edge-box
HostName 127.0.0.1
Port 22022
User edgeuser
ProxyCommand ssh public-jump -W %h:%p
优先使用 ProxyJump,配置更直接。
六、如果必须直接暴露远端端口
6.1 指定监听地址
内网设备命令:
ssh \
-NT \
-R 0.0.0.0:22022:127.0.0.1:22 \
[email protected]
但这条命令能否绑定公网地址,取决于跳板机 sshd_config。
6.2 GatewayPorts
跳板机检查:
sudo sshd -T | grep gatewayports
常见设置:
GatewayPorts no
GatewayPorts yes
GatewayPorts clientspecified
更可控的是:
GatewayPorts clientspecified
允许客户端明确指定监听地址,而不是把所有反向转发默认暴露到公网。
修改后先验证配置:
sudo sshd -t
再重载:
sudo systemctl reload sshd
部分系统服务名是 ssh。
6.3 防火墙限制来源
即使监听 0.0.0.0:22022,也不要默认允许全网访问。
以 UFW 为例:
sudo ufw allow \
from 203.0.113.10 \
to any port 22022 \
proto tcp
其中 203.0.113.10 替换为可信客户端出口地址。
云服务器还要检查安全组。
系统防火墙开放,不等于云平台安全组已经开放;反过来也一样。

图中公网绑定不是禁止项,但必须说明为什么需要,并同时限制 GatewayPorts、来源地址和账号权限。默认场景优先选择回环绑定。
七、限制 tunnel 专用账号
7.1 authorized_keys 限制
在跳板机:
restrict,port-forwarding,permitlisten="127.0.0.1:22022" ssh-ed25519 AAAA...
实际支持的选项与 OpenSSH 版本有关,修改后应查看本机:
man sshd
或:
man authorized_keys
restrict 用于关闭多项默认能力,再显式允许端口转发。
permitlisten 限制该密钥可以监听的地址与端口。
7.2 sshd_config Match
也可以在 /etc/ssh/sshd_config.d/tunnel.conf:
Match User tunnel
AllowTcpForwarding remote
GatewayPorts no
PermitTTY no
X11Forwarding no
AllowAgentForwarding no
如果确实需要客户端指定监听地址:
GatewayPorts clientspecified
不要直接复制配置后重启。
先检查:
sudo sshd -t
再查看最终展开配置:
sudo sshd -T
7.3 nologin 会不会影响隧道
ssh -N 不请求交互式 shell,但不同系统的 PAM 和 sshd 配置可能对 nologin 有额外限制。
如果连接立即关闭,查看:
sudo journalctl -u sshd -n 100 --no-pager
不要只在客户端反复改参数。
专用账号最终目标是:
能认证
能建立指定反向转发
不能执行任意命令
不能建立其他转发
八、写成 systemd 服务
8.1 为什么不用 ssh ... &
后台符号解决不了:
- 开机自动启动;
- 进程异常退出后重启;
- 日志查看;
- 启动顺序;
- 网络恢复;
- 资源和权限限制。
systemd 更适合长期守护。
8.2 创建专用本地用户
在内网设备:
sudo useradd \
--create-home \
--shell /usr/sbin/nologin \
reverse-ssh
安装私钥:
sudo install \
-d \
-m 700 \
-o reverse-ssh \
-g reverse-ssh \
/home/reverse-ssh/.ssh
sudo install \
-m 600 \
-o reverse-ssh \
-g reverse-ssh \
~/.ssh/reverse_tunnel_ed25519 \
/home/reverse-ssh/.ssh/id_ed25519
把已核对的服务器主机密钥写入:
sudo -u reverse-ssh \
ssh-keyscan \
-H server.example.com \
>> /home/reverse-ssh/.ssh/known_hosts
ssh-keyscan 只负责采集,不负责证明主机身份。首次部署应通过可信渠道核对指纹。
8.3 systemd 单元
/etc/systemd/system/reverse-ssh.service:
[Unit]
Description=Reverse SSH tunnel to public jump server
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=reverse-ssh
Group=reverse-ssh
ExecStart=/usr/bin/ssh \
-NT \
-i /home/reverse-ssh/.ssh/id_ed25519 \
-o BatchMode=yes \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ConnectTimeout=10 \
-o StrictHostKeyChecking=yes \
-R 22022:127.0.0.1:22 \
[email protected]
Restart=always
RestartSec=5s
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=read-only
[Install]
WantedBy=multi-user.target
路径必须使用绝对路径。
多行 ExecStart 的反斜杠后不要留下空格。
8.4 启用
sudo systemctl daemon-reload
sudo systemctl enable --now reverse-ssh.service
查看:
systemctl status reverse-ssh.service
实时日志:
journalctl \
-u reverse-ssh.service \
-f
九、为什么进程活着,隧道可能已经失效
9.1 TCP 半开连接
网络切换、NAT 超时或中间设备丢状态时,客户端进程可能暂时没有感知。
因此使用:
ServerAliveInterval=30
ServerAliveCountMax=3
大约连续多次无响应后退出,让 systemd 重启。
9.2 ExitOnForwardFailure
如果 22022 已被占用:
没有该参数时,SSH 主连接可能仍保持,systemd 看到进程存活,但转发并未建立。
加上:
ExitOnForwardFailure=yes
端口绑定失败会让进程退出,交给 systemd 重试。
9.3 网络在线目标不是绝对保证
network-online.target 只表示系统的网络管理组件认为网络已准备。
它不保证:
DNS 一定可用
公网一定可达
跳板机一定启动
认证一定成功
所以仍然需要 Restart=always。
十、一个跳板机连接多台设备
可以给每台设备分配一个回环端口:
edge-a → 127.0.0.1:22021
edge-b → 127.0.0.1:22022
edge-c → 127.0.0.1:22023
外部客户端配置:
Host edge-a
HostName 127.0.0.1
Port 22021
User edgeuser
ProxyJump public-jump
Host edge-b
HostName 127.0.0.1
Port 22022
User edgeuser
ProxyJump public-jump
端口分配要建立清单,避免两台机器抢同一个端口。
更严格的管理方式:
- 每台设备一个跳板机账号;
- 每台设备一把密钥;
permitlisten限制对应端口;- 日志能定位到具体账号。
共享一个 tunnel 用户虽然配置少,撤销单台设备时却不够方便。
十一、逐层排障
11.1 第一层:内网设备能否访问跳板机
getent hosts server.example.com
nc -vz server.example.com 22
或直接:
ssh -vvv \
[email protected]
关注:
DNS
TCP connect
主机指纹
公钥认证
11.2 第二层:远端转发是否建立
内网设备前台运行:
ssh -vvv \
-NT \
-o ExitOnForwardFailure=yes \
-R 22022:127.0.0.1:22 \
[email protected]
跳板机:
ss -lntp | grep 22022
如果没有监听,检查:
- 端口被占用;
AllowTcpForwarding;permitlisten;- 用户认证后被策略拒绝;
GatewayPorts与指定地址。
11.3 第三层:隧道目标能否连接
跳板机:
ssh -vvv \
-p 22022 \
[email protected]
如果出现:
connection refused
可能是内网设备的 22 端口没监听。
内网设备检查:
ss -lntp | grep ':22'
11.4 第四层:外部客户端到跳板机
如果跳板机本机能连,外部不能连:
- 使用回环绑定时,应检查
ProxyJump; - 使用公网绑定时,检查
GatewayPorts; - 检查系统防火墙;
- 检查云安全组;
- 检查客户端出口地址限制。
11.5 第五层:systemd
systemctl show reverse-ssh.service \
-p ActiveState \
-p SubState \
-p NRestarts
journalctl \
-u reverse-ssh.service \
--since "30 minutes ago" \
--no-pager
反复重启通常不是 systemd 故障,而是:
认证失败
主机密钥变化
端口冲突
DNS 不通
跳板机策略拒绝

排障顺序从服务状态、日志、远端监听到端到端 SSH。只看 systemctl active 不能证明端口真正建立,更不能证明最终登录可用。
十二、常见错误对照表
| 现象 | 常见原因 | 检查 |
|---|---|---|
| SSH 主连接成功,22022 不监听 | 转发失败但未退出 | ExitOnForwardFailure |
| 跳板机本机能连,公网不能 | 默认只绑定回环 | ss -lntp、ProxyJump |
指定 0.0.0.0 仍只监听回环 | GatewayPorts no | sshd -T |
| systemd 一直重启 | 密钥、指纹、DNS 或端口冲突 | journalctl |
| 运行几小时后失联 | NAT 状态过期或网络切换 | ServerAlive 参数 |
| 隧道端口存在但登录失败 | 内网 sshd 或目标用户问题 | 跳板机本机 ssh -p |
| 手工命令成功,服务失败 | 用户、HOME、known_hosts 不同 | User= 与绝对路径 |
| 重启 sshd 后所有隧道断开 | 配置重载或服务重启 | 使用 reload 并验证 |
十三、安全边界
13.1 反向隧道不是绕过管理制度的工具
公司、学校和生产网络可能明确禁止私建出口隧道。
部署前需要确认:
- 网络和信息安全政策;
- 数据是否允许经过公网服务器;
- 跳板机是否经过授权;
- 操作日志是否满足审计;
- 设备是否属于个人可管理资产。
技术上能建立,不代表组织上允许。
13.2 跳板机本身要加固
至少做到:
禁用密码登录
管理员密钥分离
及时更新 OpenSSH
限制来源地址
只开放必要端口
启用登录审计
备份 sshd 配置
反向隧道把内网设备的管理入口延伸到了跳板机。
跳板机失守,所有隧道都可能成为攻击路径。
13.3 不要转发数据库到公网
类似:
-R 0.0.0.0:3306:127.0.0.1:3306
风险很高。
数据库管理优先通过:
SSH 登录设备后本地操作
回环监听 + ProxyJump
临时本地转发
避免长期公网暴露。
十四、健康检查
进程存活只能证明 SSH 客户端还在。
可以从跳板机定期检查:
timeout 5 \
ssh \
-o BatchMode=yes \
-o ConnectTimeout=3 \
-p 22022 \
[email protected] \
true
或只检查 TCP:
nc -z 127.0.0.1 22022
TCP 检查只能证明监听存在,不能证明最终认证可用。
更完整的健康指标:
systemd 当前状态
最近一次成功建立时间
重启次数
跳板机监听端口
端到端 SSH 测试
内网设备 sshd 状态
十五、上线前检查单
[ ] 内网设备本机 SSH 已验证
[ ] 隧道使用专用密钥
[ ] 跳板机使用专用账号
[ ] 默认只绑定跳板机回环地址
[ ] 外部连接使用 ProxyJump
[ ] 必须公网绑定时限制来源 IP
[ ] 已启用 ExitOnForwardFailure
[ ] 已配置 ServerAliveInterval 和 CountMax
[ ] systemd 使用绝对路径和 BatchMode
[ ] known_hosts 已核对,不关闭主机校验
[ ] sshd_config 修改前执行 sshd -t
[ ] 专用账号不能执行任意命令
[ ] 每台设备有独立密钥或账号
[ ] 有端到端健康检查
[ ] 已确认组织网络安全政策允许
总结
反向 SSH 解决的是连接方向问题:
内网设备不能被主动访问
但它可以主动连接公网跳板机
稳定运行要同时满足四层条件:
- 内网设备的 SSH 服务正常;
- 到跳板机的主连接可保持;
- 远端端口确实成功绑定;
- 外部客户端使用正确的入口访问。
推荐把反向端口只绑定在跳板机 127.0.0.1,再通过 ProxyJump 登录。这样不必额外暴露公网端口。
systemd 负责重启进程,保活参数负责发现假连接,ExitOnForwardFailure 负责发现端口根本没建起来。三者解决的是不同问题,不能只写一个 Restart=always 就认为链路可靠。
最后,给隧道账号、密钥、监听端口和可转发目标都设边界。反向 SSH 很方便,也很容易把一个临时维护入口变成长期暴露面。
资源分享
【Linux】系统彻底进不去怎么办?initramfs、recovery 与 chroot 救援指南
【Linux】内核升级后启动失败?旧内核回退、initramfs 重建与 DKMS 修复
【Linux排障实战】Docker容器启动失败怎么查:端口、日志、权限与网络
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2402_87731470/article/details/161982260




