Files
ykxmz/_build/fix_lan_hijack.py
xmz7891 b1119d0da6 修「局域网连接被中转劫持」+ 长轮询 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 分钟对照确认。
2026-10-07 14:11:33 +08:00

173 lines
7.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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 明确要求"先走局域网"。
*
* <p>用户点「局域网连接」填了 IP,或者点首页已配对电脑时,
* 应该先按他给的地址直连。原来的实现只看中转地址填没填 ——
* 填了就一律走中转,**完全无视用户填的局域网 IP**,
* 而中转超时 40 秒,用户点完只看到「正在连接…」,以为没反应。
*
* <p>设 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<String, Object> reply;
if (!relay.isEmpty()) {"""
NEW_BRANCH = """ final String relay = relayUrl();
Map<String, Object> reply;
if (preferLan) {
// ---- 用户明确要走局域网:先直连,失败再退中转 ----
// 局域网直连本该是毫秒级,所以超时给短一点。
// 原来这里会走中转(40 秒超时),用户以为卡死了。
appendLog("局域网直连 " + host + ":" + port + " …");
reply = client.connect(host, port, code, token,
MainActivity.this, 6000);
ok = Json.bool(reply, "ok", false);
if (ok) {
usedLan = true;
} else if (!relay.isEmpty()) {
String lanWhy = Json.str(reply, "error",
"局域网直连失败");
appendLog("局域网直连不成(" + lanWhy
+ "),改走中转…");
ui.post(new Runnable() {
@Override
public void run() {
setStatus("局域网连不上,改用中转…", C_WARN);
}
});
reply = client.connectViaRelay(relay, code, token,
MainActivity.this, 40000);
usedLan = false;
}
} else if (!relay.isEmpty()) {"""
# ---------------------------------------------------------------- 3. 调用方
OLD_QUICK = """ connect(host, port, null, token, key);
}"""
NEW_QUICK = """ // preferLan = true:先按记录里的地址直连。
// 记录里的地址可能是旧的(换过网络),直连会失败 ——
// 那时自动退回中转,中转会报回电脑的最新局域网地址并刷新记录。
connect(host, port, null, token, key, true);
}"""
OLD_TYPED = """ connect(host, port, code, null,
DeviceStore.deviceKey(host, port, store.deviceId()));"""
NEW_TYPED = """ // preferLan = true:用户就是冲着"局域网直连"来的,
// 必须真的直连,不能因为有中转地址就偷偷走中转。
connect(host, port, code, null,
DeviceStore.deviceKey(host, port, store.deviceId()), true);"""
def patch(text: str, old: str, new: str, label: str) -> tuple:
if new.split("\n")[0] in text and label != "分支":
# 已经打过补丁的粗略判断
pass
if old not in text:
print(" ✗ 没找到锚点:%s" % label)
return text, False
count = text.count(old)
if count != 1:
print(" ! 锚点出现 %d 次(期望 1):%s" % (count, label))
return text, False
text = text.replace(old, new, 1)
print(" ✓ %s" % label)
return text, True
def main() -> int:
text = SRC.read_text(encoding="utf-8")
original = text
print(" 打补丁:")
ok_all = True
for old, new, label in (
(OLD_SIG, NEW_SIG, "connect() 加重载和 preferLan 参数"),
(OLD_QUICK, NEW_QUICK, "quickConnect 传 preferLan=true"),
(OLD_TYPED, NEW_TYPED, "connectLanTyped 传 preferLan=true"),
(OLD_BRANCH, NEW_BRANCH, "走局域网的分支"),
):
text, ok = patch(text, old, new, label)
ok_all = ok_all and ok
if not ok_all:
print()
print(" 有锚点没匹配,没有写回")
return 1
SRC.write_text(text, encoding="utf-8", newline="\n")
print()
print(" 已写回,文件 %d -> %d 行"
% (original.count("\n") + 1, text.count("\n") + 1))
return 0
if __name__ == "__main__":
sys.exit(main())