没有公网 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 autosshautossh 有两条监控回路,理解它俩的区别能省掉一整晚的排错:
- 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 实现进程级自愈。最后一定记住那句被无数人忽略的话:验证隧道,要去对端连一次,进程活着不等于链路活着。把验证做扎实,把入口权限收干净,这条隧道就能安安稳稳地陪你跑很久。