1. 起因:宽带欠费、路由器抽风、还是网卡休眠?
先说说我为什么写这玩意儿。家里宽带是那种“按天计费”的城中村套餐,欠费一秒立刻断网,但充值后线路恢复往往有延迟,而且路由器偶尔抽风,DHCP租约一到期就给我整个半死不活的假连接状态。更要命的是,我这台Ubuntu 22.04机器是当小型服务器用的,晚上挂机下载、跑定时任务,一旦断网没人发现,第二天起来任务全废,只能手动重启网络服务。
我试过几种“懒人方案”:用cron定时执行/etc/init.d/networking restart,但网络正常时重启会掉线,很蠢;装第三方网络管理工具,要么太重,要么要图形界面依赖。后来灵机一动:与其定时重启,不如“检测到断网才重连”,把判断逻辑交给脚本自己,状态正常时绝不动网络,只在真正失联时触发修复动作。这个思路后来证明是对的,整个过程用纯Shell实现,零依赖,跑了一个多月,稳定得很。
这篇就完整分享一下我的折腾记录:从检测原理、脚本写法、开机自启到防抖和日志告警,全套可以照抄。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心设计思路:如何判定“真断网”而不是“假掉线”
2.1 为什么不能只ping一次网关
新手最容易犯的错就是写一个死循环,每隔几秒ping -c 1 192.168.1.1,通了就继续,不通就重启网卡。这种做法有两个致命问题:
第一,网关这个地址就算你物理网线都拔了,某些路由器的管理芯片还是能响应ping的,因为它是CPU回应而不是线路转发。第二,WAN口断了但LAN口网关还活着,你能ping通网关,但上不了网。所以判断标准必须是“外网可达性”,不是“局域网可达性”。
我选择的检测目标是两个公共DNS:223.5.5.5(阿里)和119.29.29.29(腾讯)。选它们而不是8.8.8.8,是因为国内网络环境下,Google的DNS经常被运营商网络搞出高延迟或丢包,误报率太高。用国内DNS还有一个好处:就算运营商DNS劫持,这两个IP也基本稳。
2.2 三层判断:连通性、域名解析、默认路由
程序不能只会“能ping通就万事大吉”。我之前碰到过一个场景:能ping通223.5.5.5,但就是打不开网页,DNS解析全超时——这是运营商DNS挂了,或者路由器DNS转发异常。所以我的检测脚本分三层:
第一层,ping两个外网IP,连续两次都超时才判定网络异常(避免偶发丢包导致的误判)。
第二层,用dig或nslookup解析一个固定域名,解析失败说明DNS链路有问题,同样触发重连。这里我解析的是www.qq.com,这域名极其稳定,而且解析结果是多IP,比解析那种单IP小站更容易判断成功。
第三层,检查默认路由是否存在,ip route show如果没有default条目,说明网络压根没起来,直接进入恢复流程。
三层检测写在一个函数里,任何一个环节失败累计到阈值,就触发重联。好处是覆盖面广,坏处是脚本逻辑稍复杂,但实际跑下来误报率几乎为零。
2.3 恢复动作的顺序:先DNS后网卡再路由
断网恢复不是单一动作能解决的,我把修复动作按照“影响最小、成本最低”的顺序排:
第一步,清DNS缓存并重启systemd-resolved服务。很多“假断网”其实就是DNS缓存里的坏记录在捣乱,这一步能在几秒内解决大部分疑难杂症,而且对现有连接零影响。
第二步,dhclient -r释放租约再重新获取,或者直接nmcli networking off && nmcli networking on(如果机器用NetworkManager管理网络)。这一步能解决IP租约过期、网卡DHCP状态卡死的问题。
第三步,重启整个网络服务,systemctl restart networking,这招最暴力,但有时候前两步就是不行,只能靠它重置整个链路。
第四步,实在救不回来,就把默认网关重新添加一遍,ip route add default via 192.168.1.1 dev eth0,防止是路由表项被系统清掉了。
我把这四步按顺序放在一个函数里执行,每步之间停顿几秒,然后继续跑检测循环。实测下来,80%的情况在第一步就解决了,15%的情况需要用到第二步,真正走到第三步的很少,但脚本必须把路铺满,不然总有死角。
3. 脚本实现细节:一套可以直接抄作业的Shell版
3.1 脚本主框架与关键参数选择
先放完整脚本框架,我逐段解释:
bash复制#!/bin/bash
# 检测目标
TARGET_IP_1="223.5.5.5"
TARGET_IP_2="119.29.29.29"
DNS_HOST="www.qq.com"
# 判定阈值:连续失败次数达到该值才触发重联
MAX_FAIL=3
# 检测间隔(秒)
INTERVAL=30
# 日志文件
LOG_FILE="/var/log/network-reconnect.log"
# 失败计数
FAIL_COUNT=0
is_network_ok() {
# 第一层:ping外网IP
ping -c 2 -W 2 "$TARGET_IP_1" > /dev/null 2>&1
local p1=$?
ping -c 2 -W 2 "$TARGET_IP_2" > /dev/null 2>&1
local p2=$?
if [ $p1 -ne 0 ] && [ $p2 -ne 0 ]; then
return 1
fi
# 第二层:DNS解析
nslookup "$DNS_HOST" > /dev/null 2>&1
if [ $? -ne 0 ]; then
return 1
fi
# 第三层:默认路由
ip route show | grep -q "default"
if [ $? -ne 0 ]; then
return 1
fi
return 0
}
reconnect_network() {
echo "$(date '+%F %T') [INFO] 网络异常,开始恢复..." >> $LOG_FILE
# 第一步:刷新DNS
systemctl restart systemd-resolved
sleep 5
# 第二步:重置DHCP租约(NetworkManager版)
if command -v nmcli > /dev/null 2>&1; then
nmcli networking off
sleep 2
nmcli networking on
else
dhclient -r "$(ip route show | awk '/default/ {print $5; exit}')"
sleep 2
dhclient "$(ip route show | awk '/default/ {print $5; exit}')"
fi
sleep 5
# 第三步:重启networking服务
systemctl restart networking
sleep 5
# 第四步:重设默认路由(以实际网关为准)
local GW_IP=$(ip route show | awk '/default/ {print $3; exit}')
local GW_DEV=$(ip route show | awk '/default/ {print $5; exit}')
if [ -n "$GW_IP" ] && [ -n "$GW_DEV" ]; then
ip route del default 2>/dev/null
ip route add default via "$GW_IP" dev "$GW_DEV"
fi
echo "$(date '+%F %T') [INFO] 恢复流程执行完成" >> $LOG_FILE
}
while true; do
if is_network_ok; then
if [ $FAIL_COUNT -ne 0 ]; then
echo "$(date '+%F %T') [INFO] 网络已恢复,重置失败计数" >> $LOG_FILE
FAIL_COUNT=0
fi
else
FAIL_COUNT=$((FAIL_COUNT + 1))
echo "$(date '+%F %T') [WARN] 检测失败,第 ${FAIL_COUNT} 次" >> $LOG_FILE
if [ $FAIL_COUNT -ge $MAX_FAIL ]; then
reconnect_network
FAIL_COUNT=0
# 恢复后等待一段时间再继续检测,避免刚启动立即误判
sleep 30
fi
fi
sleep $INTERVAL
done
核心参数里最有讲究的是两个:MAX_FAIL=3和INTERVAL=30。这两个值决定了脚本对“偶发抖动”和“真断网”的区分灵敏度。如果间隔太短、阈值太低,运营商一个瞬时丢包就会触发重启网络服务,反而把稳定连接搞断了;如果间隔太长、阈值太高,断网恢复的延迟太大,失去自动修复的意义。
我实测下来,30秒检测一次、连续3次失败(即大约90秒确认断网)是个甜点位。低于60秒确认的,偶尔会误伤正常网络;超过3分钟的,遇到故障时等待太折磨。你可以根据自己的容忍度调整,但我建议别把INTERVAL调到低于15秒,日志刷得太快,而且脚本自己占用的系统资源也不划算。
3.2 防误报设计:三次确认与恢复后冷却
这个脚本最关键的保护机制就是“三次确认”和“恢复后冷却”。三次确认的逻辑上面说了,我补一个实际场景:光猫的PON光纤稍微弯折一下,可能丢包百分之几,但网络其实没断。如果只测一次就触发重连,那脚本就成了网络杀手,自己把自己的WAN口折腾断了。这种“宁可慢三分钟,不可错杀一次”的设计思路,就是防呆。
恢复后冷却是我后来加上的。最初版脚本在重连成功之后立刻进入正常30秒检测循环,结果遇到了一个奇葩问题:重连刚完成,机器还没拿到新的DHCP租约,脚本检测又失败,然后再次触发重连,形成死循环,日志刷了一屏。解决办法就是在reconnect_network之后强制sleep 30,这个时间给DHCP和路由完全初始化,别让脚本“踩自己的脚”。
3.3 日志的细节:时间戳、级别、上下文
日志虽然简单,但格式要统一,方便排查。我用的格式是[时间] [级别] 信息。级别分INFO和WARN两种——INFO记录正常恢复流程,WARN记录检测失败。这么做的好处是排查问题时能立刻看出剧本走向:
先有3条WARN说明检测异常,然后接INFO说明触发恢复,再看接下来的WARN和INFO就知道这次恢复有没有生效。如果日志里WARN和INFO反复交替,说明故障没有真正解决,脚本在空转,这时候就该人工介入了。
不过我后来发现,轮询写日志有个小坑:日志文件会用满磁盘。一台跑了几天的机器,如果每次都把成功检测记录也写进去,日志文件会膨胀到几百兆。我的最终版本只记录状态变化,不在正常时刷日志,这能极大减少I/O压力,也让日志更可读。
4. 开机自启与守护:systemd让脚本跑得更安心
4.1 systemd服务文件写法
如果你只把脚本丢在后台用nohup跑,重启机器就没了,这显然不行。我用systemd把它做成一个常驻服务,开机自动启动,崩溃自动拉起,还能看状态。
先创建服务文件:
ini复制[Unit]
Description=Auto Network Reconnect
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/auto-reconnect.sh
Restart=always
RestartSec=30
User=root
[Install]
WantedBy=multi-user.target
这里有两个关键点:After=network-online.target保证系统网络就绪后才启动脚本,不然脚本一启动就检测失败,触发一轮无意义的“重连”流程;Restart=always保证脚本意外退出后30秒自动重启,相当于脚本自己也有“看门狗”。
然后执行:
bash复制sudo cp auto-reconnect.sh /usr/local/bin/
sudo chmod +x /usr/local/bin/auto-reconnect.sh
sudo cp auto-reconnect.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable auto-reconnect.service
sudo systemctl start auto-reconnect.service
查看状态用systemctl status auto-reconnect.service,跟踪日志用journalctl -u auto-reconnect.service -f。
4.2 为什么不用cron替代systemd
有朋友看到这里会问,不是cron也能每分钟跑一次脚本吗?确实可以,但cron有个问题:如果你的脚本执行时间超过一分钟,cron会叠加启动下一个实例,导致并发冲突。而且cron最小粒度是一分钟,把检测延迟拉高了很多。systemd解决并发问题(如果上一个实例没退出,新实例不会启动),还能看状态,能拉起崩溃的进程,明显更适合这种常驻型任务。
4.3 权限问题的坑:为什么必须用root
脚本里重启systemd-resolved、networking服务、修改路由表,这些操作都需要root权限。如果你不在systemd服务里指定User=root(默认就是root),那脚本跑起来就会不停报权限不足。
我在测试过程中试过用普通用户跑,结果卡在systemctl restart networking这一步,服务拒绝了。想想也是合理的:这种脚本本来就是系统级的“自救”操作,没有管理员权限根本做不了。建议你把脚本权限设为700,服务用root跑,安全性和功能性都兼顾到了。
5. 进阶扩展:多网卡、IPv6与远程告警
5.1 多网卡环境的适配思路
如果你的机器有多个网卡(比如有线+无线,或者多个有线口),那你不能直接照抄上面的脚本,因为ip route show | awk '/default/ {print $5; exit}'只会拿到主路由的出口网卡名,副网卡断连它根本不管。
我的建议:检测函数里遍历所有非虚拟网卡,分别检查每个网卡是否有可用IP、能否ping通网关。恢复动作也拆分成“按网卡恢复”,哪个网卡断了对它单独执行ip link set <interface> down && up。但说实话,家用场景极少遇到多网卡高可用需求,如果你真的碰到,建议把这个脚本改成Systemd服务多实例,每个网卡跑一个。
5.2 IPv6断网检测
很多人家里现在有IPv6,如果主网络依赖IPv6访问某些资源,那么检测IPv6也很有必要。脚本里加一个IPv6外网地址检测即可:
bash复制ping -6 -c 2 -W 2 2400:3200::1 > /dev/null 2>&1
这里2400:3200::1是阿里的公共IPv6 DNS,响应很稳定。不过需要注意:如果IPv6检测失败但IPv4正常,是不是要触发重连?这取决于你的网络依赖。如果你完全用IPv4,那IPv6失败就不用管;如果部分访问依赖IPv6,你可以把IPv6失败单列计数,达到阈值执行一次轻量级恢复(比如只重启radvd相关服务)。我的建议是初次实现先只关注IPv4,IPv6的复杂性留给后续。
5.3 断网告警:发邮件、推送到手机
光自动重连还不够,万一重连失败,你得知道“家里断网了”这件事。我自用的版本集成了一个简单的告警函数:在MAX_FAIL达到之后尝试重连,如果重连完检测还是失败(连续两次重连仍失败),就发一封邮件到指定邮箱。
发邮件我用的是mailutils里的mail命令,配合SMTP的配置:
bash复制echo "网络自动重连失败,请检查路由器或宽带状态" | mail -s "【断网告警】$(date '+%F %T')" your-email@example.com
如果你不习惯用邮件,也可以用curl推送微信测试号、Server酱、钉钉机器人Webhook,道理都一样——脚本检测到重连失败,调用外部HTTP接口发通知。我个人的偏好是Server酱,因为手机推送延迟低,而且部署成本几乎为零。
5.4 与NetworkManager还是netplan的兼容问题
这个坑很关键,不同Ubuntu版本管理网络的方式不一样,脚本必须要兼容:
- Ubuntu 22.04及以前,如果用的是
netplan(默认),底层可能走systemd-networkd或NetworkManager,取决于你的配置。 - Ubuntu 24.04开始,netplan成了唯一标准,但底层还是两种后端可选。
我的脚本在恢复流程里做了“双保险”:先用nmcli networking off/on,如果系统没有NetworkManager就自动跳到dhclient释放/续租的老路子,最后再重启networking服务。这个顺序在22.04和24.04上都测过,没有致命冲突。不过有一点必须提醒:如果你的服务器用了虚拟机桥接模式(比如VMware或VirtualBox的NAT),那网卡行为跟物理机有些区别,我之前在KVM虚拟机里跑这个脚本,发现nmcli的操作不影响systemd-networkd管理下的网卡状态,最后是改成指定网卡ifconfig down/up的方式才生效。
6. 常见故障排查:脚本正常但网络还是不稳定?
6.1 排查日志与诊断命令速查
如果脚本跑着但还是断网,第一步永远不是改脚本,而是用命令行确认当前网络状态。我整理了一个快速排查顺序:
bash复制# 网卡物理状态
ip link show
# 是否有IP地址
ip addr show
# 默认路由是否存在
ip route show
# DNS配置
cat /etc/resolv.conf
# 连通性测试
ping -c 4 223.5.5.5
nslookup www.qq.com
把上面四条输出比对一下,基本能定位故障点。有一次我的脚本一直报检测失败,但ip link显示网卡正常,ip addr也有地址,最后发现是/etc/resolv.conf被某个软件改成了只写了127.0.0.53,而systemd-resolved没有配好上游DNS,所有域名解析都走回了本地循环,不超时才怪。
6.2 典型故障现象与对应解决方案
我把实际踩过的坑整理成一张表:
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| ping外网IP通,但域名解析失败 | systemd-resolved服务卡死或DNS配置异常 | 重启systemd-resolved,检查/etc/resolv.conf是否有有效DNS |
| 网卡显示有IP,但网关ping不通 | 路由器LAN侧端口故障或网卡协商错误 | 检查网线/光猫LAN口,用ethtool eth0查看协商速率 |
| 默认路由消失 | 接口被其他服务重置、netplan配置失误 | 手动ip route add default via <网关>,并检查netplan配置 |
| 能ping通网关,外网全不通 | 运营商链路中断或光猫注册异常 | 登录光猫后台查PON状态,重启光猫 |
| 重连后仍持续失败 | DHCP服务器地址池耗尽 | 登录路由器查租约列表,减少固定设备绑定的长租期 |
6.3 脚本误触发的“自杀”场景与防御
讲一个真实案例:我在脚本里加了定时检测,但没有做“重复恢复”保护。有一天运营商线路抖动,线路反复恢复切断,脚本就跟着反复重连,高峰期一小时重启了十几次网络服务。这不光没解决问题,反而搞得路由器反感,直接给机器断开了。
后来我在脚本里加了个“重连冷却期”改装:短时间内如果恢复次数超过3次,就暂停自动重连2小时,只告警不动作。这个设计是必须的,它能防止机器在链路不稳的情况下把自己折腾到完全离线。
6.4 如果不想写脚本:现成方案对比
如果你不想折腾Shell脚本,市面上也有现成替代品:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 路由器自带WAN口自动拨号功能(大部分家用路由) | 不需要改服务器,运营商PPPoE断线自动重拨 | 只解决拨号层,不解决应用层断网 |
| Netwatch脚本(MikroTik RouterOS) | 功能强大可以联动动作 | 需要专用路由器硬件,一般用户接触不到 |
| Cron + watchdog命令 | 一分钟粒度足够大部分场景 | 并发和状态管理不如systemd |
| 第三方网络检测软件(如Monit) | 功能全,有web界面 | 安装配置复杂,资源消耗大 |
对于大部分Ubuntu玩家,还是我的Shell方案最亲民:零依赖、逻辑透明、改起来方便。你也可以在脚本里加点自定义逻辑,比如断网时自动切换到一个4G USB网卡的默认路由,这种玩法就属于个人定制了。
7. 写在最后的体会
这一个月跑下来,最大的收获不是脚本本身,而是一套“自动自救”的思维模式。网络这东西,物理线路、运营商、路由器、DNS、网卡驱动全都有可能掉链子,指望一个脚本解决所有问题不现实,但让它把90%的“半瘫”状态拉回正常是完全可以做到的。
我个人现在最深的体会是:脚本越简单越不容易坏,判断越保守越不容易误伤。就算你把我上面的脚本抄走了,也建议你自己把网络环境琢磨一遍——你的网关是固定IP还是DHCP?有没有用IPv6?DNS是否被运营商强制劫持?这些答案不同,脚本里的参数就得跟着调。
另外提醒一句:这个脚本适合个人服务器、家庭内网这种规模,如果你是在公司生产环境用,建议还是升级成专业网络监控(比如Prometheus的黑盒监控,配合自动化运维工具),毕竟生产环境断网自动重启的代价,有时候比断网本身还大。
