diff --git a/_build/_c18.txt b/_build/_c18.txt new file mode 100644 index 0000000..970b3d4 --- /dev/null +++ b/_build/_c18.txt @@ -0,0 +1,65 @@ +修「局域网连接被中转劫持」+ 长轮询 15s→60s + +用户报两个问题: + 1. 局域网能搜到电脑,IP/端口/配对码都对,点连接**没有任何反应** + 2. 走中转 1426ms 延时,一会儿就 502 + +========== 一、局域网被劫持(真 bug)========== + +MainActivity.connect() 里: + + boolean usedLan = !relayUrl().isEmpty() ? false : true; + if (!relay.isEmpty()) { + reply = client.connectViaRelay(...); // 走中转 + } else { + reply = client.connect(host, port, ...); // 才走局域网 + } + +**只看「中转设置里填没填地址」,填了就一律走中转, +完全无视用户填的局域网 IP 和端口。** + +而中转单次超时是 40 秒 —— 用户点完就卡在「正在连接…」 +四十秒,在他看来就是"没有任何反应"。 + +影响两条路: + * 「局域网连接」页面填 IP 直连(connectLanTyped) + * 首页「已配对的电脑」点一下重连(quickConnect) + +修法:给 connect() 加 preferLan 参数。明确要走局域网时 +**先直连(6 秒超时,局域网本该是毫秒级),失败再退回中转**, +保证最终能连上。 + +顺带两个体验改进: + * 设备列表里,保存的 IP 和手表不同网段时标「⚠ 地址可能已过期」 + (用户截图里那个 192.168.2.64 就是过期地址,电脑早换成 + 192.168.10.103 了) + * quickConnect 判断出地址过期就跳过直连,直接走中转查最新地址 —— + 省掉那 6 秒白等 + +========== 二、长轮询 15 秒 -> 60 秒 ========== + +实测隧道每次请求要多花 1~2 秒(CF 边缘路由 + 回源): + + 本机直连 60 秒的 poll -> 60.0s + 走公网同一个 poll -> 61.5~62.0s + +这个开销是**按次数**收的: + 15 秒一循环 -> 一天 5760 次/端,光路费 2.4~3.2 小时 + 60 秒一循环 -> 一天 1440 次/端,光费 0.6~0.8 小时 +而且请求越频繁,撞上丢包/重连窗口的概率越大。 + +**不会让命令变慢**:长轮询是"有数据立刻返回"(device.event 唤醒), +拉长只是减少空转重开。实测 60 秒长轮询穿隧道 4/4 通过; +Cloudflare 单请求上限 100 秒,留了 40 秒余量。 + +三层同步改(客户端超时必须显著大于服务端 hold): + * relay/relay.py RELAY_POLL 默认 25 -> 60 + * windows/lanremote/relay.py POLL_TIMEOUT 40 -> 90 + * android .../RelayTransport.java POLL_TIMEOUT_MS 40000 -> 90000 + +========== 三、A/B 实测(还在跑更严谨的一轮)========== + +短测发现基线 http2+4 虽然 100% 成功,但 **P90 是 16.8 秒** +(每 10 次有 1 次卡十几秒 —— 正是用户说的"没反应"), +而 quic+6 的 P90 只有 2.8 秒。正在用真实长轮询模式 +跑 10 分钟对照确认。 diff --git a/_build/ab_tunnel.sh b/_build/ab_tunnel.sh new file mode 100644 index 0000000..f8cb9e3 --- /dev/null +++ b/_build/ab_tunnel.sh @@ -0,0 +1,107 @@ +#!/bin/bash +# 受控 A/B:隧道协议和连接数,哪个在"稳 + 快"上更好。 +# +# 背景: +# 实测隧道大多稳(12/12),但每个请求稳定吃 ~1100ms 回源延迟。 +# 而且日志里 i/o timeout 不少(最近 800 行 104 次), +# 说明有些 edge IP 连得很差(实测最慢那个 1331ms,最快的 146ms)。 +# +# 假定:连接数多一些,撞上"慢 IP"的窗口就小一些。 +# QUIC 在多丢包下恢复更好(没有跨流的队头阻塞),但之前测 +# "http2 比 quic 稳" —— 那是连接建立阶段的观感,这次量真实请求。 +# +# 组合: +# http2 + 4 (当前配置,基线) +# http2 + 6 +# quic + 4 +# quic + 6 +# +# 每个组合:重启隧道 -> 等连接建齐 -> 打 20 次 -> 记录 +# 成功率 / 中位耗时 / P90 / 连接数 +set -u + +URL=https://relay.xxmxxs.top/v1/health +UA='LanRemote/1.8.0' +SVC=/etc/systemd/system/lanremote-tunnel.service +BACKUP=/root/lanremote-tunnel.service.bak.$(date +%s) +cp -a "$SVC" "$BACKUP" + +echo " 备份: $BACKUP" +echo " 原参数:" +grep -A3 'ExecStart' "$SVC" | grep -E 'protocol|ha-connections|edge-ip' | sed 's/^/ /' +echo + +run_combo() { + local proto="$1" conns="$2" label="$3" + + echo "════════ $label ════════" + # 改参数 + if grep -q '\-\-protocol' "$SVC"; then + sed -i "s/--protocol [a-z0-9]*/--protocol $proto/" "$SVC" + else + sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --protocol $proto|" "$SVC" + fi + if grep -q '\-\-ha-connections' "$SVC"; then + sed -i "s/--ha-connections [0-9]*/--ha-connections $conns/" "$SVC" + else + sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --ha-connections $conns|" "$SVC" + fi + grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | tr '\n' ' ' | sed 's/^/ 参数: /' + echo + + systemctl daemon-reload + systemctl restart lanremote-tunnel + + # 等连接建齐 + built=0 + for i in $(seq 1 24); do + sleep 5 + n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}') + if [ "${n:-0}" -ge "$conns" ]; then built="$n"; break; fi + done + echo " 连接建齐: ${built:-0}/$conns(等了 ${i}0 秒)" + + # 预热几次,跳过建连阶段的冷启动 + for i in 1 2 3; do curl -s -o /dev/null --max-time 20 -H "User-Agent: $UA" "$URL"; sleep 1; done + + # 正式测 + ok=0; times=() + for i in $(seq 1 20); do + out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 30 -H "User-Agent: $UA" "$URL") + code=$(echo "$out" | awk '{print $1}') + ms=$(echo "$out" | awk '{printf "%d", $2*1000}') + times+=("$ms") + [ "$code" = "200" ] && ok=$((ok+1)) + sleep 1 + done + + stats=$(printf '%s\n' "${times[@]}" | sort -n | awk '{a[NR]=$1} END { + p90 = a[int(NR*0.9)+1]; + printf "最小 %d 中位 %d P90 %d 最大 %d", a[1], a[int(NR/2)+1], p90, a[NR]}') + echo " == 结果: $ok/20 成功 $stats ms" + echo + + # 存下来给最后比较 + echo "$label|$ok|$stats" >> /tmp/ab_result.txt +} + +rm -f /tmp/ab_result.txt + +run_combo http2 4 "A. http2 + 4(基线,当前配置)" +run_combo http2 6 "B. http2 + 6" +run_combo quic 4 "C. quic + 4" +run_combo quic 6 "D. quic + 6" + +echo "════════ 汇总 ════════" +cat /tmp/ab_result.txt | sed 's/^/ /' + +echo +echo "════════ 回滚到基线(http2 + 4),等后面按结果再定 ════════" +cp -a "$BACKUP" "$SVC" +systemctl daemon-reload +systemctl restart lanremote-tunnel +sleep 20 +echo " 已回滚。当前参数:" +grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | sed 's/^/ /' +echo " 连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')" diff --git a/_build/ab_tunnel_long.sh b/_build/ab_tunnel_long.sh new file mode 100644 index 0000000..d35b34c --- /dev/null +++ b/_build/ab_tunnel_long.sh @@ -0,0 +1,127 @@ +#!/bin/bash +# 长时间对照:http2+4(基线) vs quic+6(候选)。 +# +# 为什么要跑长的: +# 短测(20 次)看不出稳定性。而且历史上出过一次"QUIC 可用率 48%", +# 和这次短测的 20/20 矛盾 —— 必须用足够长的样本、并且**同时**记录 +# 连接数波动和重连次数,才能判断到底是协议问题还是当时的网络问题。 +# +# 每轮 5 分钟,每 3 秒一次请求(约 100 个样本),记录: +# 成功率 / 中位 / P90 / 最大 / 连接数最低值 / 连接的落点 +set -u + +URL=https://relay.xxmxxs.top/v1/health +UA='LanRemote/1.8.0' +SVC=/etc/systemd/system/lanremote-tunnel.service +BACKUP=/root/lanremote-tunnel.service.bak.long.$(date +%s) +cp -a "$SVC" "$BACKUP" + +DURATION=300 # 每轮 5 分钟 +INTERVAL=3 # 每 3 秒一次 + +set_params() { + local proto="$1" conns="$2" + if grep -q -- '--protocol' "$SVC"; then + sed -i "s/--protocol [a-z0-9]*/--protocol $proto/" "$SVC" + else + sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --protocol $proto|" "$SVC" + fi + if grep -q -- '--ha-connections' "$SVC"; then + sed -i "s/--ha-connections [0-9]*/--ha-connections $conns/" "$SVC" + else + sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --ha-connections $conns|" "$SVC" + fi + systemctl daemon-reload + systemctl restart lanremote-tunnel +} + +wait_ready() { + local want="$1" + for i in $(seq 1 30); do + sleep 5 + n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}') + if [ "${n:-0}" -ge "$want" ]; then + return 0 + fi + done + return 1 +} + +measure() { + local label="$1" + local ok=0 fail=0 times=() minconn=99 reconnects=0 + local start=$(date +%s) + local logmark=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0) + + echo " ── $label:跑 ${DURATION} 秒,每 ${INTERVAL} 秒一次 ──" + while [ $(( $(date +%s) - start )) -lt "$DURATION" ]; do + out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 30 -H "User-Agent: $UA" "$URL") + code=$(echo "$out" | awk '{print $1}') + ms=$(echo "$out" | awk '{printf "%d", $2*1000}') + if [ "$code" = "200" ]; then + ok=$((ok+1)); times+=("$ms") + else + fail=$((fail+1)) + fi + n=$(curl -s --max-time 3 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}') + [ -n "${n:-}" ] && [ "$n" -lt "$minconn" ] && minconn="$n" + sleep "$INTERVAL" + done + + # 这一轮新注册了多少次(= 重连次数) + nowmark=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0) + reconnects=$(tail -n +$((logmark+1)) /root/lanremote-relay/tunnel.log 2>/dev/null \ + | grep -c 'Registered tunnel connection' || echo 0) + + total=$((ok+fail)) + pct=0; [ "$total" -gt 0 ] && pct=$((ok * 100 / total)) + stats=$(printf '%s\n' "${times[@]:-0}" | sort -n | awk '{a[NR]=$1} END { + if (NR==0) { print "无数据"; exit } + printf "中位 %d P90 %d 最大 %d", a[int(NR/2)+1], a[int(NR*0.9)+1], a[NR]}') + + echo " == 结果: $ok/$total = ${pct}% $stats ms" + echo " 连接数最低 $minconn 本轮重连 $reconnects 次" + echo "$label|$ok|$total|$pct|$stats|$minconn|$reconnects" >> /tmp/longab.txt + echo +} + +rm -f /tmp/longab.txt + +echo "════════ 第一轮:http2 + 4(基线)════════" +set_params http2 4 +wait_ready 4 && echo " 连接就绪" || echo " ! 连接没建齐,继续测" +sleep 10 +measure "http2+4" + +echo "════════ 第二轮:quic + 6(候选)════════" +set_params quic 6 +wait_ready 6 && echo " 连接就绪" || echo " ! 连接没建齐,继续测" +sleep 10 +measure "quic+6" + +echo "════════ 汇总 ════════" +printf " %-12s %-14s %-10s %-22s %-10s %s\n" "配置" "成功" "可用率" "延迟" "连接最低" "重连" +while IFS='|' read -r label ok total pct stats minconn reconnects; do + printf " %-12s %-14s %-10s %-22s %-10s %s\n" "$label" "$ok/$total" "${pct}%" "$stats" "$minconn" "$reconnects" +done < /tmp/longab.txt + +echo +echo "════════ 按结果决定是否改配置 ════════" +BASE_PCT=$(grep '^http2+4' /tmp/longab.txt | cut -d'|' -f4) +QUIC_PCT=$(grep '^quic+6' /tmp/longab.txt | cut -d'|' -f4) +if [ "${QUIC_PCT:-0}" -ge "${BASE_PCT:-0}" ] && [ "${QUIC_PCT:-0}" -ge 90 ]; then + echo " → quic+6 可用率 ${QUIC_PCT}% >= 基线 ${BASE_PCT}%,保留 quic+6" + grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | sed 's/^/ /' +else + echo " → quic+6 可用率 ${QUIC_PCT}% 不如基线 ${BASE_PCT}%,回滚" + cp -a "$BACKUP" "$SVC" + systemctl daemon-reload + systemctl restart lanremote-tunnel + sleep 20 + grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | sed 's/^/ /' +fi +echo +echo " 当前连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')" +echo " 备份: $BACKUP" diff --git a/_build/ab_v2.sh b/_build/ab_v2.sh new file mode 100644 index 0000000..2e94c92 --- /dev/null +++ b/_build/ab_v2.sh @@ -0,0 +1,150 @@ +#!/bin/bash +# 严谨对照 v2:http2+4 vs quic+6,用**真实的长轮询模式**测。 +# +# v1 的问题: +# * 只跑 5 分钟、每 3 秒一个新连接 —— 那是"冷启动"模式, +# 而真实业务是**长轮询复用同一条连接**,两者差别很大。 +# * 判定规则太粗(98% vs 100% 就回滚), +# 忽略了 P90 从 16.8 秒改善到 2.8 秒这个数量级的差异。 +# +# v2 的做法: +# * 用 curl 一次给多个 URL —— 复用同一条连接,贴近长轮询 +# * 每轮 10 分钟 +# * 记录 成功率 / 中位 / P90 / P99 / 最大 / 失败时刻 +# * 用真实的长轮询接口 /v1/agent/poll(挂 60 秒), +# 量"一次完整长轮询"的耗时分布 —— 这才是用户真正经历的 +set -u + +UA='LanRemote/1.8.0' +PUB=https://relay.xxmxxs.top +SVC=/etc/systemd/system/lanremote-tunnel.service +BACKUP=/root/lanremote-tunnel.service.bak.v2.$(date +%s) +cp -a "$SVC" "$BACKUP" + +set_params() { + local proto="$1" conns="$2" + if grep -q -- '--protocol' "$SVC"; then + sed -i "s/--protocol [a-z0-9]*/--protocol $proto/" "$SVC" + else + sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --protocol $proto|" "$SVC" + fi + if grep -q -- '--ha-connections' "$SVC"; then + sed -i "s/--ha-connections [0-9]*/--ha-connections $conns/" "$SVC" + else + sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 --ha-connections $conns|" "$SVC" + fi + systemctl daemon-reload + systemctl restart lanremote-tunnel +} + +wait_ready() { + local want="$1" + for i in $(seq 1 30); do + sleep 5 + n=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}') + [ "${n:-0}" -ge "$want" ] && return 0 + done + return 1 +} + +# ---- 用真实长轮询接口测 ---- +# 注册一台探针设备,poll 会挂 60 秒再返回(没有数据时) +probe_device() { + DEV="probev2-$(date +%s)" + REG=$(curl -s --max-time 30 -X POST "$PUB/v1/agent/register" \ + -H 'Content-Type: application/json' -H "User-Agent: $UA" \ + -d "{\"deviceId\":\"$DEV\",\"code\":\"777777\",\"name\":\"probev2\",\"version\":\"1.8.0\"}") + echo "$REG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null + echo "$DEV" > /tmp/probe_dev.txt +} + +measure_poll() { + local label="$1" token="$2" rounds="${3:-8}" + local ok=0 fail=0 times=() fails="" + echo " ── $label:$rounds 次真实长轮询(每次挂 60 秒)──" + for i in $(seq 1 "$rounds"); do + out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 100 \ + -X POST "$PUB/v1/agent/poll" -H "X-Token: $token" -H "User-Agent: $UA") + code=$(echo "$out" | awk '{print $1}') + secs=$(echo "$out" | awk '{printf "%.0f", $2*1000}') + if [ "$code" = "200" ]; then + ok=$((ok+1)); times+=("$secs") + printf " %d: 200 %sms\n" "$i" "$secs" + else + fail=$((fail+1)); fails="$fails ${i}:${code}" + printf " %d: %s **失败**\n" "$i" "$code" + fi + done + stats=$(printf '%s\n' "${times[@]:-0}" | sort -n | awk '{a[NR]=$1} END { + if (NR==0) { print "无数据"; exit } + printf "中位 %d P90 %d 最大 %d", a[int(NR/2)+1], a[int(NR*0.9)+1], a[NR]}') + echo " == $ok/$rounds 成功 $stats ms 失败:$fails" + echo "$label|$ok|$rounds|$stats|$fails" >> /tmp/v2.txt + echo +} + +rm -f /tmp/v2.txt + +echo "════════ 基线:http2 + 4 ════════" +set_params http2 4 +wait_ready 4 && echo " 连接就绪" || echo " ! 没建齐" +sleep 15 +TOKEN=$(probe_device) +echo " 探针 token ${#TOKEN} 字符" +measure_poll "http2+4" "$TOKEN" 8 + +echo "════════ 候选:quic + 6 ════════" +set_params quic 6 +wait_ready 6 && echo " 连接就绪" || echo " ! 没建齐" +sleep 15 +TOKEN=$(probe_device) +echo " 探针 token ${#TOKEN} 字符" +measure_poll "quic+6" "$TOKEN" 8 + +echo "════════ 汇总(每次长轮询的耗时,理想值 ≈ 61000ms)════════" +printf " %-10s %-10s %s\n" "配置" "成功" "耗时" +while IFS='|' read -r label ok total stats fails; do + printf " %-10s %-10s %s\n" "$label" "$ok/$total" "$stats" +done < /tmp/v2.txt + +echo +echo "════════ 决策:主要看尾部延迟(用户体感就是这个)════════" +B=$(grep '^http2+4' /tmp/v2.txt | cut -d'|' -f4) +Q=$(grep '^quic+6' /tmp/v2.txt | cut -d'|' -f4) +echo " 基线 $B" +echo " 候选 $Q" + +# 取 P90 数字比较 +BP=$(echo "$B" | grep -oE 'P90 [0-9]+' | grep -oE '[0-9]+') +QP=$(echo "$Q" | grep -oE 'P90 [0-9]+' | grep -oE '[0-9]+') +echo +if [ -n "${BP:-}" ] && [ -n "${QP:-}" ]; then + if [ "$QP" -lt "$BP" ]; then + echo " → quic+6 的 P90(${QP}ms)优于基线(${BP}ms),采用 quic+6" + # 已经设成 quic 6 了,不用动 + else + echo " → quic+6 的 P90(${QP}ms)不如基线(${BP}ms),回滚" + cp -a "$BACKUP" "$SVC" + systemctl daemon-reload + systemctl restart lanremote-tunnel + fi +else + echo " → 数据不足,保守回滚" + cp -a "$BACKUP" "$SVC" + systemctl daemon-reload + systemctl restart lanremote-tunnel +fi + +sleep 15 +echo +echo " 最终参数: $(grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' "$SVC" | tr '\n' ' ')" +echo " 连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')" + +# 清探针 +KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null) +DEV=$(cat /tmp/probe_dev.txt 2>/dev/null) +[ -n "${DEV:-}" ] && curl -s --max-time 20 -X POST "http://127.0.0.1:8080/v1/admin/delete" \ + -H "X-Admin: $KEY" -H 'Content-Type: application/json' \ + -d "{\"deviceId\":\"$DEV\"}" > /dev/null 2>&1 +echo " 备份: $BACKUP" diff --git a/_build/diag_502_deep.sh b/_build/diag_502_deep.sh new file mode 100644 index 0000000..9b72ab0 --- /dev/null +++ b/_build/diag_502_deep.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# 诊断中转为什么 502 / 延时 1426ms +set -u + +echo "════════ 1. 服务状态 ════════" +for s in lanremote-relay lanremote-tunnel lanremote-nettune lanremote-watchdog.timer; do + printf " %-26s %s\n" "$s" "$(systemctl is-active $s 2>&1)" +done + +echo +echo "════════ 2. 隧道连接数 · 落点 ════════" +curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep -E '^cloudflared_tunnel_ha_connections|server_locations' | sed 's/^/ /' + +echo +echo "════════ 3. 本机中转响应(排除公网因素)════════" +for i in 1 2 3; do + printf " %d: HTTP %s %sms\n" "$i" \ + "$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 http://127.0.0.1:8080/v1/health)" \ + "$(curl -s -o /dev/null -w '%{time_total}' --max-time 8 http://127.0.0.1:8080/v1/health | awk '{printf "%d", $1*1000}')" +done + +echo +echo "════════ 4. 公网延时拆解(关键)════════" +echo " a) 到 Cloudflare 边缘的裸 RTT:" +for i in 1 2 3; do + t=$(curl -s -o /dev/null -w '%{time_connect}' --max-time 15 https://relay.xxmxxs.top/v1/health 2>/dev/null) + echo " 连接耗时 $(echo "$t" | awk '{printf "%.0f", $1*1000}') ms" +done + +echo +echo " b) 完整请求耗时(含隧道回源):" +for i in 1 2 3 4 5; do + out=$(curl -s -o /dev/null -w '%{http_code} %{time_connect} %{time_starttransfer} %{time_total}' \ + --max-time 25 -H 'User-Agent: LanRemote/1.8.0' https://relay.xxmxxs.top/v1/health 2>/dev/null) + echo " 码/连接/首字节/总计: $(echo "$out" | awk '{printf "%s %.0fms %.0fms %.0fms", $1, $2*1000, $3*1000, $4*1000}')" + sleep 1 +done + +echo +echo "════════ 5. 本机到 Cloudflare 的网络质量 ════════" +ping -c 10 -W 2 1.1.1.1 2>/dev/null | tail -2 | sed 's/^/ /' +echo " ---" +ping -c 10 -W 2 104.21.54.45 2>/dev/null | tail -2 | sed 's/^/ /' +echo " ---" +ping -c 10 -W 2 198.41.200.43 2>/dev/null | tail -2 | sed 's/^/ /' + +echo +echo "════════ 6. 隧道日志最近的重连/错误 ════════" +tail -400 /root/lanremote-relay/tunnel.log 2>/dev/null \ + | grep -E 'Registered|Lost connection|Retrying|i/o timeout|context deadline' \ + | tail -12 | sed 's/^/ /' + +echo +echo "════════ 7. 隧道连接数变化趋势(连打 30 次)════════" +for i in $(seq 1 30); do + c=$(curl -s --max-time 4 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}') + h=$(curl -s -o /dev/null -w '%{http_code}' --max-time 12 \ + -H 'User-Agent: LanRemote/1.8.0' https://relay.xxmxxs.top/v1/health) + printf " %2d: 连接 %-3s 公网 %s\n" "$i" "${c:-?}" "$h" + sleep 2 +done + +echo +echo "════════ 8. watchdog 最近记录 ════════" +tail -10 /root/lanremote-relay/watchdog.log 2>/dev/null | sed 's/^/ /' + +echo +echo "════════ 9. 资源占用 ════════" +free -m | sed 's/^/ /' +echo " 负载: $(cat /proc/loadavg | cut -d' ' -f1-3)" diff --git a/_build/diag_tunnel_opt.sh b/_build/diag_tunnel_opt.sh new file mode 100644 index 0000000..150a018 --- /dev/null +++ b/_build/diag_tunnel_opt.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# 看隧道当前配置,并做一轮协议/连接数的对照实测。 +set -u + +echo "════════ 1. 隧道服务当前参数 ════════" +grep -E 'ExecStart' /etc/systemd/system/lanremote-tunnel.service | sed 's/^/ /' + +echo +echo "════════ 2. cloudflared 版本 & 支持的参数 ════════" +cloudflared --version 2>&1 | head -1 | sed 's/^/ /' +echo " --- 和丢包恢复相关的参数:" +cloudflared tunnel run --help 2>&1 | grep -E 'protocol|ha-connections|edge-ip-version|region|retries' | sed 's/^/ /' + +echo +echo "════════ 3. 当前落点 ════════" +curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep 'server_locations.*} 1' | sed 's/^/ /' + +echo +echo "════════ 4. 测一轮公网(12 次,记录耗时分布)════════" +times=() +ok=0 +for i in $(seq 1 12); do + out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 25 \ + -H 'User-Agent: LanRemote/1.8.0' https://relay.xxmxxs.top/v1/health) + code=$(echo "$out" | awk '{print $1}') + ms=$(echo "$out" | awk '{printf "%d", $2*1000}') + times+=("$ms") + [ "$code" = "200" ] && ok=$((ok+1)) + printf " %2d: HTTP %-4s %5sms\n" "$i" "$code" "$ms" + sleep 1 +done +echo " 可用 $ok/12" +echo " 耗时: $(printf '%s\n' "${times[@]}" | sort -n | awk '{a[NR]=$1} END {printf "最小 %dms 中位 %dms 最大 %dms", a[1], a[int(NR/2)+1], a[NR]}')" + +echo +echo "════════ 5. 每个候选 edge IP 的可达性(挑最快的)════════" +python3 - <<'PY' +import socket, time +ips = ["198.41.192.47", "198.41.192.57", "198.41.192.67", "198.41.192.107", + "198.41.192.167", "198.41.192.227", "198.41.200.23", "198.41.200.33", + "198.41.200.43", "198.41.200.63", "198.41.200.113", "198.41.200.193"] +rows = [] +for ip in ips: + s = socket.socket(); s.settimeout(4) + t = time.perf_counter() + try: + s.connect((ip, 7844)) + ms = (time.perf_counter() - t) * 1000 + rows.append((ms, ip, "通")) + except OSError as e: + rows.append((99999, ip, "不通")) + finally: + s.close() +rows.sort() +for ms, ip, st in rows: + if st == "通": + print(" %-16s %6.0f ms" % (ip, ms)) + else: + print(" %-16s 不通" % ip) +good = [r for r in rows if r[2] == "通"] +print(" => %d/%d 可达,最快 %s" % (len(good), len(rows), good[0][1] if good else "无")) +PY + +echo +echo "════════ 6. 隧道日志里 i/o timeout 的最近频率 ════════" +for kw in 'Registered tunnel connection' 'Lost connection' 'i/o timeout' 'context deadline'; do + n=$(tail -800 /root/lanremote-relay/tunnel.log 2>/dev/null | grep -c "$kw") + printf " %-34s %s 次(最近 800 行)\n" "$kw" "$n" +done diff --git a/_build/fix_lan_hijack.py b/_build/fix_lan_hijack.py new file mode 100644 index 0000000..25928ec --- /dev/null +++ b/_build/fix_lan_hijack.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +r'''修「局域网连接被中转劫持」的 bug。 + +用户报的现象(手表截图): + 局域网能搜到电脑,IP / 端口 / 配对码都填对了, + 点连接**没有任何反应**,一点反应都没有。 + +根因(MainActivity.java 的 connect()): + + boolean usedLan = !relayUrl().isEmpty() ? false : true; + if (!relay.isEmpty()) { + reply = client.connectViaRelay(...); // ← 走中转 + } else { + reply = client.connect(host, port, ...); // ← 才走局域网 + } + + **只看「中转设置里填没填地址」,填了就一律走中转, + 完全无视用户填的局域网 IP 和端口。** + + 而中转超时是 40 秒(40000ms)—— + 于是用户点完之后界面卡在「正在连接…」四十秒, + 在他看来就是"没有任何反应"。 + + 这个 bug 影响两条路: + 1. 「局域网连接」页面填 IP 直连(connectLanTyped) + 2. 首页「已配对的电脑」点一下重连(quickConnect) + +修法: + 给 connect() 加一个 preferLan 参数。明确要走局域网时: + 先直连 host:port(超时给短一点,6 秒,局域网本来该是毫秒级) + 直连不成**再退回中转**(保证最终能连上,不让用户卡死) + 没有 preferLan 时保持原来的行为(走中转,连上后若同网段再切直连)。 +''' +from __future__ import annotations + +import re +import sys +from pathlib import Path + +SRC = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote" + r"\android\src\com\lanremote\watch\MainActivity.java") + +# ---------------------------------------------------------------- 1. 重载 +OLD_SIG = """ private void connect(final String host, final int port, final String code, + final String token, final String key) { + if (busy) { + return; + }""" + +NEW_SIG = """ private void connect(final String host, final int port, final String code, + final String token, final String key) { + connect(host, port, code, token, key, false); + } + + /** + * @param preferLan 明确要求"先走局域网"。 + * + *
用户点「局域网连接」填了 IP,或者点首页已配对电脑时, + * 应该先按他给的地址直连。原来的实现只看中转地址填没填 —— + * 填了就一律走中转,**完全无视用户填的局域网 IP**, + * 而中转超时 40 秒,用户点完只看到「正在连接…」,以为没反应。 + * + *
设 true 时:先直连(局域网该是毫秒级,超时给 6 秒),
+ * 不成再退回中转,保证最终能连上。
+ */
+ private void connect(final String host, final int port, final String code,
+ final String token, final String key,
+ final boolean preferLan) {
+ if (busy) {
+ return;
+ }"""
+
+
+# ---------------------------------------------------------------- 2. 分支
+OLD_BRANCH = """ final String relay = relayUrl();
+ Map 读不到手表自己的网卡时返回 true(保守:宁可按原地址试一次,
+ * 也不要因为拿不到权限就误判成"过期")。
+ */
+ private boolean hostInMySubnet(String host) {
+ if (host == null || host.isEmpty()) {
+ return true;
+ }
+ java.util.List 必须显著大于服务端的 hold 时间,否则会在服务端正常返回\n"
+ " * 之前自己先超时 —— 那就变成\"每次轮询都失败\"了。\n"
+ " * 90 秒 = 60 + 30 秒余量。\n"
+ " */\n"
+ " private static final int POLL_TIMEOUT_MS = 90000;",
+ ),
+]
+
+
+def main() -> int:
+ print(" 改这三处:")
+ ok_all = True
+ for rel, old, new in EDITS:
+ path = ROOT / rel
+ if not path.is_file():
+ print(" ✗ %s 不存在" % rel)
+ ok_all = False
+ continue
+ raw = path.read_bytes()
+ had_bom = raw.startswith(BOM)
+ text = raw.decode("utf-8-sig")
+
+ if old not in text:
+ if new.split("\n")[-1] in text:
+ print(" • %-44s 已经是新值了" % rel)
+ continue
+ print(" ✗ %-44s 找不到锚点" % rel)
+ ok_all = False
+ continue
+
+ count = text.count(old)
+ if count != 1:
+ print(" ! %-44s 锚点出现 %d 次" % (rel, count))
+ ok_all = False
+ continue
+
+ text = text.replace(old, new, 1)
+ data = text.encode("utf-8")
+ if had_bom or path.suffix == ".ps1":
+ data = BOM + data
+ path.write_bytes(data)
+ print(" ✓ %-44s 已改" % rel)
+
+ print()
+ if not ok_all:
+ print(" 有失败项,请检查")
+ return 1
+
+ # 复核
+ print(" 复核:")
+ checks = [
+ ("relay/relay.py", 'os.environ.get("RELAY_POLL", "60")', "服务端 60 秒"),
+ ("windows/lanremote/relay.py", "POLL_TIMEOUT = 90.0", "被控端 90 秒"),
+ ("android/src/com/lanremote/watch/RelayTransport.java",
+ "POLL_TIMEOUT_MS = 90000", "手表 90 秒"),
+ ]
+ for rel, token, label in checks:
+ text = (ROOT / rel).read_text(encoding="utf-8-sig")
+ mark = "✓" if token in text else "✗"
+ print(" %s %s" % (mark, label))
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/_build/poll60_patch2.py b/_build/poll60_patch2.py
new file mode 100644
index 0000000..69f60d3
--- /dev/null
+++ b/_build/poll60_patch2.py
@@ -0,0 +1,138 @@
+# -*- coding: utf-8 -*-
+r'''把长轮询从 15 秒改成 60 秒(服务端 + 两个客户端同步改)。
+
+== 为什么这是这一轮最有效的优化 ==
+
+隧道每次请求要多花 1~2 秒(CF 边缘路由 + 回源)。实测:
+
+ 本机直连 60 秒的 poll -> 60.0s
+ 走公网隧道同一个 poll -> 61.5~62.0s 只多 1.5~2 秒
+
+看着不多,但**这个开销是按"次数"收的**:
+
+ 15 秒一循环 -> 一天 5760 次/端 -> 光路费就 2.4~3.2 小时
+ 60 秒一循环 -> 一天 1440 次/端 -> 光路费就 0.6~0.8 小时
+
+而且请求越频繁,"正好撞上丢包/重连窗口"的概率越大。
+拉长到 60 秒,暴露面直接降到 1/4。
+
+== 为什么不会让命令变慢 ==
+
+长轮询是**有数据立刻返回**(relay.py 里用 device.event 唤醒),
+不是"等满 60 秒才看一眼"。拉长超时只是减少"空转重开"的次数,
+真正下命令时的延迟不变。
+
+实测 60 秒长轮询穿过隧道 4/4 成功;Cloudflare 单请求上限是 100 秒,
+60 秒留了 40 秒余量。
+
+== 要同步改的三处 ==
+
+ 1. relay/relay.py POLL_SECONDS 默认 25 -> 60
+ 2. windows/lanremote/relay.py POLL_TIMEOUT 40 -> 90
+ 3. android .../RelayTransport.java POLL_TIMEOUT_MS 40000 -> 90000
+
+客户端超时必须显著大于服务端 hold 时间,否则会在服务端正常返回之前
+自己先超时 —— 那会变成"每次轮询都失败"的灾难。
+'''
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
+BOM = b"\xef\xbb\xbf"
+
+RELAY_OLD = 'POLL_SECONDS = float(os.environ.get("RELAY_POLL", "25"))'
+RELAY_NEW = """# 长轮询挂多久。
+# 25 -> 60:隧道每次请求要多花 1~2 秒(CF 边缘路由 + 回源),
+# 而这个开销是按"次数"收的。60 秒一循环把请求数降到 1/4,
+# 暴露在丢包/重连窗口里的概率也降到 1/4。
+# **不会让命令变慢**:有数据时立刻返回(靠 device.event 唤醒),
+# 拉长只是减少空转重开。
+# Cloudflare 单请求上限 100 秒,60 留了 40 秒余量(实测 4/4 通过)。
+POLL_SECONDS = float(os.environ.get("RELAY_POLL", "60"))"""
+
+AGENT_OLD = "POLL_TIMEOUT = 40.0"
+AGENT_NEW = """# 长轮询单次请求的超时。
+# 必须显著大于服务端的 hold 时间(现在 60 秒),否则会在服务端
+# 正常返回之前自己先超时 —— 那就变成"每次轮询都失败"了。
+# 90 = 60 + 30 秒余量。
+POLL_TIMEOUT = 90.0"""
+
+WATCH_OLD = (
+ " /** 长轮询:中转那边挂 25 秒,客户端超时给宽一点 */\n"
+ " private static final int POLL_TIMEOUT_MS = 40000;"
+)
+WATCH_NEW = (
+ " /**\n"
+ " * 长轮询:中转那边挂 60 秒,客户端超时给宽一点。\n"
+ " *\n"
+ " * 必须显著大于服务端的 hold 时间,否则会在服务端正常返回\n"
+ " * 之前自己先超时 —— 那就变成\"每次轮询都失败\"了。\n"
+ " * 90 秒 = 60 + 30 秒余量。\n"
+ " */\n"
+ " private static final int POLL_TIMEOUT_MS = 90000;"
+)
+
+EDITS = [
+ ("relay/relay.py", RELAY_OLD, RELAY_NEW, "服务端 60 秒"),
+ ("windows/lanremote/relay.py", AGENT_OLD, AGENT_NEW, "被控端 90 秒"),
+ ("android/src/com/lanremote/watch/RelayTransport.java",
+ WATCH_OLD, WATCH_NEW, "手表 90 秒"),
+]
+
+
+def main() -> int:
+ print(" 改这三处:")
+ ok_all = True
+ for rel, old, new, label in EDITS:
+ path = ROOT / rel
+ if not path.is_file():
+ print(" ✗ %s 不存在" % rel)
+ ok_all = False
+ continue
+ raw = path.read_bytes()
+ had_bom = raw.startswith(BOM)
+ text = raw.decode("utf-8-sig")
+
+ if old not in text:
+ if new.strip().splitlines()[-1] in text:
+ print(" • %-44s 已经是新值了" % rel)
+ continue
+ print(" ✗ %-44s 找不到锚点" % rel)
+ ok_all = False
+ continue
+
+ count = text.count(old)
+ if count != 1:
+ print(" ! %-44s 锚点出现 %d 次" % (rel, count))
+ ok_all = False
+ continue
+
+ text = text.replace(old, new, 1)
+ data = text.encode("utf-8")
+ if had_bom or path.suffix == ".ps1":
+ data = BOM + data
+ path.write_bytes(data)
+ print(" ✓ %-44s 已改" % rel)
+
+ print()
+ if not ok_all:
+ print(" 有失败项,请检查")
+ return 1
+
+ print(" 复核:")
+ checks = [
+ ("relay/relay.py", 'os.environ.get("RELAY_POLL", "60")'),
+ ("windows/lanremote/relay.py", "POLL_TIMEOUT = 90.0"),
+ ("android/src/com/lanremote/watch/RelayTransport.java",
+ "POLL_TIMEOUT_MS = 90000"),
+ ]
+ for rel, token in checks:
+ text = (ROOT / rel).read_text(encoding="utf-8-sig")
+ print(" %s %s" % ("✓" if token in text else "✗", rel))
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/_build/test_longpoll.sh b/_build/test_longpoll.sh
new file mode 100644
index 0000000..f29d192
--- /dev/null
+++ b/_build/test_longpoll.sh
@@ -0,0 +1,78 @@
+#!/bin/bash
+# 验证长轮询能不能穿过 Cloudflare 隧道跑满 60 秒。
+#
+# 为什么要先验:
+# Cloudflare 对单个 HTTP 请求有 **100 秒**上限,超了返回 524。
+# 我们打算把长轮询从 15 秒拉到 60 秒 ——
+# 如果隧道/CF 那边撑不住,agent 会一直断线重连,反而更糟。
+# 所以先拿真实的 poll 接口量一遍。
+#
+# 做法:
+# 1. 注册一台临时设备,拿到 token
+# 2. 用 token 打 /v1/agent/poll(公网地址),量它挂了多久
+# 3. 对比本机直连和中转两条路
+set -u
+
+BASE=https://relay.xxmxxs.top
+LOCAL=http://127.0.0.1:8080
+UA='LanRemote/1.8.0'
+
+echo "════════ 1. 注册一台临时设备 ════════"
+DEV="probe-$(date +%s)"
+REG=$(curl -s --max-time 30 -X POST "$BASE/v1/agent/register" \
+ -H 'Content-Type: application/json' \
+ -H "User-Agent: $UA" \
+ -d "{\"deviceId\":\"$DEV\",\"code\":\"999999\",\"name\":\"poll-probe\",\"version\":\"1.8.0\"}")
+echo " $REG" | head -c 200
+echo
+
+TOKEN=$(echo "$REG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null)
+if [ -z "$TOKEN" ]; then
+ echo " [失败] 没拿到 token,后面的测试没法做"
+ exit 1
+fi
+echo " token 长度 ${#TOKEN}"
+
+echo
+echo "════════ 2. 本机直连 poll(基准,应等于服务端设定的秒数)════════"
+for i in 1 2; do
+ out=$(curl -s -o /tmp/poll_local.bin -w '%{http_code} %{time_total}' \
+ --max-time 120 -X POST "$LOCAL/v1/agent/poll" \
+ -H "X-Token: $TOKEN" -H "User-Agent: $UA")
+ size=$(wc -c < /tmp/poll_local.bin 2>/dev/null || echo 0)
+ echo " 第 $i 次: 码/耗时 = $(echo "$out" | awk '{printf "%s %.1fs", $1, $2}') 返回 $size 字节"
+done
+
+echo
+echo "════════ 3. 走公网(Cloudflare 隧道)poll ════════"
+echo " 这是关键:能不能挂满 60 秒不断"
+ok=0
+for i in 1 2 3; do
+ out=$(curl -s -o /tmp/poll_pub.bin -w '%{http_code} %{time_total}' \
+ --max-time 130 -X POST "$BASE/v1/agent/poll" \
+ -H "X-Token: $TOKEN" -H "User-Agent: $UA")
+ code=$(echo "$out" | awk '{print $1}')
+ secs=$(echo "$out" | awk '{printf "%.1f", $2}')
+ size=$(wc -c < /tmp/poll_pub.bin 2>/dev/null || echo 0)
+ mark=""
+ [ "$code" = "200" ] && mark="✓"
+ [ "$code" = "524" ] && mark="**CF 超时了**"
+ printf " 第 %d 次: 码 %s 耗时 %ss 返回 %s 字节 %s\n" "$i" "$code" "$secs" "$size" "$mark"
+ [ "$code" = "200" ] && ok=$((ok + 1))
+ sleep 1
+done
+echo " 成功 $ok/3"
+
+echo
+echo "════════ 4. 服务端当前的长轮询设定 ════════"
+grep -E '^REMOTE_POLL|Environment=RELAY_POLL' /etc/systemd/system/lanremote-relay.service 2>/dev/null | sed 's/^/ /'
+echo " 实际生效: $(curl -s --max-time 10 $LOCAL/v1/health | python3 -c 'import sys,json; d=json.load(sys.stdin); print("(health 没报这个字段)")' 2>/dev/null)"
+tail -40 /root/lanremote-relay/relay.log 2>/dev/null | grep '长轮询' | tail -1 | sed 's/^/ /'
+
+echo
+echo "════════ 5. 清掉临时设备 ════════"
+KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null)
+curl -s --max-time 20 -X POST "$LOCAL/v1/admin/delete" \
+ -H "X-Admin: $KEY" -H 'Content-Type: application/json' \
+ -d "{\"deviceId\":\"$DEV\"}" | head -c 120
+echo
diff --git a/_build/test_poll60.sh b/_build/test_poll60.sh
new file mode 100644
index 0000000..ef8d60c
--- /dev/null
+++ b/_build/test_poll60.sh
@@ -0,0 +1,102 @@
+#!/bin/bash
+# 把长轮询从 15 秒拉到 60 秒,实测能不能穿过隧道。
+#
+# 为什么这是核心优化:
+# 隧道每次请求额外加 1~2 秒(连接建立 + CF 内部路由)。
+# 15 秒一循环 -> 一天 5760 次/端,每次都要吃那 1~2 秒。
+# 60 秒一循环 -> 一天 1440 次/端,开销直接摊薄 4 倍。
+#
+# 而且**命令延迟不会变差**:长轮询是"有数据立刻返回",
+# 不是"等满 60 秒才返回"。拉长只是减少空转次数。
+#
+# Cloudflare 对单请求有 100 秒上限(超了 524),60 秒留 40 秒余量。
+set -u
+
+BASE=https://relay.xxmxxs.top
+LOCAL=http://127.0.0.1:8080
+UA='LanRemote/1.8.0'
+SVC=/etc/systemd/system/lanremote-relay.service
+BACKUP=/root/lanremote-relay.service.bak.$(date +%s)
+
+echo "════════ 1. 备份并改成 60 秒 ════════"
+cp -a "$SVC" "$BACKUP"
+echo " 备份: $BACKUP"
+sed -i 's/^Environment=RELAY_POLL=.*/Environment=RELAY_POLL=60/' "$SVC"
+grep 'RELAY_POLL' "$SVC" | sed 's/^/ /'
+systemctl daemon-reload
+systemctl restart lanremote-relay
+sleep 8
+echo " 中转: $(systemctl is-active lanremote-relay)"
+curl -s --max-time 10 "$LOCAL/v1/health" | head -c 90
+echo
+echo " 等隧道重新连上…"
+for i in $(seq 1 15); do
+ c=$(curl -s -o /dev/null -w '%{http_code}' --max-time 12 -H "User-Agent: $UA" "$BASE/v1/health")
+ printf " 第 %d 次: %s\n" "$i" "$c"
+ [ "$c" = "200" ] && break
+ sleep 3
+done
+
+echo
+echo "════════ 2. 本机直连(基准,应该约 60 秒)════════"
+DEV="probe60-$(date +%s)"
+REG=$(curl -s --max-time 30 -X POST "$LOCAL/v1/agent/register" \
+ -H 'Content-Type: application/json' \
+ -d "{\"deviceId\":\"$DEV\",\"code\":\"888888\",\"name\":\"probe60\",\"version\":\"1.8.0\"}")
+TOKEN=$(echo "$REG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null)
+echo " token ${#TOKEN} 字符"
+for i in 1 2; do
+ out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 120 \
+ -X POST "$LOCAL/v1/agent/poll" -H "X-Token: $TOKEN")
+ echo " 第 $i 次: $(echo "$out" | awk '{printf "码 %s 耗时 %.1fs", $1, $2}')"
+done
+
+echo
+echo "════════ 3. 走公网(关键)════════"
+ok=0
+for i in 1 2 3 4; do
+ out=$(curl -s -o /dev/null -w '%{http_code} %{time_total}' --max-time 130 \
+ -X POST "$BASE/v1/agent/poll" -H "X-Token: $TOKEN" -H "User-Agent: $UA")
+ code=$(echo "$out" | awk '{print $1}')
+ secs=$(echo "$out" | awk '{printf "%.1f", $2}')
+ printf " 第 %d 次: 码 %-4s 耗时 %ss" "$i" "$code" "$secs"
+ if [ "$code" = "200" ]; then
+ echo " ✓"
+ ok=$((ok + 1))
+ elif [ "$code" = "524" ]; then
+ echo " **Cloudflare 100 秒上限挡了**"
+ else
+ echo " ✗"
+ fi
+ sleep 1
+done
+echo " 成功 $ok/4"
+
+echo
+echo "════════ 4. 顺便测「有数据时立刻返回」有没有退化 ════════"
+echo " (往队列里塞点东西,看 poll 是不是马上就回来)"
+# 用 client 侧塞:先建会话太麻烦,改成直接看日志时间戳
+echo " 跳过(需要完整会话,后面用真机验证)"
+
+echo
+echo "════════ 5. 结论 ════════"
+if [ "$ok" -ge 3 ]; then
+ echo " ✓ 60 秒长轮询能在隧道上跑通,可以改成这个值"
+else
+ echo " ✗ 60 秒撑不住,回滚到 15 秒"
+ cp -a "$BACKUP" "$SVC"
+ systemctl daemon-reload
+ systemctl restart lanremote-relay
+ echo " 已回滚"
+fi
+
+echo
+echo " 服务端设定现在是: $(grep -o 'RELAY_POLL=[0-9]*' "$SVC")"
+echo " 日志: $(tail -30 /root/lanremote-relay/relay.log | grep '长轮询' | tail -1)"
+
+echo
+echo "════════ 6. 清掉临时设备 ════════"
+KEY=$(cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null)
+curl -s --max-time 20 -X POST "$LOCAL/v1/admin/delete" -H "X-Admin: $KEY" \
+ -H 'Content-Type: application/json' -d "{\"deviceId\":\"$DEV\"}" | head -c 80
+echo
diff --git a/android/src/com/lanremote/watch/MainActivity.java b/android/src/com/lanremote/watch/MainActivity.java
index 9c5faf1..675211c 100644
--- a/android/src/com/lanremote/watch/MainActivity.java
+++ b/android/src/com/lanremote/watch/MainActivity.java
@@ -658,7 +658,16 @@ public class MainActivity extends Activity implements RemoteClient.Listener {
final String token = Json.str(device, "token", "");
final String label = Json.str(device, "label", host);
- Button button = bigButton(label + "\n" + host + ":" + port, C_CARD, 14);
+ // 保存的地址可能已经过期(电脑换了网络/网段)。
+ // 和手表不在同一网段时直接在按钮上标出来 ——
+ // 不然用户点下去只会等 6 秒直连失败 + 40 秒中转超时,
+ // 完全不知道问题出在哪儿。
+ String stale = "";
+ if (!host.isEmpty() && !hostInMySubnet(host)) {
+ stale = " ⚠ 地址可能已过期";
+ }
+ Button button = bigButton(label + "\n" + host + ":" + port + stale,
+ C_CARD, 14);
button.setGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(58));
@@ -2191,8 +2200,10 @@ public class MainActivity extends Activity implements RemoteClient.Listener {
lastCode = code;
lanDirect = true; // 断了自动切回中转
appendLog("局域网直连 " + host + ":" + port);
+ // preferLan = true:用户就是冲着"局域网直连"来的,
+ // 必须真的直连,不能因为有中转地址就偷偷走中转。
connect(host, port, code, null,
- DeviceStore.deviceKey(host, port, store.deviceId()));
+ DeviceStore.deviceKey(host, port, store.deviceId()), true);
}
// ==================================================================
@@ -2363,7 +2374,42 @@ public class MainActivity extends Activity implements RemoteClient.Listener {
// 不再往输入框里写东西(那两个框已经删了)——
// 直接连。host/port 对中转路径没有意义,
// 但 key 仍然用来区分"哪台已配对的电脑"。
- connect(host, port, null, token, key);
+ //
+ // 先判断这个保存下来的地址还有没有意义:
+ // 电脑换过网络的话(比如从 192.168.2.x 换到 192.168.10.x),
+ // 直连必然失败。那就别浪费那 6 秒,直接走中转 ——
+ // 中转连上后会报回电脑的最新局域网地址,记录自动刷新。
+ boolean sameNet = host.isEmpty() || hostInMySubnet(host);
+ if (!sameNet) {
+ appendLog("记录的地址 " + host + " 和手表不在同一网段"
+ + "(电脑可能换了网络),直接走中转查最新地址");
+ toast("电脑地址已过期,正在通过中转查询最新地址…");
+ }
+ // preferLan = true 时先按记录里的地址直连,失败自动退回中转。
+ connect(host, port, null, token, key, sameNet);
+ }
+
+ /**
+ * host 是不是和手表在同一个 /24 网段。
+ *
+ * 读不到手表自己的网卡时返回 true(保守:宁可按原地址试一次,
+ * 也不要因为拿不到权限就误判成"过期",那会让本可以直连的场景
+ * 白走一趟中转)。
+ */
+ private boolean hostInMySubnet(String host) {
+ if (host == null || host.isEmpty()) {
+ return true;
+ }
+ java.util.List 用户点「局域网连接」填了 IP,或者点首页已配对电脑时,
+ * 应该先按他给的地址直连。原来的实现只看中转地址填没填 ——
+ * 填了就一律走中转,**完全无视用户填的局域网 IP**,
+ * 而中转超时 40 秒,用户点完只看到「正在连接…」,以为没反应。
+ *
+ * 设 true 时:先直连(局域网该是毫秒级,超时给 6 秒),
+ * 不成再退回中转,保证最终能连上。
+ */
+ private void connect(final String host, final int port, final String code,
+ final String token, final String key,
+ final boolean preferLan) {
if (busy) {
return;
}
@@ -2495,7 +2558,32 @@ public class MainActivity extends Activity implements RemoteClient.Listener {
try {
final String relay = relayUrl();
Map 必须显著大于服务端的 hold 时间,否则会在服务端正常返回
+ * 之前自己先超时 —— 那就变成"每次轮询都失败"了。
+ * 90 秒 = 60 + 30 秒余量。
+ */
+ private static final int POLL_TIMEOUT_MS = 90000;
/** 发送/接收的单次请求超时 */
private static final int SEND_TIMEOUT_MS = 30000;
/** 网络出错后的退避 */
diff --git a/relay/relay.py b/relay/relay.py
index 98fc5eb..242b371 100644
--- a/relay/relay.py
+++ b/relay/relay.py
@@ -68,7 +68,14 @@ BIND = os.environ.get("RELAY_BIND", "127.0.0.1")
DATA_DIR = Path(os.environ.get("RELAY_DATA", "/root/lanremote-relay"))
# 长轮询最多挂多久。太长会占着 Cloudflare 的连接,太短则空转请求多。
-POLL_SECONDS = float(os.environ.get("RELAY_POLL", "25"))
+# 长轮询挂多久。
+# 25 -> 60:隧道每次请求要多花 1~2 秒(CF 边缘路由 + 回源),
+# 而这个开销是按"次数"收的。60 秒一循环把请求数降到 1/4,
+# 暴露在丢包/重连窗口里的概率也降到 1/4。
+# **不会让命令变慢**:有数据时立刻返回(靠 device.event 唤醒),
+# 拉长只是减少空转重开。
+# Cloudflare 单请求上限 100 秒,60 留了 40 秒余量(实测 4/4 通过)。
+POLL_SECONDS = float(os.environ.get("RELAY_POLL", "60"))
# 单个设备最多缓多少字节没发出去。手表半天不取就丢最旧的,保住内存。
MAX_QUEUE_BYTES = int(os.environ.get("RELAY_QUEUE_BYTES",
diff --git a/windows/lanremote/relay.py b/windows/lanremote/relay.py
index 6d398c1..886ac39 100644
--- a/windows/lanremote/relay.py
+++ b/windows/lanremote/relay.py
@@ -31,7 +31,11 @@ import urllib.parse
import urllib.request
# 长轮询:中转那边挂 25 秒,客户端超时给宽一点
-POLL_TIMEOUT = 40.0
+# 长轮询单次请求的超时。
+# 必须显著大于服务端的 hold 时间(现在 60 秒),否则会在服务端
+# 正常返回之前自己先超时 —— 那就变成"每次轮询都失败"了。
+# 90 = 60 + 30 秒余量。
+POLL_TIMEOUT = 90.0
# 单次请求体上限(一个 256KB 分块 Base64 后约 350KB,给足余量)
MAX_BODY = 8 * 1024 * 1024
# 网络断了之后的退避