1. 问题背景
在日常的异地组网或家庭实验室(Home Lab)互联中,我们经常使用群晖(Synology NAS)的内置 VPN 客户端拨号到另一个家里的路由器(如 MikroTik RouterOS 或其他支持 L2TP 的设备)。
由于家庭宽带的公网 IP 通常是动态的(Dynamic IP),我们一般会使用 DDNS(动态域名解析)来绑定对端路由器。但在实际使用中遇到了一个痛点:
- 现象:当对端路由器的公网 IP 发生变化时,群晖的 L2TP 连接会断开。
- 原因:群晖底层的 VPN 守护进程(
xl2tpd/pppd)在重连时死死缓存了初次解析的旧 IP,并不会重新发起 DNS 查询,导致即使 DDNS 已经更新,群晖也无法自动恢复连接,处于“僵死”状态。
为了实现无人值守的稳定性,我们需要一个自动检测、清理 DNS 缓存并强制重连的自动化方案。
2. 核心解决方案
既然群晖内置机制不会自动刷新 DNS,我们利用群晖自带的 任务计划(Task Scheduler),定期运行一个 Shell 脚本。
脚本的逻辑非常直接:
- 定时(如每 5 分钟)检测对端 VPN 网关的连通性。
- 如果连通性正常,直接退出。
- 如果检测到断开,强制终止当前 VPN 客户端进程,重启系统 DNS 缓存服务(nscd) 释放旧 IP,然后读取配置文件重新拨号,并将全过程记录到指定的日志文件中。
3. 详细配置步骤
步骤一:获取群晖 VPN 配置文件 ID
群晖的 VPN 拨号是基于配置文件的,我们需要通过 SSH 获取该配置文件的唯一 ID。
- 开启群晖的 SSH 功能(控制面板 -> 终端机和 SNMP)。
- 使用终端工具登录群晖,并切换至
root权限:
sudo -i
- 查看 L2TP 配置文件目录:
ls /usr/syno/etc/synovpnclient/l2tp/
- 你会看到类似
l2tp_o1634567890.conf的文件。记录下其中的l2tp_o1634567890(注意:每个人生成的数字不同),这就是后续脚本中需要的VPN_ID。
步骤二:准备自动化脚本
在群晖的任意共享文件夹中(例如 public),创建一个日志文件,以便后续观察(如 /volume1/public/l2tp_reconnect.log)。
核心 Shell 脚本内容如下:
#!/bin/bash
# 群晖 L2TP 客户端自动重连(加固版 v2)
# 适用 DSM 7.x,任务计划中须以 root 运行
# ================= 基础配置 =================
VPN_ID="l2tp_o1634567890" # SSH: ls /usr/syno/etc/synovpnclient/l2tp/ 获取
VPN_GATEWAY="10.255.30.1" # 隧道对端地址(ip addr 里 peer 后面那个)
VPN_HOST="your.ddns.net" # 对端 DDNS 域名,仅用于日志记录解析结果
LOG_FILE="/volume1/public/l2tp_reconnect.log"
PROBE_RETRY=3 # 连续失败几次才判定断线
PROBE_INTERVAL=10 # 每次探测间隔(秒)
CONNECT_TIMEOUT=60 # synovpnc connect 最长等待(秒)
WAIT_UP=60 # 拨号后等待隧道建立的最长时间(秒)
# ===========================================
STATE_FILE="/usr/syno/etc/synovpnclient/vpnc_connecting"
LOCK_FILE="/tmp/l2tp_reconnect.lock"
# ---- 防重入:上一轮没跑完就直接退出,避免打断正在进行的握手 ----
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
exit 0
fi
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; }
# 隧道存活判断:去往对端的路由走 ppp 接口(隧道存在)+ 对端可达(隧道可用)
# 不写死接口名——群晖分配的 ppp 单元号是动态的(可能是 ppp300 这类编号)
tunnel_up() {
ip route get "$VPN_GATEWAY" 2>/dev/null | grep -q " dev ppp" || return 1
ping -c 2 -W 2 "$VPN_GATEWAY" > /dev/null 2>&1
}
# ---- 探测阶段:连续多次失败才认定断线,过滤瞬时丢包 ----
for i in $(seq 1 "$PROBE_RETRY"); do
if tunnel_up; then
exit 0 # 正常则静默退出;想留心跳日志在此行前加 log
fi
[ "$i" -lt "$PROBE_RETRY" ] && sleep "$PROBE_INTERVAL"
done
log "[WARN] 连续 ${PROBE_RETRY} 次探测失败,开始重连流程"
# ---- 清理阶段:进程 + 残留状态文件一起清,防止 UI 卡在“正在连接中” ----
/usr/syno/bin/synovpnc kill_client > /dev/null 2>&1
sleep 3
pkill -f xl2tpd > /dev/null 2>&1
pkill -f "pppd.*${VPN_ID}" > /dev/null 2>&1 # 只杀本客户端的 pppd,避免误伤 VPN Server
sleep 2
rm -f "$STATE_FILE"
# ---- 刷新 DNS 缓存,让 pppd 重新解析 DDNS ----
synosystemctl restart nscd > /dev/null 2>&1 || log "[WARN] nscd 重启失败(可忽略)"
sleep 3
RESOLVED=$(getent hosts "$VPN_HOST" | awk '{print $1}' | head -1)
log "[INFO] ${VPN_HOST} 当前解析为: ${RESOLVED:-解析失败}"
# ---- 重新拨号,超时强制结束,绝不允许无限期挂住 ----
timeout "$CONNECT_TIMEOUT" /usr/syno/bin/synovpnc connect --id="$VPN_ID" >> "$LOG_FILE" 2>&1
RC=$?
[ "$RC" -eq 124 ] && log "[ERROR] connect 命令超时被强制终止"
# ---- 验证结果 ----
DEADLINE=$((SECONDS + WAIT_UP))
while [ "$SECONDS" -lt "$DEADLINE" ]; do
if tunnel_up; then
log "[OK] 隧道已恢复(对端公网 IP: ${RESOLVED:-unknown})"
exit 0
fi
sleep 5
done
# ---- 失败兜底:必须清掉状态文件,否则 DSM 界面会永久卡在“正在连接中” ----
log "[ERROR] ${WAIT_UP}s 内未建立隧道,清理残留状态,等待下一轮重试"
/usr/syno/bin/synovpnc kill_client > /dev/null 2>&1
sleep 2
rm -f "$STATE_FILE"
exit 1
步骤三:在群晖控制面板中设置计划任务
- 登录群晖 DSM 后台,打开 控制面板 -> 任务计划。
- 点击 新增 -> 计划的任务 -> 用户定义的脚本。
- 常规设置:
- 任务名称:
Auto-Reconnect-L2TP - 用户账号:必须选择
root(重启网络服务需要最高权限)。
- 计划设置:
- 运行频率:设置为每天运行,每隔 5 分钟 运行一次(可根据对实时性的要求微调)。
- 任务设置:
- 在“运行命令”文本框中,将上面的脚本粘贴进去。
- 点击 确定 保存。
4. 调试与效果验证
脚本部署完成后,我们可以通过以下方式进行验证与日常维护:
- 手动触发测试:在任务计划列表中选中该任务,点击“运行”。随后去你的共享文件夹下查看
l2tp_reconnect.log,如果网络正常,会看到[OK] VPN 隧道连接正常。的字样。 - 断网重连测试:可以故意拔掉对端路由器的 WAN 口网线或等待对端 IP 发生变化。脚本在下次执行时,日志会记录
[WARNING] VPN 网络异常断开...,随后刷新 DNS 并重新拨号。在 10 秒左右即可在群晖“网络”界面看到 VPN 重新变成了已连接状态。
通过这种“网络状态探针 + 强刷 DNS + 重新拨号”的组合拳,完美解决了群晖 L2TP 客户端在动态 IP 环境下的断流死锁问题,大大提升了异地组网的可用性。
标签:#群晖 #Synology #L2TP #VPN #动态IP #DDNS #自动化脚本 #网络运维