Files
ykxmz/_build/setup_dual.sh
T
xmz7891 d4f335b8f9 管理页新增:允许/禁止新设备接入 + 修改设备配对码
== 功能一:允许新设备接入开关 ==

关掉之后只有已经登记过的电脑能继续上线,没见过的新设备一律拒绝。
状态存在 relay_state.json 里,重启不丢。

判断新设备用的是 deviceId,不是配对码 ——
这一点很关键:管理员给老设备改过码之后,
那台设备必须仍然能进来;按码判断的话它会被自己的开关挡在外面。
测试里专门有一条 A3b 守这个行为。

被拒时回 blocked=True,让客户端能区分被管理员挡了
和配对码冲突,而不是去匹配中文错误文案(文案一改就坏)。

== 功能二:修改任意设备的配对码 ==

管理页每台设备一个「改码」按钮,填 6 位数字即生效。
要点:
  * 新旧码冲突检查
  * 旧码映射必须删掉,否则变成孤儿条目,
    管理页配对码占用的数字会虚高
  * 改码踢掉该设备当前会话(会话绑在码上)
  * 立刻持久化

== 顺手修掉两个真 bug ==

1. 改码功能一上线就 500:Session 用了 __slots__,
   根本没有 session_id 属性(会话是以 token 为 key 存在 dict 里的),
   写 s.session_id 直接 AttributeError。
   改成遍历 sessions.items() 拿 key。

2. snapshot() 里有同样的错(清理超时设备那段):
   平时 stale 列表是空的所以从没炸过,一旦真有设备超过 6 小时
   没上报就会 500。这次一起修了。

== 管理页 UI ==
  * 设备小节加了「允许新设备接入」勾选框
  * 每行加「改码」按钮(走 prompt,不引入弹窗层)
  * 改码按钮用 data-recode 而不是 data-device ——
    现有单台删除按 button.danger[data-device] 绑,
    带上 data-device 会被当成删除按钮,点改码变删设备。
    测试 C6 专门断言这一点。

== 测试 ==
新增 _tests/test_admin_device_control.py(23 项),
已注册进 _tests/verify_all.ps1 第 3e 阶段。
2026-10-07 17:01:14 +08:00

129 lines
5.5 KiB
Bash

#!/bin/bash
# 双协议隧道:同时跑 QUIC 和 HTTP/2 两个 cloudflared 实例。
#
# == 为什么 ==
#
# 实测两种协议各有各的病:
# QUIC 尾部延迟好(P90 ~2.5 秒),但 DialContext 会
# "no recent network activity" 超时,建连慢、偶发失败
# HTTP/2 建连可靠(TCP + MSS 钳制保护),但尾部延迟差(P90 14 秒,
# TCP 队头阻塞:一条连接上丢一个包,所有流一起卡)
#
# 同一个隧道 ID 可以起**多个副本**,Cloudflare 会把请求分摊到
# 所有副本注册上来的连接上。所以:
#
# 副本 A: --protocol quic --ha-connections 5
# 副本 B: --protocol http2 --ha-connections 5
#
# 一共 10 条连接、横跨两种协议。QUIC 犯病时 HTTP/2 那半边还在服务,
# 反过来也一样 —— 这是真正的**故障隔离**,而不只是加连接数。
#
# == 资源 ==
# 每个 cloudflared 约占 25 MB。设备 460 MB 总内存、平时可用 300+ MB,
# 两个实例没问题(原来 MemoryMax=160M 是给单实例的,两个各自限 128M)。
set -u
TUNNEL_ID=7fd33d9c-f910-42e3-88aa-752d6b2a9f2f
CONFIG=/root/.cloudflared/config.yml
MAIN=/etc/systemd/system/lanremote-tunnel.service
SECOND=/etc/systemd/system/lanremote-tunnel-h2.service
echo "════════ 1. 备份现有主服务 ════════"
cp -a "$MAIN" "/root/lanremote-tunnel.service.bak.dual.$(date +%s)"
echo " 当前参数: $(grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$MAIN" | tr '\n' ' ')"
echo
echo "════════ 2. 主服务改成 QUIC + 5 ════════"
sed -i 's/--protocol [a-z0-9]*/--protocol quic/' "$MAIN"
sed -i 's/--ha-connections [0-9]*/--ha-connections 5/' "$MAIN"
sed -i 's/^MemoryMax=.*/MemoryMax=128M/' "$MAIN"
grep -E 'ha-connections|protocol|MemoryMax' "$MAIN" | sed 's/^/ /'
echo
echo "════════ 3. 创建 HTTP/2 副本服务 ════════"
cat > "$SECOND" <<'UNITEOF'
[Unit]
Description=ykxmz Cloudflare 具名隧道(HTTP/2 副本)
# 和主实例跑同一个隧道 ID —— Cloudflare 会把请求分摊到
# 两个副本注册的全部连接上,形成协议级的故障隔离。
After=network-online.target lanremote-relay.service lanremote-nettune.service
Wants=network-online.target lanremote-relay.service
[Service]
Type=simple
ExecStart=/usr/local/bin/cloudflared tunnel \
--protocol http2 \
--edge-ip-version 4 \
--ha-connections 5 \
--no-autoupdate \
--retries 5 \
--config /root/.cloudflared/config.yml run lanremote
Restart=always
RestartSec=5
MemoryMax=128M
StandardOutput=append:/root/lanremote-relay/tunnel-h2.log
StandardError=append:/root/lanremote-relay/tunnel-h2.log
[Install]
WantedBy=multi-user.target
UNITEOF
echo " 已写入 $SECOND"
cat "$SECOND" | grep -E 'protocol|ha-connections|MemoryMax' | sed 's/^/ /'
echo
echo "════════ 4. 启动,等连接建齐 ════════"
systemctl daemon-reload
systemctl enable lanremote-tunnel-h2 > /dev/null 2>&1
systemctl restart lanremote-tunnel
systemctl restart lanremote-tunnel-h2
for i in $(seq 1 40); do
sleep 4
n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
# 两个实例各自暴露 metrics 端口会冲突,靠下面分别看进程
[ "${n:-0}" -ge 10 ] && break
done
echo " metrics 报的连接数: ${n:-?}"
echo
echo "════════ 5. 两个实例的进程和内存 ════════"
for svc in lanremote-tunnel lanremote-tunnel-h2; do
pid=$(systemctl show $svc -p MainPID --value)
mem=$(systemctl show $svc -p MemoryCurrent --value)
proto=$(grep -oE '\-\-protocol [a-z0-9]+' /etc/systemd/system/$svc.service | head -1)
conns=$(grep -oE '\-\-ha-connections [0-9]+' /etc/systemd/system/$svc.service | head -1)
printf " %-26s PID=%-7s 内存=%s MB %s %s 状态=%s\n" \
"$svc" "$pid" "$(( ${mem:-0} / 1048576 ))" "$proto" "$conns" \
"$(systemctl is-active $svc)"
done
echo
echo " ⚠ 两个实例都想占 20241 metrics 端口,后起的会失败 —— 这是正常的。"
echo " 实际连接数看 cloudflared 日志里的 Registered 行。"
echo " ---"
echo " 主实例(QUIC)最近注册:"
tail -60 /root/lanremote-relay/tunnel.log 2>/dev/null | grep -c 'Registered tunnel connection' | sed 's/^/ /'
echo " 副本实例(HTTP/2)最近注册:"
tail -60 /root/lanremote-relay/tunnel-h2.log 2>/dev/null | grep -c 'Registered tunnel connection' | sed 's/^/ /'
echo
echo "════════ 6. 等公网可用 ════════"
for i in $(seq 1 25); do
c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 12 -H 'User-Agent: LanRemote/1.8.2' https://relay.xxmxxs.top/v1/health)
printf " 第 %d 次: %s\n" "$i" "$c"
[ "$c" = "200" ] && break
sleep 3
done
echo
echo "════════ 7. 快速验收(40 次)════════"
ok=0; times=()
for i in $(seq 1 40); do
out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 -H 'User-Agent: LanRemote/1.8.2' https://relay.xxmxxs.top/v1/health)
c=$(echo "$out" | awk '{print $1}'); ms=$(echo "$out" | awk '{printf "%d", $2*1000}')
[ "$c" = "200" ] && { ok=$((ok+1)); times+=("$ms"); }
done
stats=$(printf '%s\n' "${times[@]:-0}" | sort -n | awk '{a[NR]=$1} END {printf "中位 %d P90 %d P99 %d 最大 %d", a[int(NR/2)+1], a[int(NR*0.9)+1], a[int(NR*0.99)+1], a[NR]}')
echo " 可用率 $ok/40 = $((ok*100/40))% $stats ms"