CHANGELOG 加 1.8.1

This commit is contained in:
xmz7891
2026-10-07 14:24:06 +08:00
parent b1119d0da6
commit 46196c3618
5 changed files with 226 additions and 148 deletions
+21
View File
@@ -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
**这一版主要是「修看不见的稳定性问题」。**
-65
View File
@@ -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 分钟对照确认。
+98
View File
@@ -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
+95 -80
View File
@@ -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 <<EOF
[Unit]
Description=LanRemote Cloudflare 具名隧道
After=network-online.target lanremote-relay.service
Wants=network-online.target lanremote-relay.service
[Service]
Type=simple
ExecStart=/usr/local/bin/cloudflared tunnel --protocol http2 --ha-connections 2 $BASE
Restart=always
RestartSec=5
MemoryMax=160M
StandardOutput=append:/root/lanremote-relay/tunnel.log
StandardError=append:/root/lanremote-relay/tunnel.log
[Install]
WantedBy=multi-user.target
EOF
echo "════════ 5. 回滚到备份 ════════"
cp -a "$BACKUP" "$SVC"
systemctl daemon-reload
systemctl start lanremote-tunnel
sleep 30
echo " 隧道: $(systemctl is-active lanremote-tunnel)"
echo " 连接数: $(curl -s --max-time 5 http://127.0.0.1:20241/metrics | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')"
systemctl restart lanremote-tunnel
sleep 20
echo " 参数: $(grep -oE '--protocol [a-z0-9]+|--ha-connections [0-9]+|--edge-ip-version [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}')"
echo " 落点:"
curl -s --max-time 5 http://127.0.0.1:20241/metrics 2>/dev/null \
| grep 'server_locations.*} 1' | sed 's/^/ /'
echo " 备份: $BACKUP"
@@ -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() {