autossh 反向 SSH 隧道持久化实战:家宽无公网 IP 稳定暴露内网服务、systemd 保活与断线自愈

没有公网 IP 的家宽,怎么把服务稳定暴露出去

个人站长常见的一种困境:源码、数据库、博客都放在家里的树莓派或旧笔记本上,带宽够用、机器闲置,唯独运营商不给公网 IPv4。你有几个选择——frp、Cloudflare Tunnel、WireGuard,这些都是好东西,但它们要么依赖一台额外的中转服务器加客户端软件,要么把流量交给第三方。而 SSH 本身就能做隧道,服务端几乎不用装任何东西,只要你有一台带公网 IP 的 VPS,就能把家里的服务反向映射出去。

问题出在「稳定」两个字上。SSH 隧道有个坏习惯:网络一抖动、路由器一重启、NAT 表项一过期,连接就断了,而且断得非常安静——你这边 ssh 进程还在,端口还在监听,但从外面就是连不进来。这就是 autossh 存在的意义。它不替代 SSH,它做的只有一件事:盯着这条隧道,断了就重连。

先理解反向隧道到底做了什么

在写任何脚本之前,把方向搞清楚,后面所有的坑都源于方向搞混。

  • 本地转发(-L):在你本地开一个端口,流量经 SSH 发到远端。方向是「本地端口 → 远端服务」。
  • 远程转发(-R):在远端(VPS)上开一个端口,流量经 SSH 反向送回到你本地能访问的服务。方向是「远端端口 → 本地服务」。

我们要的是后者。典型命令长这样:

ssh -N -R 8000:127.0.0.1:80 user@vps.example.com

逐段拆解:-N 表示不执行远程命令,只做转发;-R 8000:127.0.0.1:80 的意思是「在 VPS 的 8000 端口上监听,把收到的连接通过这条 SSH 隧道,转发给本地的 127.0.0.1:80」。注意这里的 127.0.0.1:80 是相对于你这台发起 SSH 的机器而言的,不是你 VPS 上的本地。

还有个关键细节:默认情况下,-R 开的监听端口只绑定在 VPS 的 127.0.0.1 上,外网访问不到。要让 VPS 把它绑到所有网卡,需要设置 GatewayPorts,有两种写法:

# 客户端侧:只把这一个端口暴露到所有网卡
ssh -N -R 0.0.0.0:8000:127.0.0.1:80 user@vps.example.com

注意绑定地址是写在端口前面的,语法是 -R [绑定地址:]远端端口:目标主机:目标端口。如果写错顺序,SSH 会报「Bad remote forwarding specification」。服务端侧则是在 /etc/ssh/sshd_config 里设置 GatewayPorts clientspecified,改完记得 systemctl reload sshd——注意是 reload 而不是 restart,reload 不会掐断你当前正在用的这条 SSH 连接,否则你自己把自己踢下线。

为什么裸 SSH 隧道一定会断

把上面那条命令跑起来,你可能半天、一天都平安无事,然后在某个凌晨它悄悄死了。原因通常有三种,理解它们才能理解 autossh 的参数该怎么调。

  • NAT 表项超时:家庭路由器维护 NAT 映射是有生命周期的,通常几十秒到几分钟。如果隧道上长时间没有数据流动,路由器就把这条映射回收了,之后 VPS 再往家里发数据,找不到对应关系,连接废掉。这是最常见的原因。
  • 链路抖动与休眠:WiFi 重连、笔记本合盖、运营商短暂掉线,都会让底层 TCP 连接失效。
  • VPS 侧的 sshd 主动断开:服务端有 ClientAliveInterval 与 ClientAliveCountMax,长时间空闲会被服务端判定为死连接。

对应地,SSH 自己有两个参数专门用来对抗前两种:ServerAliveInterval 30 让客户端每 30 秒发一个保活包,ServerAliveCountMax 3 表示连续 3 次没响应才判定断开。这两个参数能让 TCP 连接「活着」,但它们解决不了进程崩溃或链路彻底中断后的重连问题——连接真断了,ssh 进程会退出,而它不会自己再连。这正是 autossh 补上的那块。

autossh 的两种监控模式

先安装,主流发行版都有:

apt install -y autossh    # Debian/Ubuntu
# 或 yum install -y autossh

autossh 有两条监控回路,理解它俩的区别能省掉一整晚的排错:

  • ServerAlive 模式(推荐):autossh 设置 AUTOSSH_GATETIME 与 SSH 自身的 ServerAlive 参数,让 ssh 在探测到对端无响应时主动退出,autossh 看到子进程退出就重启它。
  • 回环端口模式(老式):autossh 在本机起一个端口转发一对回环地址的探测端口,通过隧道来回发心跳,探测隧道是否存活。它需要 -M 指定监控端口,且要求隧道两端都支持回环端口转发。

老式 -M 模式在某些云 VPS 上会翻车,因为探测端口要来回穿透 SSH,而很多环境把回环转发关掉了。现代实践里,直接依赖 SSH 自己的 ServerAlive 更干净。典型命令:

autossh -M 0 \
  -o "ServerAliveInterval 30" \
  -o "ServerAliveCountMax 3" \
  -o "ExitOnForwardFailure yes" \
  -N -R 0.0.0.0:8000:127.0.0.1:80 \
  user@vps.example.com

-M 0 是关键,它关闭了 autossh 的老式监控端口,转而完全依赖 SSH 的 ServerAlive 机制。这几行里还有两个参数值得单独说。

ExitOnForwardFailure yes 是保命的。默认情况下,如果远端端口被占用,SSH 会打印一条警告然后继续运行——隧道是通不了的,但 ssh 进程活着,autossh 看你进程没死就不重启,于是你陷入「进程在、服务不可用」的假死状态。ExitOnForwardFailure yes 让 SSH 在这种情况下直接退出,autossh 立刻重启,重启时端口通常已经释放,于是自愈成功。

AUTOSSH_GATETIME 控制的是「SSH 至少存活多久,autossh 才认为这是一次成功的连接」。默认 30 秒。如果设为 0,表示连接一开始就走保活逻辑,适合需要秒级重连的场景;保留默认则能避免连接刚建立就断开导致的疯狂重连轰炸。

用 systemd 把它变成常驻服务

在临时终端里跑 autossh 是没有意义的——你一关终端就没了。正确的做法是交给 systemd 托管。在 /etc/systemd/system/ 下建一个单元文件,比如 autossh-home.service:

[Unit]
Description=Reverse SSH tunnel to home web server
After=network-online.target
Wants=network-online.target

[Service]
User=root
Environment="AUTOSSH_GATETIME=0"
Environment="AUTOSSH_PORT=0"
ExecStart=/usr/bin/autossh -M 0 \
  -o "ServerAliveInterval 30" \
  -o "ServerAliveCountMax 3" \
  -o "ExitOnForwardFailure yes" \
  -o "StrictHostKeyChecking=accept-new" \
  -i /root/.ssh/id_home \
  -N -R 0.0.0.0:8000:127.0.0.1:80 \
  user@vps.example.com
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

几个容易踩的点:

  • 用密钥而非密码:systemd 服务里没法交互输密码。事先用 ssh-copy-id -i /root/.ssh/id_home.pub user@vps.example.com 把公钥装上去。
  • 指定 IdentityFile:-i /root/.ssh/id_home 明确指定私钥,否则以 root 身份跑的服务可能用错密钥。
  • AcceptHostKey:首次连接会问 host key,服务里没法回答。要么预先 ssh-keyscan vps.example.com >> ~/.ssh/known_hosts,要么用 StrictHostKeyChecking=accept-new(比 no 安全,首次信任后锁定)。
  • After/Wants network-online:保证网络就绪后再启动,否则开机时会白白失败一轮。

启动并设为开机自启:

systemctl daemon-reload
systemctl enable --now autossh-home.service
systemctl status autossh-home.service

怎么确认它真的在工作,而不是假活着

这是本文学得最有价值的一步。很多人 systemctl status 看到绿色的 active (running) 就以为万事大吉,其实隧道可能早就断了而进程还挂着。要真正验证,得从 VPS 那一端去连:

# 在 VPS 上执行:确认端口在监听
ss -ltnp | grep 8000

# 在 VPS 上执行:实际请求一次,验证数据能回传
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/

如果 ss 显示 0.0.0.0:8000 在监听且 curl 返回 200,说明隧道是通的。如果端口在但 curl 超时,往往是本地服务没起来,不是隧道的问题。如果端口压根不在监听,说明这条隧道没建立成功,去看 journalctl -u autossh-home.service -n 50。

更省心的做法是写一个探测脚本,挂在 VPS 的 crontab 上每隔几分钟跑一次,验证失败就重启服务——但注意,这个脚本要跑在发起隧道的那一端才最合理,因为 autossh 在那里。简单版本:

#!/bin/bash
# /usr/local/bin/tunnel-watch.sh —— 放在家宽这一端
if ! curl -sS --max-time 10 -o /dev/null http://127.0.0.1:80/; then
    logger -t tunnel-watch "local service down, skip"
    exit 0
fi
if ! systemctl is-active --quiet autossh-home.service; then
    logger -t tunnel-watch "autossh inactive, restarting"
    systemctl restart autossh-home.service
fi

把入口收好:别让隧道变成后门

隧道打通之后,你等于在 VPS 上开了一个通往家庭内网的入口。这个入口必须当成生产服务来对待,否则它就是一个后门。几个必做的动作:

  • VPS 的防火墙只放行需要的端口:如果你不打算让全世界访问,就别用 0.0.0.0 绑定,改绑 127.0.0.1,然后再在前面放一个 Nginx 反向代理,把域名、HTTPS、访问控制都交给 Nginx。这样家庭端口永远不直接面向公网。
  • 限制来源:如果一定要直接暴露,用云防火墙或 nftables 只允许你自己的办公 IP、监控探针 IP 访问。
  • 给隧道账号最小权限:在 VPS 上单独建一个只能转发、不能登录 shell 的账号,把 shell 设为 /usr/sbin/nologin,并在 authorized_keys 里用 no-pty,no-X11-forwarding,permitlisten="8000" 这类限制选项锁死它能监听的端口。
  • 本地服务只监听本地:家里的 Web 服务保持 listen 127.0.0.1:80,隧道转发的是本地回环,本来就够用,不要让它在局域网上也敞着。

把 authorized_keys 里的限制写全,效果是这样的:

no-pty,no-agent-forwarding,no-X11-forwarding,permitlisten="8000" ssh-ed25519 AAAA... tunnel@home

常见故障与判读

  • 报 Warning: remote port forwarding failed for listen port 8000:远端端口被占用,通常上一次的连接还没被回收。等一两分钟或换端口;配上 ExitOnForwardFailure yes 让 autossh 自动重试。
  • 报 Permission denied (publickey):密钥没装好,或 -i 指定的路径不对。以服务身份跑时,注意 systemd 里 User= 对应的家目录是否和你放密钥的位置一致。
  • 反复重连、日志刷屏:多半是 AUTOSSH_GATETIME=0 配上了一个必然失败的连接(比如密钥错)。这种配置会让 autossh 高频重试,把 auth 日志刷满,也可能触发 VPS 的 fail2ban 把你的 IP 封了。排查清楚再落地。
  • 进程 active 但外网打不开:八成是 GatewayPorts 没配,端口只绑在 127.0.0.1。用 ss -ltnp 看绑定地址是 127.0.0.1:8000 还是 0.0.0.0:8000。

小结

autossh 反向隧道适合这样的场景:你有一台闲置的家里机器、一台公网 VPS,不想引入额外的中转软件,希望用最小依赖把家里的服务稳定地映射出去。核心就三件事——用 -R 建反向隧道并想清楚绑哪个地址、用 -M 0 加 ServerAlive 让 SSH 自己管保活、用 systemd 加 ExitOnForwardFailure 实现进程级自愈。最后一定记住那句被无数人忽略的话:验证隧道,要去对端连一次,进程活着不等于链路活着。把验证做扎实,把入口权限收干净,这条隧道就能安安稳稳地陪你跑很久。

Last modification:October 1st, 2026 at 07:25 pm

Leave a Comment