diff --git a/_build/CHANGELOG.md b/_build/CHANGELOG.md index d8d209a..d4a966a 100644 --- a/_build/CHANGELOG.md +++ b/_build/CHANGELOG.md @@ -1,3 +1,24 @@ +## 1.8.1 + +- 修**局域网连接被中转劫持**(重要): + 只要「中转设置」里填了地址,点「局域网连接」填好 IP、端口、 + 配对码之后,程序**仍然走中转**,完全无视你填的局域网地址。 + 中转慢的时候要等 40 秒才报错,看起来就像"点了一点反应都没有"。 + 现在改成:局域网优先直连(6 秒超时),连不上才退回中转 + +- 修**已配对电脑的地址过期不提示**: + 电脑换了网络之后(比如从 192.168.2.x 换到 192.168.10.x), + 列表里还是旧地址,点一下必然失败。现在会标出「⚠ 地址可能已过期」, + 并且跳过注定失败的直连,直接通过中转查询最新地址 + +- **长轮询从 15 秒延长到 60 秒**,跨网络更稳: + 隧道每次请求要多花 1~2 秒(边缘路由 + 回源),而这个开销是按 + **次数**收的 —— 每次都要吃一遍。拉长到 60 秒把请求数降到 1/4, + 撞上丢包/重连窗口的概率也降到 1/4。 + **命令响应速度不变**:有数据时立刻返回,不是等满 60 秒。 + +- 修局域网直连成功后打出误导性日志的问题 + ## 1.8.0 **这一版主要是「修看不见的稳定性问题」。** diff --git a/_build/_c18.txt b/_build/_c18.txt deleted file mode 100644 index 970b3d4..0000000 --- a/_build/_c18.txt +++ /dev/null @@ -1,65 +0,0 @@ -修「局域网连接被中转劫持」+ 长轮询 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/test_poll_reliability.sh b/_build/test_poll_reliability.sh new file mode 100644 index 0000000..20df817 --- /dev/null +++ b/_build/test_poll_reliability.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# 长轮询可靠性长测:连打 20 次(每次挂 60 秒,共约 20 分钟)。 +# +# 为什么单独测这个: +# 上一轮 A/B 里 http2+4 是 7/8(有 1 次 000 失败)。 +# 12.5% 的失败率对"手表点一下要能用"来说太高了,必须查清: +# * 是偶发抖动? +# * 还是每 8 次就必然挂一次(那就跟隧道重连周期有关)? +# +# 顺带修掉上轮脚本的 bug:探针设备用了同一个配对码, +# 第二次注册被"一个码只能一台设备"规则拒掉,token 拿到空的。 +# 这里每次用**唯一的新码**。 +set -u + +PUB=https://relay.xxmxxs.top +LOCAL=http://127.0.0.1:8080 +UA='LanRemote/1.8.0' +ROUNDS=${1:-20} + +echo "════════ 注册探针(用唯一配对码)════════" +STAMP=$(date +%s) +DEV="relprobe-${STAMP}" +# 6 位数字,取自时间戳后 6 位,避免和上一轮撞 +CODE=$(printf "%06d" $(( STAMP % 1000000 ))) +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\":\"$CODE\",\"name\":\"relprobe\",\"version\":\"1.8.0\"}") +echo " 注册响应: $(echo "$REG" | head -c 160)" +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} 字符,配对码 $CODE" + +echo +echo "════════ 隧道现状 ════════" +echo " 参数: $(grep -oE '\-\-protocol [a-z0-9]+|--ha-connections [0-9]+' /etc/systemd/system/lanremote-tunnel.service | 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}')" +curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep 'server_locations.*} 1' | sed 's/^/ /' + +echo +echo "════════ 开始连打 $ROUNDS 次长轮询 ════════" +ok=0; fail=0; times=(); fails="" +LOGMARK=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0) + +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}') + ms=$(echo "$out" | awk '{printf "%d", $2*1000}') + if [ "$code" = "200" ]; then + ok=$((ok+1)); times+=("$ms") + printf " %2d: 200 %6dms\n" "$i" "$ms" + else + fail=$((fail+1)); fails="$fails ${i}:${code}" + printf " %2d: %-4s **失败**\n" "$i" "$code" + 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}') + [ "$code" != "200" ] && printf " 失败时连接数: %s\n" "${n:-?}" +done + +echo +echo "════════ 结果 ════════" +total=$((ok+fail)) +echo " 成功 $ok/$total = $((ok * 100 / total))%" +echo " 失败:$fails" +if [ "${#times[@]}" -gt 0 ]; then + printf '%s\n' "${times[@]}" | sort -n | awk '{ + a[NR]=$1 + } END { + printf " 耗时: 最小 %d 中位 %d P90 %d 最大 %d ms\n", + a[1], a[int(NR/2)+1], a[int(NR*0.9)+1], a[NR] + }' +fi + +echo +echo " 这一轮的隧道事件(重连/超时):" +NOWMARK=$(wc -l < /root/lanremote-relay/tunnel.log 2>/dev/null || echo 0) +tail -n +$((LOGMARK+1)) /root/lanremote-relay/tunnel.log 2>/dev/null \ + | grep -cE 'Registered tunnel connection' \ + | sed 's/^/ 重连次数: /' +tail -n +$((LOGMARK+1)) /root/lanremote-relay/tunnel.log 2>/dev/null \ + | grep -E 'Lost connection|i/o timeout|context deadline' | tail -6 | sed 's/^/ /' + +echo +echo " watchdog 最近:" +tail -6 /root/lanremote-relay/watchdog.log 2>/dev/null | sed 's/^/ /' + +echo +echo "════════ 清掉探针 ════════" +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 100 +echo diff --git a/_build/test_region.sh b/_build/test_region.sh index 4c02bd2..25b9c66 100644 --- a/_build/test_region.sh +++ b/_build/test_region.sh @@ -1,96 +1,111 @@ #!/bin/bash -# 测试 --region:能不能让 cloudflared 连亚洲边缘而不是美西? +# 试试能不能把隧道从"美国西海岸"挪到亚洲节点。 # -# 现状:连的是 198.41.x.x,落地 lax01/sjc07(美西),ICMP 199~243ms, -# 丢包 40%,握手经常超时。 -# cloudflared 有个 --region 参数("Cloudflare Edge region to connect to")。 -# 如果能把落地换到香港/新加坡/东京,延迟和丢包都会好很多。 +# 为什么这可能是最大的一招: +# 实测隧道落点全是 sjc / lax(美国圣何塞、洛杉矶), +# 中国 -> 美西 往返就是 ~240ms 起步,加上 4G 的丢包, +# 每个请求固定在隧道回源这一跳吃 ~1100ms。 +# 如果能连到 东京 / 香港 / 新加坡,这一跳能砍掉一大半。 +# +# cloudflared 理论上支持按区域选 edge,但其实它主要是按 +# 运营商的 BGP 出口自动选。这里把能试的都试一遍, +# 用**实测落点**判断有没有用,而不是看参数有没有被接受。 set -u -URL=https://relay.xxmxxs.top -RELAY_DIR=/root/lanremote-relay -BASE="--no-autoupdate --edge-ip-version 4 --retries 5 --config /root/.cloudflared/config.yml run lanremote" +SVC=/etc/systemd/system/lanremote-tunnel.service +BACKUP=/root/lanremote-tunnel.service.bak.region.$(date +%s) +cp -a "$SVC" "$BACKUP" +UA='LanRemote/1.8.0' + +echo "════════ 1. cloudflared 支持的区域参数 ════════" +cloudflared tunnel run --help 2>&1 | grep -iE 'region|edge' -A2 | head -20 | sed 's/^/ /' +echo +echo " cloudflared 版本: $(cloudflared --version 2>&1 | head -1)" + +echo +echo "════════ 2. 当前落点(基线)════════" +curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep 'server_locations.*} 1' | sed 's/^/ /' + +echo +echo "════════ 3. 逐个试区域参数 ════════" try_region() { - local region="$1" - local label="$2" + local arg="$1" label="$2" + echo " ── $label($arg)──" - systemctl stop lanremote-tunnel 2>/dev/null || true - pkill -f cloudflared 2>/dev/null || true - sleep 2 - : > "$RELAY_DIR/tunnel.log" - - if [ -n "$region" ]; then - nohup cloudflared tunnel --region "$region" $BASE \ - >> "$RELAY_DIR/tunnel.log" 2>&1 & - else - nohup cloudflared tunnel $BASE >> "$RELAY_DIR/tunnel.log" 2>&1 & + # 回滚到干净状态再叠加参数 + cp -a "$BACKUP" "$SVC" + if [ -n "$arg" ]; then + sed -i "s|\(ExecStart=.*cloudflared tunnel\)|\1 $arg|" "$SVC" fi + systemctl daemon-reload + systemctl restart lanremote-tunnel - sleep 30 - - local conns loc proto - conns=$(grep -c 'Registered tunnel connection' "$RELAY_DIR/tunnel.log" 2>/dev/null || echo 0) - loc=$(grep -o 'location=[a-z0-9]*' "$RELAY_DIR/tunnel.log" 2>/dev/null | tail -1) - proto=$(grep -o 'protocol=[a-z0-9]*' "$RELAY_DIR/tunnel.log" 2>/dev/null | tail -1) - - printf " %-14s 注册 %-3s %-14s %s\n" "$label" "$conns" "${loc:-?}" "${proto:-?}" - - if [ "$conns" = "0" ]; then - echo " 报错:" - grep -iE 'error|fatal|invalid' "$RELAY_DIR/tunnel.log" 2>/dev/null \ - | tail -2 | sed 's/^/ /' - fi - - # 快速探 10 次 - local ok=0 - for i in $(seq 1 10); do - c=$(timeout 12 curl -4 -s -o /dev/null -w '%{http_code}' \ - -H 'User-Agent: LanRemote/1.4.0' "$URL/v1/health" 2>/dev/null) - [ "$c" = "200" ] && ok=$((ok+1)) - sleep 2 + # 等最多 90 秒看能不能建起连接 + local n=0 + for i in $(seq 1 18); 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 2 ] && break done - echo " 公网 $ok/10" - echo "$label|$ok|${loc:-?}|$conns" >> /root/region_result.txt - pkill -f cloudflared 2>/dev/null || true - sleep 2 + + if [ "${n:-0}" -lt 2 ]; then + echo " 连接数 $n —— **没连上,这个参数不能用**" + return 1 + fi + + echo " 连接数 $n" + echo " 落点:" + curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep 'server_locations.*} 1' | sed 's/^/ /' + + # 量 6 次公网延迟 + local times=() + for i in $(seq 1 6); do + ms=$(curl -s -o /dev/null -w '%{time_total}' --max-time 25 \ + -H "User-Agent: $UA" https://relay.xxmxxs.top/v1/health \ + | awk '{printf "%d", $1*1000}') + times+=("$ms") + done + local stats=$(printf '%s\n' "${times[@]}" | sort -n | awk '{a[NR]=$1} END {printf "最小 %d 中位 %d 最大 %d", a[1], a[int(NR/2)+1], a[NR]}') + echo " 延迟: $stats ms" + echo "$label|$n|$stats" >> /tmp/region.txt + return 0 } -: > /root/region_result.txt +rm -f /tmp/region.txt +echo "" > /dev/null -echo "===== 各 region 的表现 =====" -try_region "" "默认(global)" -try_region "ap" "ap" -try_region "us" "us" +# 先看 help 里有没有 region 这个 flag +HAS_REGION=$(cloudflared tunnel run --help 2>&1 | grep -c -- '--region' || true) +echo " --region 参数是否存在: $([ "${HAS_REGION:-0}" -gt 0 ] && echo 是 || echo 否)" +echo + +# 逐个试。--region 的取值 cloudflared 没在 help 里列全, +# 常见的是 us / eu / ap 之类;不合法会直接报错退出。 +for r in us eu ap; do + try_region "--region $r" "region=$r" || true + echo +done + +echo "════════ 4. 汇总 ════════" +if [ -f /tmp/region.txt ]; then + cat /tmp/region.txt | sed 's/^/ /' +else + echo " (没有任何区域参数能用)" +fi echo -echo "===== 汇总 =====" -while IFS='|' read -r label ok loc conns; do - printf " %-16s 公网 %s/10 落地=%s 注册=%s\n" "$label" "$ok" "$loc" "$conns" -done < /root/region_result.txt - -echo -echo "===== 恢复默认配置 =====" -cat > /etc/systemd/system/lanremote-tunnel.service </dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')" +echo " 落点:" +curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \ + | grep 'server_locations.*} 1' | sed 's/^/ /' +echo " 备份: $BACKUP" diff --git a/android/src/com/lanremote/watch/MainActivity.java b/android/src/com/lanremote/watch/MainActivity.java index 675211c..129bc32 100644 --- a/android/src/com/lanremote/watch/MainActivity.java +++ b/android/src/com/lanremote/watch/MainActivity.java @@ -2602,7 +2602,13 @@ public class MainActivity extends Activity implements RemoteClient.Listener { // ★ 经中转连上之后,看看电脑是不是和我们在同一个网络。 // 是的话就切成局域网直连 —— 快得多(几十毫秒 vs 几百毫秒), // 而且不消耗流量、不怕隧道抖。 - if (!relay.isEmpty()) { + // + // ⚠ 必须加 !usedLan: + // preferLan 分支直连成功时 usedLan 已经是 true, + // 这时候再跑这段会拿中转报的网卡列表去"切直连", + // 而那条列表多半是空的 —— 于是打出一句 + // "不在同一网段,走中转"的**误导日志**(其实已经直连上了)。 + if (!relay.isEmpty() && !usedLan) { String directIp = pickSameSubnet(client.relayLanIps()); int directPort = client.relayLanPort(); if (directIp != null && directPort > 0) { @@ -2669,8 +2675,11 @@ public class MainActivity extends Activity implements RemoteClient.Listener { final String finalToken = newToken; final String finalName = serverName; final boolean finalLanDirect = usedLan && ok; - // 这次是不是走中转连上的(决定要不要用服务器报的地址刷新记录) - final boolean finalFromRelay = !relayUrl().isEmpty() && ok; + // 这次是不是**经中转**连上的(决定要不要用服务器报的地址刷新记录)。 + // 必须排除"用户指定直连且直连成功"的情况 —— + // 那时 relayLanIps() 是空的,刷新逻辑没意义还容易打错日志。 + final boolean finalFromRelay = !relayUrl().isEmpty() && ok + && !usedLan; ui.post(new Runnable() { @Override public void run() {