1. 为什么需要开机自启:从一次凌晨断连说起
很多人第一次接触 autossh,是因为一条隧道断了,而人不在机器旁边。我印象很深的一次,是帮一个朋友维护一台放在机房的采集服务器,它需要长期把本地的一个数据库端口映射到一台中转机上,供外部的分析脚本读取。白天测试一切正常,结果第二天早上发现数据全断了——原因是凌晨机房网络抖动了一下,SSH 会话被重置,而 autossh 虽然会自动重连,但那台机器本身重启过一次,autossh 进程根本没起来。
这就是"配置开机启动"这件事的真正价值所在。autossh 解决的是"连接断了自动重连",而开机启动解决的是"机器重启后 autossh 自己能不能爬起来"。这两件事经常被混为一谈,但它们是两个独立的问题。你只配了 autossh 的监控参数,机器一重启,一切归零;你只配了开机启动,但 autossh 参数写得不对,进程起来了却连不上,照样白搭。
所以这篇内容要讲清楚的,是把 autossh 从"手动敲命令"变成"系统级常驻服务"的完整链路。适合谁看?适合那些需要长期维持一条 SSH 隧道、又不想每天手动检查进程的运维同学、后端开发、以及自己折腾家庭服务器或者远程采集节点的人。不管你用的是 systemd 还是老式的 init 脚本,不管你跑在哪种 Linux 发行版上,核心思路是相通的。
在展开之前,先把一个基础概念说透:autossh 本身并不是一个"新协议",它只是对系统自带 ssh 命令的一层包装。它的工作原理是启动一个 ssh 子进程,然后通过一个额外的监控端口(默认是 0,也就是关闭,通常我们会显式指定)来探测这条连接是否还活着。一旦发现连接异常,它就杀掉旧的 ssh 进程重新拉起一个。理解这一点很关键,因为它决定了后面所有配置的写法——autossh 的参数几乎就是 ssh 的参数,你平时怎么连,这里就怎么写,只是多包了一层。
2. autossh 的监控机制与参数选择逻辑
2.1 监控端口到底在监控什么
autossh 最容易被误解的地方,就是那个-M参数。很多人以为它是"心跳端口",随便填一个就行。实际上它的机制是这样的:autossh 会在本地起一个监听端口(由-M指定),然后让 ssh 子进程通过远程转发把这个端口映射到对端。autossh 定期往这个端口发数据,如果数据能顺利绕一圈回来,说明隧道是通的;如果发出去没回应,就判定连接已死。
这就带来一个实际问题:-M指定的端口必须在本地和对端都不会被占用,而且对端的 sshd 要允许端口转发。如果你填的端口恰好被别的服务占了,autossh 启动时就会报错退出,表现出来就是"服务起不来"。我一般习惯用-M 0,也就是关闭 autossh 自带的监控,改用 ssh 自己的ServerAliveInterval和ServerAliveCountMax来保活。为什么?因为 ssh 原生的心跳机制更轻量,不额外占端口,也不依赖对端的转发权限,实测下来在大多数场景里更稳。
2.2 关键参数逐个拆解
下面这张表是我在实际配置里最常用的几个参数,以及它们各自的作用和常见取值:
| 参数 | 作用 | 推荐取值 | 说明 |
|---|---|---|---|
-M | autossh 监控端口 | 0 | 设为 0 表示关闭自带监控,改用 ssh 心跳 |
-N | 不执行远程命令 | 固定加上 | 只做端口转发,不开 shell |
-L | 本地转发 | 按需 | 把本地端口映射到对端 |
-R | 远程转发 | 按需 | 把对端端口映射回本地 |
-o ServerAliveInterval | 心跳间隔 | 30 | 每 30 秒发一次保活包 |
-o ServerAliveCountMax | 最大失败次数 | 3 | 连续 3 次没回应就断开重连 |
-o ExitOnForwardFailure | 转发失败即退出 | yes | 避免"进程活着但转发没建立"的假象 |
-o StrictHostKeyChecking | 主机密钥检查 | no | 首次连接免交互,自动化场景常用 |
这里重点说两个容易被忽略的。第一个是ExitOnForwardFailure yes。默认情况下,ssh 即使端口转发失败,进程也会继续挂着,autossh 看进程还在,就以为一切正常,结果隧道根本没通。加上这个参数后,转发一失败 ssh 就退出,autossh 立刻感知并重连,这才是真正的"自愈"。
第二个是ServerAliveInterval和ServerAliveCountMax的配合。假设间隔 30 秒、次数 3 次,那么最长 90 秒内如果对端毫无响应,ssh 就会主动断开。这个时间不能太短,否则网络稍微抖一下就被判定为断线,频繁重连反而增加负担;也不能太长,否则真断了要等很久才恢复。30 秒配 3 次是我在多数网络环境下验证过的平衡点。
2.3 密钥认证是自动化的前提
开机启动的 autossh 是没有人盯着输密码的,所以必须用密钥认证。生成密钥对、把公钥放到对端的authorized_keys里,这是标准流程。但有个细节:私钥的权限必须是600,.ssh目录必须是700,否则 ssh 会拒绝使用这个密钥,报"permissions are too open"。这个坑我在第一次配的时候踩过,进程起来了但一直认证失败,日志里翻半天才看到权限问题。
另外,如果你用的是非默认端口的 ssh,记得在 autossh 命令里用-p指定,别指望它自动读~/.ssh/config——虽然它确实会读,但在 systemd 环境下,运行用户的家目录可能和你预期的不一样,配置文件不一定被加载。显式写参数永远比依赖隐式配置可靠。
3. 用 systemd 把 autossh 变成常驻服务
3.1 为什么首选 systemd
现在的 Linux 发行版基本都上了 systemd,用它来管理 autossh 有几个明显好处:开机自动拉起、进程挂了自动重启、日志统一走 journalctl、可以设置依赖关系(比如等网络就绪后再启动)。相比之下,老式的 rc.local 或者 crontab 的@reboot方式,要么没有自动重启能力,要么日志散落各处,排查起来很痛苦。
我现在的做法是:只要系统有 systemd,就一律用 systemd 单元文件,不再考虑其他方案。下面这套配置是我在多个环境里反复用过的,可以直接抄。
3.2 单元文件的完整写法
先创建服务文件,路径是/etc/systemd/system/autossh-tunnel.service:
[Unit] Description=AutoSSH Tunnel Service After=network-online.target Wants=network-online.target [Service] Type=simple User=tunnel Environment="AUTOSSH_GATETIME=0" ExecStart=/usr/bin/autossh -M 0 -N \ -o "ServerAliveInterval 30" \ -o "ServerAliveCountMax 3" \ -o "ExitOnForwardFailure yes" \ -o "StrictHostKeyChecking no" \ -i /home/tunnel/.ssh/id_rsa \ -L 3306:127.0.0.1:3306 \ tunnel@remote.example.com -p 22 Restart=always RestartSec=10 [Install] WantedBy=multi-user.target逐段解释一下。After=network-online.target和Wants=network-online.target是保证网络真正可用之后再启动,否则开机时网络还没起来,autossh 一连就失败,虽然会重试,但白白多等一轮。AUTOSSH_GATETIME=0这个环境变量很关键,它告诉 autossh 不要因为首次连接失败就退出——默认情况下 autossh 有个"启动宽限期",首次连不上会直接放弃,设成 0 就变成无限重试。
Restart=always配合RestartSec=10,意思是无论进程因为什么原因退出,10 秒后都重新拉起。这是双保险:autossh 自己会重连 ssh,systemd 则在 autossh 整个进程挂掉时兜底。
3.3 专用运行用户与权限隔离
注意上面我用了User=tunnel,而不是 root。这是一个安全习惯:隧道服务不需要 root 权限,用一个专用的低权限用户跑就行。创建这个用户、给它配好密钥、把私钥权限设成 600,这些步骤虽然多花几分钟,但能避免"一个隧道服务拥有整机权限"的风险。
如果你图省事用 root 跑,也不是不行,但私钥就暴露在 root 家目录下,一旦这台机器被入侵,攻击者直接拿到对端的登录凭据。用专用用户的话,即使出问题,影响面也小得多。这个取舍我建议不要省。
3.4 启用与验证的完整流程
配置写完后,按顺序执行:
sudo systemctl daemon-reload sudo systemctl enable autossh-tunnel.service sudo systemctl start autossh-tunnel.service sudo systemctl status autossh-tunnel.servicedaemon-reload是必须的,否则 systemd 不认识你新写的文件。enable是设置开机自启,start是立即启动。看状态时重点确认两件事:一是Active那一行是不是active (running),二是下面有没有报错信息。
验证隧道是否真的通了,不能只看进程在不在。我的做法是直接连本地转发端口:
nc -zv 127.0.0.1 3306如果显示 succeeded,说明转发链路是通的。更进一步,可以真的用客户端连一下,确认数据能过去。进程活着但端口不通的情况太常见了,一定要用实际连接来验证。
4. 没有 systemd 时的替代方案与踩坑记录
4.1 老系统的 rc.local 方案
有些精简系统或者老版本发行版没有 systemd,那就退回到 rc.local。在/etc/rc.local里(exit 0之前)加一行:
su - tunnel -c "AUTOSSH_GATETIME=0 autossh -M 0 -N -o 'ServerAliveInterval 30' -o 'ServerAliveCountMax 3' -o 'ExitOnForwardFailure yes' -i /home/tunnel/.ssh/id_rsa -L 3306:127.0.0.1:3306 tunnel@remote.example.com -p 22" &注意这里用su - tunnel -c切换到专用用户执行,末尾的&让它后台运行。这个方案的缺点是:没有自动重启,autossh 进程真挂了就没了;日志也不好找。所以它只适合作为临时方案,能上 systemd 就上 systemd。
4.2 我踩过的三个真实坑
第一个坑是AUTOSSH_GATETIME没设。当时服务开机启动,网络还没完全就绪,autossh 首次连接失败,直接退出,systemd 虽然会重启,但每次重启都赶上网络没好的窗口,结果陷入"启动-失败-重启"的循环,好几分钟后才连上。加上AUTOSSH_GATETIME=0之后,首次失败也会持续重试,问题消失。
第二个坑是StrictHostKeyChecking的交互。第一次连接新主机时,ssh 会问"是否信任这个主机指纹",在无人值守的服务里,这个交互会导致进程卡住。设成no可以跳过,但要注意这降低了安全性,只在你信任的网络环境里用。更稳妥的做法是提前用ssh-keyscan把对端指纹写进known_hosts,这样既免交互又保留了校验。
第三个坑最隐蔽:端口转发目标写成了127.0.0.1,但实际想转发的是对端另一台机器的服务。-L 3306:127.0.0.1:3306里的127.0.0.1是相对于对端而言的,也就是对端本机。如果你想通过跳板机访问它内网的另一台数据库,得写成-L 3306:10.0.0.5:3306,这个地址是对端视角能访问到的地址。这个理解偏差让我排查了很久,因为进程一切正常,就是连不上。
4.3 日志排查的正确姿势
出问题时,第一手信息永远是日志。systemd 环境下:
journalctl -u autossh-tunnel.service -f-f是实时跟踪,能看到 autossh 和 ssh 的输出。如果日志里出现Permission denied,多半是密钥或权限问题;出现Connection refused,是对端 sshd 没监听或者端口不对;出现remote port forwarding failed,是转发端口在对端被占用。把 autossh 的-v参数加上(verbose),能看到更详细的握手过程,排查认证类问题特别有用。
我一般会在调试阶段临时把ExecStart里的 autossh 换成带-v的版本,跑通了再改回去。因为 verbose 日志量大,长期开着会刷屏,不适合生产环境。
5. 让隧道更抗折腾的几个进阶设置
5.1 网络切换后的快速恢复
如果这台机器会在不同网络之间切换(比如笔记本、或者有双网卡的设备),网络切换的瞬间,原有的 ssh 连接会失效。这时候ServerAliveInterval的探测就显得很重要,它能在几十秒内发现连接已死并触发重连。但有个细节:网络切换后,本地路由变了,autossh 重连时如果还用旧的源地址,可能连不上。这种情况可以考虑在ExecStart里加上-o BindAddress=0.0.0.0,让它每次重连都重新选源地址。
5.2 防止隧道"假活"
前面提过ExitOnForwardFailure yes,这里再强调一次它的重要性。没有这个参数时,可能出现一种情况:ssh 进程在,autossh 认为一切正常,但实际上端口转发早就失效了,外部根本连不进来。这种"假活"状态最坑人,因为监控看不出来。加上这个参数后,转发一失败 ssh 就退出,autossh 立即重连,状态始终真实。
5.3 资源占用与重连频率的平衡
autossh 频繁重连会消耗 CPU 和网络,尤其是对端有连接数限制的时候,重连太猛可能被临时封禁。RestartSec=10和ServerAliveCountMax 3的组合,实际上把重连间隔控制在了合理范围。如果你的网络特别不稳定,可以适当调大ServerAliveCountMax,让它更"宽容"一些,避免因为短暂抖动就断开重连。
5.4 多隧道场景的组织方式
如果一台机器要维持多条隧道,不要把所有转发都塞进一个 autossh 命令里。我的做法是一条隧道一个 service 文件,命名上区分开,比如autossh-db.service、autossh-cache.service。这样某一条出问题不影响其他,排查时也清晰。虽然文件多了几个,但维护成本反而更低。
6. 从手动到自动:我的配置习惯总结
折腾 autossh 开机启动这件事,说到底就是把"人肉运维"变成"系统自愈"。我现在的习惯是:任何需要长期维持的隧道,第一件事就是写 systemd 单元文件,而不是先手动跑起来再说。因为手动跑的时候你会不自觉地用交互式的方式(比如输密码、比如依赖当前 shell 的环境变量),这些习惯一旦带到自动化配置里就是坑。
密钥认证、专用用户、AUTOSSH_GATETIME=0、ExitOnForwardFailure yes、Restart=always,这几个要素凑齐,基本就能做到"配一次,长期不用管"。剩下的就是偶尔看看日志,确认没有异常重连。真正稳定的隧道服务,应该是你几乎感觉不到它存在的——它就在那里,断了自己接上,重启自己爬起来,这才是配置到位了。
最后分享一个小技巧:在正式部署前,先用systemd-analyze verify检查一下单元文件有没有语法错误,能省掉不少"改了配置重启才发现写错"的时间。这个命令很多人不知道,但确实好用。