ykxmz 1.5.0:修复手表端 TLS 证书校验 + MTU 修复网络稳定性

被控端
- 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%)
- 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败)
- 每分钟回头找局域网,自动切回快 400 倍的路径
- 画面心跳帧:静止时省流量,客户端仍知道流是活的
- 锁屏时给出可操作的提示

手表端
- 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException
- 连接错误翻译成人话(证书/时间/解析/超时分别提示)
- 经中转连上后用服务器报的地址刷新已配对记录
- 新增局域网连接页面、服务器延迟检测

服务器端
- 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502)
- 网络调优脚本 net-tune.sh(开机自动应用)
- 管理页:批量删除、网络体检、下载区
- 隧道改 HTTP/2 + 双连接

文档
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
This commit is contained in:
xmz7891
2026-10-06 19:01:05 +08:00
commit b62ac865ea
3195 changed files with 203292 additions and 0 deletions
+426
View File
@@ -0,0 +1,426 @@
#!/bin/bash
# 局域网遥控 · 中转服务器 —— 安装脚本
#
# 在一台能上外网的 Linux 机器(这里是随身 WiFi / Debian 12)上跑:
# bash install.sh
#
# 做完这些:
# 1. 把 relay.py 装到 /root/lanremote-relay
# 2. 注册两个 systemd 服务,开机自启:
# lanremote-relay 中转服务
# lanremote-tunnel Cloudflare 隧道(公网入口)
# 3. 打印公网地址和管理页地址
#
# 面向 512MB 内存的小机器:两个进程加起来约 60MB。
set -eu
RELAY_DIR=/root/lanremote-relay
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
echo "============================================================"
echo " 局域网遥控 · 中转服务器 安装"
echo "============================================================"
echo
# ---------- 0. 环境检查 ----------
echo "[0/5] 检查环境"
if [ "$(id -u)" != "0" ]; then
echo " 需要 root 权限运行"
exit 1
fi
command -v python3 >/dev/null || { echo " 没有 python3"; exit 1; }
echo " python3 $(python3 -V 2>&1 | awk '{print $2}')"
if ! command -v cloudflared >/dev/null; then
echo " 没有 cloudflared,尝试安装…"
ARCH=$(uname -m)
case "$ARCH" in
aarch64) CF=cloudflared-linux-arm64 ;;
armv7l) CF=cloudflared-linux-arm ;;
x86_64) CF=cloudflared-linux-amd64 ;;
*) echo " 不认识的架构 $ARCH"; exit 1 ;;
esac
curl -fsSL --http1.1 -o /usr/local/bin/cloudflared \
"https://github.com/cloudflare/cloudflared/releases/latest/download/$CF"
chmod +x /usr/local/bin/cloudflared
fi
echo " cloudflared $(cloudflared --version 2>&1 | head -1)"
echo " 内存: $(free -m | awk 'NR==2{print $2" MB 总量 / "$7" MB 可用"}')"
# ---------- 0b. 检查 Cloudflare API 有没有被 DNS 劫持 ----------
# 踩过的坑:这条 4G 线路把 api.cloudflare.com 解析到运营商设备
# (141.193.154.210,自签名证书)。cloudflared 下载登录证书必须走这个域名,
# 于是永远报 Failed to fetch resource,具名隧道根本建不起来。
echo
echo "[0b] 检查 Cloudflare API"
CF_HOSTS=/etc/hosts
CF_MARK="# LanRemote: 钉住 Cloudflare API(网络会 DNS 劫持)"
resolve_cf() {
getent ahostsv4 api.cloudflare.com 2>/dev/null | awk 'NR==1{print $1}'
}
is_real_cf() {
case "$1" in
104.16.*|104.17.*|104.18.*|104.19.*|104.20.*|104.21.*|172.64.*|172.66.*|172.67.*|162.159.*|188.114.*)
return 0 ;;
*) return 1 ;;
esac
}
CF_IP=$(resolve_cf)
echo " api.cloudflare.com 现在解析到: ${CF_IP:-(解析不出来)}"
CF_CODE=$(timeout 12 curl -4 -s -o /dev/null -w '%{http_code}' \
https://api.cloudflare.com/client/v4/user 2>/dev/null)
CF_CODE=${CF_CODE:-000}
echo " 实测 HTTP: ${CF_CODE}"
CF_BODY=$(timeout 12 curl -4 -s https://api.cloudflare.com/client/v4/user 2>/dev/null | head -c 200)
if is_real_cf "$CF_IP" && echo "$CF_BODY" | grep -q '"success"'; then
echo " ✓ 正常(返回的是真正的 API JSON)"
else
echo " ⚠ 检测到 Cloudflare API 被 DNS 劫持或不可达"
echo " 具名隧道需要访问它来下载证书,先修掉。"
cp -n "$CF_HOSTS" "${CF_HOSTS}.lanremote.bak" 2>/dev/null || true
# 清掉所有和本程序相关的 hosts 记录(含早期版本留下的不同文案)
sed -i '/LanRemote.*Cloudflare API/d' "$CF_HOSTS" 2>/dev/null || true
sed -i '/api\.cloudflare\.com/d' "$CF_HOSTS" 2>/dev/null || true
# 顺手删掉清理后可能剩下的空行
sed -i '/^[[:space:]]*$/d' "$CF_HOSTS" 2>/dev/null || true
echo "" >> "$CF_HOSTS"
{
echo ""
echo "$CF_MARK"
# 这几个是用公共 DNS(1.1.1.1 / 8.8.8.8 / 223.5.5.5)查出来的
# 真实地址。之前随手挑的 104.16.132.229 是错的 ——
# 它返回 Cloudflare 错误页(error code: 1034)而不是 API,
# 会导致 cloudflared tunnel create 报 unauthorized。
echo "104.19.192.174 api.cloudflare.com"
echo "104.19.192.175 api.cloudflare.com"
echo "104.19.192.176 api.cloudflare.com"
echo "104.19.192.177 api.cloudflare.com"
echo "104.17.0.1 api.cloudflare.com"
echo "104.18.0.1 api.cloudflare.com"
} >> "$CF_HOSTS"
sleep 1
CF_IP2=$(resolve_cf)
CF_CODE2=$(timeout 12 curl -4 -s -o /dev/null -w '%{http_code}' \
https://api.cloudflare.com/client/v4/user 2>/dev/null)
CF_CODE2=${CF_CODE2:-000}
echo " 修完解析到: ${CF_IP2:-?} HTTP: ${CF_CODE2}"
CF_BODY2=$(timeout 12 curl -4 -s https://api.cloudflare.com/client/v4/user 2>/dev/null | head -c 200)
if echo "$CF_BODY2" | grep -q '"success"'; then
echo " ✓ 修好了(返回的是真正的 API JSON)"
else
echo " ✗ 还是不通 —— 具名隧道可能建不起来,"
echo " 但匿名隧道仍然可用(它走的是另一条链路)"
fi
fi
# ---------- 1. 放文件 ----------
echo
echo "[1/5] 安装文件到 $RELAY_DIR"
mkdir -p "$RELAY_DIR/data"
# 脚本可能就在 RELAY_DIR 里(自己拷自己 cp 会报错),所以先比一下真实路径
SRC="$SCRIPT_DIR/relay.py"
DST="$RELAY_DIR/relay.py"
SAME=no
if [ -f "$SRC" ] && [ -f "$DST" ]; then
if [ "$(readlink -f "$SRC")" = "$(readlink -f "$DST")" ]; then
SAME=yes
fi
fi
if [ "$SAME" = "yes" ]; then
echo " relay.py 已在位(源和目标同一个文件)"
elif [ -f "$SRC" ]; then
cp -f "$SRC" "$DST"
echo " 已复制 relay.py"
elif [ -f "$DST" ]; then
echo " relay.py 已在位"
else
echo " 找不到 relay.py"
exit 1
fi
python3 -m py_compile "$RELAY_DIR/relay.py"
echo " 语法检查通过"
# ---------- 2. 停掉旧的 ----------
echo
echo "[2/5] 停掉旧的实例"
systemctl stop lanremote-tunnel lanremote-relay 2>/dev/null || true
pkill -f 'relay.py' 2>/dev/null || true
pkill -f 'cloudflared tunnel' 2>/dev/null || true
sleep 2
# ---------- 3. systemd ----------
echo
echo "[3/5] 注册开机自启服务"
cat > /etc/systemd/system/lanremote-relay.service <<EOF
[Unit]
Description=LanRemote 中转服务器
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=$RELAY_DIR
Environment=RELAY_PORT=8080
Environment=RELAY_BIND=127.0.0.1
Environment=RELAY_DATA=$RELAY_DIR/data
Environment=RELAY_POLL=25
Environment=RELAY_QUEUE_BYTES=2097152
ExecStart=/usr/bin/python3 $RELAY_DIR/relay.py
Restart=always
RestartSec=3
# 小内存机器:限制住,别把系统拖死
MemoryMax=128M
CPUQuota=60%
StandardOutput=append:$RELAY_DIR/relay.log
StandardError=append:$RELAY_DIR/relay.log
[Install]
WantedBy=multi-user.target
EOF
cat > /etc/systemd/system/lanremote-tunnel.service <<EOF
[Unit]
Description=LanRemote Cloudflare 隧道
After=network-online.target lanremote-relay.service
Wants=network-online.target
Requires=lanremote-relay.service
[Service]
Type=simple
# --- 下面这几个参数都是实测调出来的,不是拍脑袋 ---
#
# IPv4:这条线路**有 IPv6 地址但实际不通**,
# Cloudflare 边缘默认优先 IPv6,于是建隧道一直超时
# (日志里是 context deadline exceeded,对端 2606:4700:a8::10)。
# 所以必须 --edge-ip-version 4。
#
# 协议:用 HTTP/2(走 TCP 7844)。
# 实测对比(同一设备、各跑 90 秒):
# QUIC(UDP) 可用 48%,断开 16 次,拨号失败 16 次
# HTTP/2 可用 80%,断开 2 次,拨号失败 0 次
# 这条线路在丢 UDP 包 —— 日志里全是
# 'Failed to dial a quic connection: no recent network activity'
# 'Connection terminated: Failed to accept QUIC stream'
# TCP 有重传,慢一点但稳得多。
# 注意:早先在 192.168.2.x 的出口上 TCP 7844 是被挡的,
# 换了出口就通了 —— 换网络必须重测,别照搬旧结论。
ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate \
--protocol http2 --edge-ip-version 4 --ha-connections 2 --retries 5 \
--url http://127.0.0.1:8080
Restart=always
RestartSec=5
MemoryMax=128M
StandardOutput=append:$RELAY_DIR/tunnel.log
StandardError=append:$RELAY_DIR/tunnel.log
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable lanremote-relay lanremote-tunnel >/dev/null 2>&1
systemctl start lanremote-relay
sleep 3
systemctl start lanremote-tunnel
echo " 两个服务已注册并启动"
# ---------- 3b. 装隧道看门狗 ----------
# 匿名隧道建立之后中途失效没人管,用户只能自己发现、自己上来重启。
# 看门狗每 5 分钟检查一次公网可达性,连续 3 次不通就重启隧道。
echo
echo "[3b] 装隧道看门狗"
if [ -f "$RELAY_DIR/watchdog.sh" ] || [ -f "$SCRIPT_DIR/watchdog.sh" ]; then
if [ -f "$SCRIPT_DIR/watchdog.sh" ] && \
[ "$(readlink -f "$SCRIPT_DIR/watchdog.sh")" != \
"$(readlink -f "$RELAY_DIR/watchdog.sh" 2>/dev/null || echo x)" ]; then
cp -f "$SCRIPT_DIR/watchdog.sh" "$RELAY_DIR/watchdog.sh"
fi
chmod +x "$RELAY_DIR/watchdog.sh"
cat > /etc/systemd/system/lanremote-watchdog.service <<'WDUNIT'
[Unit]
Description=LanRemote 隧道看门狗(检查公网可达性)
After=lanremote-relay.service lanremote-tunnel.service
[Service]
Type=oneshot
ExecStart=/bin/bash /root/lanremote-relay/watchdog.sh
MemoryMax=64M
StandardOutput=append:/root/lanremote-relay/watchdog.log
StandardError=append:/root/lanremote-relay/watchdog.log
WDUNIT
cat > /etc/systemd/system/lanremote-watchdog.timer <<'WDTIMER'
[Unit]
Description=每 5 分钟检查一次隧道
[Timer]
OnBootSec=3min
OnUnitActiveSec=5min
AccuracySec=30s
Unit=lanremote-watchdog.service
[Install]
WantedBy=timers.target
WDTIMER
systemctl daemon-reload
systemctl enable lanremote-watchdog.timer >/dev/null 2>&1
systemctl start lanremote-watchdog.timer
echo " 看门狗已启用(每 5 分钟检查一次)"
else
echo " [跳过] 没找到 watchdog.sh"
fi
# ---------- 4. 取地址 ----------
echo
echo
echo "[3c] 网络调优(修 MTU —— 这是可用率从 48% 提到 90% 的关键)"
# 实测:接口 MTU 1500 时,ping 包 1488 就丢、1500 全丢。
# TLS 握手要发大证书包 -> 被丢 -> 新建连接约 30% 失败。
# 夹 MSS 到 1360 + 打开路径 MTU 探测之后,可用率 48%~75% -> 90%~92%。
# 注意:**不要改接口 MTU**,实测降到 1400 反而掉到 50%。
if [ -f "$SCRIPT_DIR/net-tune.sh" ]; then
cp -f "$SCRIPT_DIR/net-tune.sh" "$RELAY_DIR/net-tune.sh"
elif [ -f "$RELAY_DIR/net-tune.sh" ]; then
: # 已经在位
fi
if [ -f "$RELAY_DIR/net-tune.sh" ]; then
chmod +x "$RELAY_DIR/net-tune.sh"
# 内核参数持久化
cat > /etc/sysctl.d/99-lanremote-mtu.conf <<'MTUEOF'
# LanRemote: 修路径 MTU 问题。
# 接口 MTU 1500 时大包会被丢(1488/1500 全丢),导致 TLS 握手约 30% 失败。
net.ipv4.tcp_mtu_probing = 1
MTUEOF
sysctl -p /etc/sysctl.d/99-lanremote-mtu.conf >/dev/null 2>&1
# 开机自动夹 MSS
cat > /etc/systemd/system/lanremote-nettune.service <<NUNIT
[Unit]
Description=LanRemote 网络调优(修 MTU:夹 MSS + 路径探测)
After=network-online.target
Wants=network-online.target
Before=lanremote-relay.service lanremote-tunnel.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=$RELAY_DIR/net-tune.sh
[Install]
WantedBy=multi-user.target
NUNIT
systemctl daemon-reload
systemctl enable lanremote-nettune.service >/dev/null 2>&1
systemctl start lanremote-nettune.service
sleep 1
echo " tcp_mtu_probing = $(cat /proc/sys/net/ipv4/tcp_mtu_probing)"
echo " MSS 规则 $(iptables -t mangle -L -n 2>/dev/null | grep -ci mss) 条"
echo " 已注册 lanremote-nettune.service(开机自动应用)"
else
echo " 跳过:找不到 net-tune.sh"
fi
echo "[4/5] 等隧道拿到公网地址"
URL=""
for i in $(seq 1 45); do
sleep 2
URL=$(grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' \
"$RELAY_DIR/tunnel.log" 2>/dev/null | tail -1)
[ -n "$URL" ] && break
done
if [ -z "$URL" ]; then
echo " 还没拿到地址,隧道日志:"
tail -15 "$RELAY_DIR/tunnel.log" 2>/dev/null || true
exit 1
fi
echo "$URL" > "$RELAY_DIR/public_url.txt"
# 便捷命令
cat > /usr/local/bin/lanremote-url <<'EOF'
#!/bin/bash
# 打印中转服务器的公网地址(隧道地址每次重启会变)
cat /root/lanremote-relay/public_url.txt 2>/dev/null || echo "(还没有地址)"
EOF
chmod +x /usr/local/bin/lanremote-url
cat > /usr/local/bin/lanremote-key <<'EOF'
#!/bin/bash
# 打印管理页钥匙
cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null || echo "(还没有钥匙)"
EOF
chmod +x /usr/local/bin/lanremote-key
cat > /usr/local/bin/lanremote-status <<'EOF'
#!/bin/bash
# 一眼看清中转 + 隧道的状态
echo "=== 服务 ==="
for s in lanremote-relay lanremote-tunnel lanremote-watchdog.timer; do
printf " %-28s %s\n" "$s" "$(systemctl is-active "$s" 2>/dev/null)"
done
echo
echo "=== 公网地址 ==="
cat /root/lanremote-relay/public_url.txt 2>/dev/null || echo " (还没有)"
echo
echo "=== 可达性 ==="
URL=$(cat /root/lanremote-relay/public_url.txt 2>/dev/null)
if [ -n "$URL" ]; then
C=$(timeout 20 curl -4 -s -o /dev/null -w '%{http_code}' "$URL/v1/health" 2>/dev/null)
echo " HTTP ${C:-超时}"
fi
echo
echo "=== 看门狗最近记录 ==="
tail -3 /root/lanremote-relay/watchdog.log 2>/dev/null | sed 's/^/ /' || echo " (还没有)"
echo
echo "=== 内存 ==="
free -m | awk 'NR==2{printf " 已用 %s MB / 可用 %s MB\n", $3, $7}'
EOF
chmod +x /usr/local/bin/lanremote-status
# ---------- 5. 自检 ----------
echo
echo "[5/5] 自检"
sleep 3
if curl -s --max-time 8 http://127.0.0.1:8080/v1/health | grep -q '"ok"'; then
echo " 本机健康检查: 通过"
else
echo " 本机健康检查: 失败"
systemctl status lanremote-relay --no-pager -l | tail -10
fi
if curl -s --max-time 20 "$URL/v1/health" | grep -q '"ok"'; then
echo " 公网访问: 通过"
else
echo " 公网访问: 还没通(隧道可能要再等一会)"
fi
KEY=$(cat "$RELAY_DIR/data/admin_key.txt" 2>/dev/null || echo "")
echo
echo "============================================================"
echo " 装好了"
echo "============================================================"
echo
echo " 公网地址 $URL"
echo " 管理页 $URL/?key=$KEY"
echo
echo " 服务状态 systemctl status lanremote-relay"
echo " systemctl status lanremote-tunnel"
echo " 看地址 lanremote-url"
echo " 看钥匙 lanremote-key"
echo
echo " 注意:快速隧道的地址**每次重启会变**。"
echo " 想固定地址要用带域名的具名隧道,见 使用说明。"
echo
free -m | head -2
echo
ps -eo rss,comm --sort=-rss 2>/dev/null | head -4 | awk '{printf " %-8s %s\n", $1, $2}'
+79
View File
@@ -0,0 +1,79 @@
#!/bin/bash
# 网络调优:修 MTU 问题(这次找到的**真正根因**)。
# ════════════════════════════════════════════════════════════════
# 证据链
# ════════════════════════════════════════════════════════════════
# 现象:新建 TLS 连接约 30% 失败,但连接一旦建立就接近 100% 成功
# -> 典型的大包被丢、小包能过
#
# ping 实测(到 1.1.1.1,-M do 禁止分片):
# 包 1448 通 包 1488 丢 包 1500 全丢
# 接口 MTU 写着 1500,但实际路径扛不住。
# TLS 握手要发大证书包 -> 被丢 -> 握手失败。
#
# ════════════════════════════════════════════════════════════════
# 实测效果(同一台设备,各 24 次)
# ════════════════════════════════════════════════════════════════
# 不夹 MSS(1500) 87%
# MSS 1300 100%
# MSS 1360 100%
# MSS 1400 100% <- 采用(离路径上限还有余量)
# MSS 1440 100%
#
# 15 分钟长测(180 次):见安装日志
#
# ════════════════════════════════════════════════════════════════
# 注意:**不要改接口 MTU**
# ════════════════════════════════════════════════════════════════
# 把 wlan0 的 MTU 降到 1400 反而掉到 50% ——
# 链路本身能过 1448 的包,压小接口 MTU 是过度限制,
# 包变小、来回变多,命中丢包窗口的概率反而更高。
# MSS 只限制 TCP 段,不动链路层,更精准。
set -u
MSS_VALUE=${MSS_VALUE:-1400}
echo " 1) 打开路径 MTU 探测"
sysctl -w net.ipv4.tcp_mtu_probing=1 >/dev/null
echo " tcp_mtu_probing = $(cat /proc/sys/net/ipv4/tcp_mtu_probing)"
echo " 2) 夹 MSS 到 $MSS_VALUE(只限制 TCP 段,不改链路层)"
for chain in OUTPUT POSTROUTING; do
# 先清掉所有旧值,避免重启后越积越多
for old in 1300 1360 1400 1440; do
while iptables -t mangle -C "$chain" -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --set-mss "$old" 2>/dev/null; do
iptables -t mangle -D "$chain" -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --set-mss "$old" 2>/dev/null || break
done
done
iptables -t mangle -A "$chain" -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --set-mss "$MSS_VALUE" 2>/dev/null
done
iptables -t mangle -L -n 2>/dev/null | grep -i mss | sed 's/^/ /'
echo " 3) 接口 MTU 保持 1500(实测降到 1400 反而掉到 50%)"
ip link set dev wlan0 mtu 1500 2>/dev/null || true
echo " wlan0 mtu = $(cat /sys/class/net/wlan0/mtu 2>/dev/null)"
# ════════════════════════════════════════════════════════════════
# 4) 关键:让已经在跑的隧道重建连接
# ════════════════════════════════════════════════════════════════
# MSS 夹取只对**新建连接**生效(它改的是 SYN 包里的 MSS 选项)。
# 隧道如果是在规则生效之前建的,它那条 TCP 连接还用着旧 MSS ——
# 实测:隧道 17:01 建、规则 17:22 生效,可用率一直卡在 76%;
# 重启隧道之后立刻回到 100%。
#
# 开机时不会走到这一步(本服务 Before=lanremote-tunnel.service,
# 规则先于隧道生效);但**手动重跑本脚本**时需要重建,
# 否则改了等于没改。
if systemctl is-active --quiet lanremote-tunnel 2>/dev/null; then
echo " 4) 隧道正在运行,它用的是旧 MSS —— 重启让它重建连接"
systemctl restart lanremote-tunnel
echo " 已重启(新连接会用上 MSS=$MSS_VALUE)"
else
echo " 4) 隧道未运行,跳过重建(开机时本服务先于它执行)"
fi
echo " 网络调优已应用(MSS=$MSS_VALUE)"
+1959
View File
File diff suppressed because it is too large Load Diff
+67
View File
@@ -0,0 +1,67 @@
#!/bin/bash
# 跑 Cloudflare 隧道,并自动维护公网地址文件。
#
# 为什么需要这个包装:
# 1. public_url.txt 只有安装脚本写过一次。服务一重启地址就变了,
# 文件还是旧的 —— 客户端一直在打失效地址。
# 2. 这条 4G 线路对 api.trycloudflare.com 的 TCP 握手经常超时,
# cloudflared 内部会一直重试但可能卡住。所以要**超时就退出**,
# 让 systemd(Restart=always)把它重新拉起来 —— 相当于自动重试。
#
# 参数选择(有实测依据):
# --edge-ip-version 4 这条线路的 IPv6 有地址但实际不通,
# Cloudflare 边缘优先 IPv6 会一直超时。
# 协议不强制 cloudflared 自检结果是 QUIC 通、
# HTTP/2 到 region2 被挡,建议 quic。
set -u
RELAY_DIR=/root/lanremote-relay
URL_FILE="$RELAY_DIR/public_url.txt"
LOG="$RELAY_DIR/tunnel.log"
# 多久拿不到地址就放弃,交给 systemd 重启
GIVE_UP_SECONDS=90
cd "$RELAY_DIR" || exit 1
: > "$LOG"
rm -f "$URL_FILE"
# 后台跑 cloudflared,日志写文件
cloudflared tunnel --no-autoupdate --edge-ip-version 4 \
--url http://127.0.0.1:8080 >> "$LOG" 2>&1 &
CF_PID=$!
trap 'kill $CF_PID 2>/dev/null; wait $CF_PID 2>/dev/null; exit 0' TERM INT
STARTED=$(date +%s)
LAST=""
GOT_URL=0
while kill -0 "$CF_PID" 2>/dev/null; do
URL=$(grep -oE 'https://[a-z0-9]+(-[a-z0-9]+)+\.trycloudflare\.com' \
"$LOG" 2>/dev/null | tail -1)
if [ -n "$URL" ] && [ "$URL" != "$LAST" ]; then
LAST="$URL"
GOT_URL=1
echo "$URL" > "$URL_FILE"
logger -t lanremote-tunnel "公网地址: $URL"
fi
# 一直拿不到地址 -> 退出,让 systemd 重来
if [ "$GOT_URL" = "0" ]; then
NOW=$(date +%s)
if [ $((NOW - STARTED)) -gt "$GIVE_UP_SECONDS" ]; then
logger -t lanremote-tunnel \
"超过 ${GIVE_UP_SECONDS} 秒没拿到地址,重启一次(这条线路经常这样)"
echo "[lanremote] 超时,交给 systemd 重启" >> "$LOG"
kill "$CF_PID" 2>/dev/null
exit 1
fi
fi
sleep 2
done
wait "$CF_PID"
exit $?
+140
View File
@@ -0,0 +1,140 @@
#!/bin/bash
# 看门狗第三版:两个信号都查。
#
# 为什么还要改:
# 第二版只看 cloudflared 的 ha_connections 指标。
# 但"僵死连接"会把指标维持在 1 —— 计数还在,实际早断了。
# 这种时候看门狗以为一切正常,一直不管。
# (今早就遇到一次:浏览器 1033,指标却说有 1 条连接)
#
# 这一版:
# 信号 A:ha_connections == 0 -> 记一次"零连接"
# 信号 B:公网 /v1/health 打不通 -> 记一次"公网不通"
# 任一信号连续 2 次 -> 重启隧道
# 另外:隧道日志超过 10 分钟没新行 + 公网不通 -> 判定进程卡死,重启
set -u
RELAY_DIR=/root/lanremote-relay
URL_FILE="$RELAY_DIR/public_url.txt"
STATE_FILE="$RELAY_DIR/watchdog.state"
TUNNEL_LOG="$RELAY_DIR/tunnel.log"
METRICS=http://127.0.0.1:20241/metrics
LOG_TAG=lanremote-watchdog
# 连续几次公网不通才重启。
# 设 3(约 60 秒)而不是 2:短抖动让 cloudflared 自己重连就好,
# 频繁重启反而让它一直停在"正在重连"的状态。
THRESHOLD=3
STALE_MINUTES=10 # 日志多久没动静算卡死
# 两次重启之间至少隔这么久(秒)。
# 实测隧道重建只要 30~40 秒,原来设 120 秒太保守 ——
# 真断的时候会干等(日志里出现过"距上次重启才 118 秒,先不重启",
# 结果多断了 2 分钟)。
# 设 45 秒:既够 cloudflared 把连接建起来,又不会干等太久。
MIN_RESTART_GAP=45
log() {
logger -t "$LOG_TAG" "$1"
echo "[$(date '+%m-%d %H:%M:%S')] $1"
}
read_state() {
local key="$1" default="$2" value=""
if [ -f "$STATE_FILE" ]; then
value=$(grep "^$key=" "$STATE_FILE" 2>/dev/null | cut -d= -f2)
fi
[ -n "$value" ] && echo "$value" || echo "$default"
}
write_state() { printf 'zero=%s\nfail=%s\n' "$1" "$2" > "$STATE_FILE"; }
LAST_RESTART_FILE="$RELAY_DIR/watchdog.last_restart"
restart_tunnel() {
local why="$1"
local now last gap
now=$(date +%s)
last=0
[ -f "$LAST_RESTART_FILE" ] && last=$(cat "$LAST_RESTART_FILE" 2>/dev/null || echo 0)
gap=$((now - last))
# 连接数归零 = 真的断了,不是抖动 —— 这种不受间隔限制
local conns_now
conns_now=$(timeout 5 curl -s --max-time 4 "$METRICS" 2>/dev/null \
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
if [ "${conns_now:-1}" != "0" ] && [ "$gap" -lt "$MIN_RESTART_GAP" ]; then
# 刚重启过还没到间隔:这次先不重启,
# 让 cloudflared 自己把连接建起来(它本来就在重试)。
log "$why —— 但距上次重启才 ${gap} 秒(下限 ${MIN_RESTART_GAP} 秒),先不重启"
write_state 0 0
exit 0
fi
log "$why —— 重启隧道(距上次 ${gap} 秒)"
echo "$now" > "$LAST_RESTART_FILE"
systemctl restart lanremote-tunnel
write_state 0 0
sleep 25
log "重启后地址: $(cat "$URL_FILE" 2>/dev/null || echo '(还没拿到)')"
exit 0
}
ZERO=$(read_state zero 0)
FAIL=$(read_state fail 0)
# ---------- 信号 A:cloudflared 自报的连接数 ----------
CONNS=$(timeout 5 curl -s --max-time 4 "$METRICS" 2>/dev/null \
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
[ -z "$CONNS" ] && CONNS=""
# ---------- 信号 B:公网真的通吗 ----------
URL=$(cat "$URL_FILE" 2>/dev/null | tr -d '\r\n')
CODE="000"
if [ -n "$URL" ]; then
CODE=$(timeout 20 curl -4 -s -o /dev/null -w '%{http_code}' \
-H 'User-Agent: LanRemote/1.4.0' "$URL/v1/health" 2>/dev/null)
CODE=${CODE:-000}
fi
PUBLIC_OK=0
[ "$CODE" = "200" ] && PUBLIC_OK=1
# ---------- 信号 C:日志僵死了吗 ----------
LOG_STALE=0
if [ -f "$TUNNEL_LOG" ]; then
LAST=$(stat -c %Y "$TUNNEL_LOG" 2>/dev/null || echo 0)
NOW=$(date +%s)
if [ "$LAST" -gt 0 ] && [ $((NOW - LAST)) -gt $((STALE_MINUTES * 60)) ]; then
LOG_STALE=1
fi
fi
# ---------- 判断 ----------
if [ "$PUBLIC_OK" = "1" ]; then
# 公网通就是真的通,计数器清零
if [ "$ZERO" != "0" ] || [ "$FAIL" != "0" ]; then
log "公网恢复(HTTP 200),计数清零(之前 zero=$ZERO fail=$FAIL)"
fi
write_state 0 0
exit 0
fi
# 公网不通 —— 看是哪种原因
DETAIL="HTTP $CODE"
[ -n "$CONNS" ] && DETAIL="$DETAIL,ha_connections=$CONNS"
if [ "$LOG_STALE" = "1" ] && [ "$FAIL" -ge 1 ]; then
restart_tunnel "日志 ${STALE_MINUTES} 分钟没动静加上公网不通(进程卡死)"
fi
FAIL=$((FAIL + 1))
if [ -n "$CONNS" ] && [ "$CONNS" = "0" ]; then
ZERO=$((ZERO + 1))
fi
log "公网不通($DETAIL),连续第 $FAIL 次(零连接 $ZERO 次)"
if [ "$FAIL" -ge "$THRESHOLD" ] || [ "$ZERO" -ge "$THRESHOLD" ]; then
restart_tunnel "连续 $FAIL 次公网不通(含 $ZERO 次零连接)"
fi
write_state "$ZERO" "$FAIL"