ykxmz 1.5.0:修复手表端 TLS 证书校验 + MTU 修复网络稳定性

被控端
- 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%)
- 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败)
- 每分钟回头找局域网,自动切回快 400 倍的路径
- 画面心跳帧:静止时省流量,客户端仍知道流是活的
- 锁屏时给出可操作的提示

手表端
- 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException
- 连接错误翻译成人话(证书/时间/解析/超时分别提示)
- 经中转连上后用服务器报的地址刷新已配对记录
- 新增局域网连接页面、服务器延迟检测

服务器端
- 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502)
- 网络调优脚本 net-tune.sh(开机自动应用)
- 管理页:批量删除、网络体检、下载区
- 隧道改 HTTP/2 + 双连接

文档
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
This commit is contained in:
xmz7891
2026-10-06 19:01:05 +08:00
commit b62ac865ea
3195 changed files with 203292 additions and 0 deletions
+537
View File
@@ -0,0 +1,537 @@
# -*- coding: utf-8 -*-
"""打「跨网络版」的三件套压缩包。
用户要求:压缩包里三个东西
1. 被控端(Windows 电脑)
2. 服务器端(随身 WiFi / Linux)
3. 控制端(手表 APK)
产物:
dist/LanRemote-跨网络版-1.5.0.zip
"""
from __future__ import annotations
import shutil
import sys
import zipfile
from pathlib import Path
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
VERSION = "1.5.0"
NAME = f"ykxmz-{VERSION}"
AGENT_SRC = ROOT / "dist_exe" / "ykxmz"
# 三份正式文档(许可协议 / 安装前必读 / 安装后须知)
DOCS = ROOT / "docs"
RELAY_SRC = ROOT / "relay"
APK_SRC = ROOT / "dist" / f"ykxmz-watch-{VERSION}.apk"
# 被控端目录里带的 .bat 工具
TOOLS = ("fxfhq.bat", "wlzj.bat", "xffhq.bat")
TOP_README = f"""========================================================
ykxmz {VERSION} · 远程管理工具
========================================================
作者:小麦子
先看这三份:
LICENSE.txt 许可协议(安装前必看,有使用红线)
BEFORE-INSTALL.txt 安装前必读(前信息)
AFTER-INSTALL.txt 安装后须知(后信息)
下面是这个包里有哪三个东西、怎么装。
这个包里三个东西,分别装在三台设备上:
1-被控端(电脑) 装在你**要控制的那台 Windows 电脑**上。
双击 ykxmz.exe 就行,不用装 Python。
2-服务器端(随身WiFi) 装在一台能上外网的 Linux 小机器上(你的随身 WiFi)。
它负责把"不在同一个网络"的电脑和手表接起来。
3-控制端(手表) 装在你的安卓手表上。一个 APK,直接装。
【怎么用】三步
第一步:架服务器端(只做一次)
把「2-服务器端」整个文件夹拷到随身 WiFi 上,执行:
bash install.sh
它会装好中转服务 + Cloudflare 隧道,并打印一个**公网地址**。
记下这个地址(形如 https://relay.你的域名)。
小提示:公网地址每次重启会变。忘了地址就打开
http://<随身WiFi的IP>:8080/url —— 直接返回当前地址。
第二步:装被控端(电脑上,只做一次)
双击 ykxmz.exe
→ 输入 6 位配对码(自己定,比如 789113)
→ 勾上"开机自启"和"放行防火墙"
→ 点确定,会弹一次管理员确认,之后窗口全消失,后台静默运行
告诉它中转地址(这样才跨网络):
ykxmz.exe --relay https://relay.你的域名
第三步:装控制端(手表上)
装 APK → 首页点「中转设置」→ 填同一个中转地址
→ 点「自动搜索电脑」(同局域网)或直接输配对码
→ 连上
【关于配对码:单设备单配对码】
一台电脑第一次设的配对码会被服务器记住。
以后同一台电脑再报别的码,服务器会**改回它原来那个**。
好处:你不会因为重装/误操作把配对码搞乱,手表永远认得出这台电脑。
一个配对码也不能同时属于两台电脑(服务器会拒绝)。
【服务器端能管什么】
浏览器打开: <中转地址>/?key=... 或者直接用管理密码登录
管理密码:120830(可在服务器上改)
能看:哪些设备在线、配对码、流量、会话数
能做:踢掉某个设备的会话、抹掉某个配对码绑定
注意:中转服务器**只是管道**,它不认识传输内容。
配对、令牌校验、权限判断全部由电脑端自己做 ——
所以就算中转机被攻破,也进不了你的电脑。
【每台设备分别看哪个说明】
电脑: 1-agent/README-agent.txt
随身 WiFi:2-server/README-server.txt
手表: 装完打开 App,首页有「连不上?点这里诊断」
"""
AGENT_README = f"""========================================================
被控端(Windows 电脑) 版本 {VERSION}
========================================================
*** 不需要安装 Python ***
ykxmz.exe 是编译好的独立程序,
拷到任何一台 Windows 电脑上双击就能用。
【装】
1) 双击 ykxmz.exe
2) 弹出窗口,输入 6 位配对码(自己定个记得住的,比如 789113)
两个勾:
[x] 设置开机自启
[x] 顺便放行防火墙(要管理员确认一次)
3) 点「确定,开始使用」
→ 弹一次 UAC,点「是」
→ 黑终端一闪,在修防火墙
→ 窗口全消失,转入后台静默运行
(没有托盘图标、没有窗口、没有终端)
【开箱即用:一个地址都不用填】
程序里**已经内置了中转地址**,双击就能用。
内置的是两个,用分号隔开,按顺序尝试:
192.168.2.156:8080;https://relay.xxmxxs.top
电脑和随身 WiFi 在同一个网络 -> 走第一个
(局域网直连,几十毫秒,地址永远不变)
电脑在外面 -> 第一个连不上,自动切第二个
(Cloudflare 隧道,600 毫秒左右)
想改成别的当然也可以,见下一节。
【跨网络:告诉它中转地址】
支持**多个地址**,用分号隔开,按顺序尝试,第一个通的就用它。
推荐这样配(局域网优先,断了自动切隧道):
ykxmz.exe --relay "192.168.2.156:8080;relay.xxmxxs.top"
为什么局域网优先?
* 快:局域网几十毫秒,绕隧道要 600 毫秒左右
* 稳:不依赖隧道,隧道抽风也不影响
* 地址永远不变:不用管隧道地址变来变去
只走隧道也行:
ykxmz.exe --relay https://relay.xxmxxs.top
地址**不用写 http:// 或 https://**,程序会自己判断:
192.168.x.x / 10.x.x.x / *.local -> 按 http 连(局域网是明文)
域名 -> 按 https 连
看看当前状态(会显示用了哪个地址、有几个候选):
ykxmz.exe --relay-status
关掉中转(只走局域网直连):
ykxmz.exe --relay ""
【忘了中转地址?】
如果随身 WiFi 在你身边(同一个网络),浏览器打开:
http://<随身WiFi的IP>:8080/url
它会告诉你当前的公网地址(纯文本,不用登录)。
小技巧:把局域网地址也填进 --relay,电脑在家里就走局域网,
完全不用管隧道地址变没变:
ykxmz.exe --relay "192.168.2.156:8080;relay.xxmxxs.top"
【配对码被别人占了会弹通知】
每台电脑第一次设的配对码会存到**中转服务器**上。
如果另一台电脑也想用同一个码:
1. 服务器拒绝它
2. 那台电脑上**弹通知**:
「该配对码已被使用,请换一个配对码」
3. 它停下等你换码(不会反复弹、不会刷屏)
4. 你在设置里换个新码,它自动接着连上
所以一个配对码同时只能有一台电脑用。
【配对码:单设备单配对码】
这台电脑第一次设的配对码会被服务器记住。
以后就算你重设成别的,服务器也会改回原来那个 ——
手表永远认得出这台电脑。
相关:
--new-code 换一组随机配对码(服务器会按上面的规则处理)
--reset-devices 清空已配对的手表
【目录里这几个文件】
ykxmz.exe 主程序
wlzj.bat 网络自检(连不上时跑它)
xffhq.bat 一键修复防火墙(右键以管理员运行)
fxfhq.bat 放行脚本(程序自动生成的)
【其它命令行】
--setup 重新打开设置窗口
--silent 直接静默运行
--add-device 10 静默运行 + 开放 10 分钟配对
--autostart on|off 开关开机自启
--status 看当前状态
--netcheck 90 网络自检(同 wlzj.bat)
--fix-firewall 提权修复防火墙
【连不上?按这个顺序】
1) 任务管理器里能看到 ykxmz.exe 吗?没有就 --setup
2) 右键 xffhq.bat → 以管理员身份运行
3) 双击 wlzj.bat 做网络自检
4) 手表上点「连不上?点这里诊断」
5) 跨网络的话,确认 --relay-status 显示在线
"""
RELAY_README = f"""========================================================
服务器端(随身 WiFi / Linux) 版本 {VERSION}
========================================================
作用:让**不在同一个网络**的电脑和手表连上。
手表 ──HTTPS──┐
├──> Cloudflare ──> 本机 :8080
电脑 ──HTTPS──┘
本程序是**纯标准库**的 Python,不装任何第三方包。
内存占用约 20MB,加 cloudflared 共约 60MB —— 512MB 的小机器跑得动。
【装】
把本文件夹拷到设备上,然后:
bash install.sh
它会:
1. 检查 python3 / cloudflared(没有就下载)
2. 装两个 systemd 服务并设为开机自启:
lanremote-relay 中转服务
lanremote-tunnel Cloudflare 隧道
3. 打印公网地址和管理页地址
【常用命令】
lanremote-url 打印当前公网地址
lanremote-key 打印管理钥匙
systemctl status lanremote-relay
systemctl status lanremote-tunnel
journalctl -u lanremote-relay -f
【地址老变怎么办?记住一个局域网地址就行】
匿名隧道的公网地址每次重启都会变。查当前地址:
浏览器打开 http://<随身WiFi的IP>:8080/url
比如 http://192.168.2.156:8080/url
它会直接返回当前的公网地址,纯文本,不用登录。
下面还有两个命令(SSH 上去用):
lanremote-url 打印当前公网地址
systemctl restart lanremote-tunnel 换个地址(超时的时候用)
【管理】
管理页: <公网地址>/?key=<lanremote-key 的输出>
管理页顶部也会大字显示"当前公网地址"。
管理密码:120830(存在 data/admin_password.txt,可改)
能看到:设备列表、在线状态、配对码、流量、会话数
能操作:踢会话、抹掉配对码绑定
【重要:这几个参数都是实测调出来的】
install.sh 里给 cloudflared 加了:
--edge-ip-version 4 只走 IPv4
--ha-connections 1 并发连接数
--retries 5 连边缘失败时多重试
为什么(都是踩出来的,不是拍脑袋):
① IPv6:这条线路**分配了 IPv6 地址但实际不通**,
而 Cloudflare 边缘默认优先 IPv6,会导致建隧道一直超时
(日志里是 context deadline exceeded)。
② 协议:**用默认的 QUIC,不要写 --protocol http2**。
实测这条线路把 HTTP/2 用的 TCP 7844 挡了:
TLS handshake with edge error: read tcp ...:7844: i/o timeout
而 QUIC 走 UDP 7844 是通的。
(早期版本这里误写成 --protocol http2,隧道完全起不来。)
③ 连接数:用 1(默认是 4)。这条线路对并发 UDP 很敏感:
链路好时 ha=4 只成功注册 1 条(9/10),ha=2 是 10/10;
链路退化时 ha=2 掉到 0/10(一条失败重试把另一条也拖下水),
而 ha=1 是 15/15 + 10/10(100%)。
多一条连接不是多一份冗余,而是多一份争抢。
冗余交给看门狗(每 30 秒查一次,断了就重启)更靠谱。
换了网络环境可以试着去掉这些参数用默认值。
【关于隧道地址会变】
默认用的是 Cloudflare **匿名快速隧道**,地址每次重启会变。
想固定地址要用**具名隧道**(需要你自己的域名):
cloudflared tunnel login # 浏览器点授权
cloudflared tunnel create lanremote
cloudflared tunnel route dns lanremote relay.你的域名
# 然后把 lanremote-tunnel.service 的 ExecStart 换成:
# cloudflared tunnel --no-autoupdate run lanremote
具名隧道不受匿名隧道限流,稳定性完全不是一个级别。
【省内存】
默认给两个服务各设了 MemoryMax=128M。
要再省就把用不上的服务停掉:
systemctl stop mariadb php8.2-fpm apache2
systemctl disable mariadb php8.2-fpm apache2
"""
def write_readme(path: Path, text: str) -> None:
"""说明文档统一写成 UTF-8 with BOM —— Windows 记事本不加 BOM 会认成 GBK。"""
path.write_bytes(b"\xef\xbb\xbf" + text.replace("\n", "\r\n").encode("utf-8"))
def main() -> int:
if not AGENT_SRC.is_dir():
print(f" [失败] 找不到被控端: {AGENT_SRC}")
return 1
if not APK_SRC.is_file():
print(f" [失败] 找不到 APK: {APK_SRC}")
return 1
build = ROOT / "dist" / NAME
if build.exists():
shutil.rmtree(build, ignore_errors=True)
# ---------- 1. 被控端 ----------
agent_dir = build / "1-agent"
print(" 复制被控端…")
shutil.copytree(AGENT_SRC, agent_dir,
ignore=shutil.ignore_patterns("data", "received",
"*.idsig", "*.log"))
for tool in TOOLS:
if not (agent_dir / tool).is_file():
src = ROOT / "tools" / tool
if src.is_file():
shutil.copy2(src, agent_dir / tool)
write_readme(agent_dir / "README-agent.txt", AGENT_README)
# 老版本往被控端目录塞过一份顶层说明的副本,清掉
for junk in ("先看这个.txt", "使用说明.txt"):
target = agent_dir / junk
if target.is_file():
target.unlink()
# ---------- 2. 服务器端 ----------
relay_dir = build / "2-server"
relay_dir.mkdir(parents=True)
print(" 复制服务器端…")
# 文件清单写死容易漏 —— watchdog.sh 就漏过一次。
# 改成:relay 目录里所有 .py / .sh 都带上。
wanted = sorted(
[f for f in RELAY_SRC.iterdir()
if f.suffix in (".py", ".sh") and f.is_file()],
key=lambda f: f.name)
for src in wanted:
shutil.copy2(src, relay_dir / src.name)
write_readme(relay_dir / "README-server.txt", RELAY_README)
# ---------- 3. 控制端 ----------
watch_dir = build / "3-watch"
watch_dir.mkdir(parents=True)
print(" 复制控制端…")
shutil.copy2(APK_SRC, watch_dir / APK_SRC.name)
write_readme(watch_dir / "README-watch.txt", f"""========================================================
控制端(安卓手表) 版本 {VERSION}
========================================================
【装】
把 {APK_SRC.name} 传到手表上安装。
手表如果提示"未知来源",在设置里允许一下。
【连】
1) 打开 App,首页点「中转设置(跨网络用)」
填服务器端给你的地址,形如 https://relay.你的域名
(不填也能用,但只在同一个 WiFi 下)
2) 点「自动搜索电脑」—— 同一个局域网时能直接搜到
搜不到就点「连不上?点这里诊断」
3) 输 6 位配对码(电脑上设的那个)
4) 连上后是 10 个标签页:
媒体 屏幕 文件 语音 电脑 网络 系统 教室 效率 娱乐
每个页面上半部分是常用按钮,往下滑是更多功能(共 206 个)。
【回退】
系统页最上面三个按钮:
回退上一步 撤销刚才那一步
回退全部 把能撤的都撤了
看能回退什么 列出还能撤哪些
能撤的:所有开关(15 个)、所有画面(26 个,全屏大字/投图/通知等)
撤不了的:关机、重启、删文件、杀进程 —— 这些**不会**出现在回退列表里,
免得你以为什么都能退回去。
""")
# ---------- 4. 顶层说明 ----------
write_readme(build / "README.txt", TOP_README)
# ---------- 4b. 三份正式文档(作者:小麦子)----------
# 放顶层,和 README.txt 并排,解压第一眼就能看到。
# 统一转成 UTF-8 BOM + CRLF —— 不带 BOM 记事本会按 GBK 读,全乱。
doc_files = [
("LICENSE.txt", "许可协议"),
("BEFORE-INSTALL.txt", "安装前必读"),
("AFTER-INSTALL.txt", "安装后须知"),
]
for name, label in doc_files:
src = DOCS / name
if not src.is_file():
print(f" [警告] 缺少文档 {src}")
continue
# 读进来(原文件是无 BOM 的 UTF-8),交给 write_readme
# 统一加 BOM + CRLF 后写出
write_readme(build / name, src.read_text(encoding="utf-8"))
print(f" {label}: {name}")
# ---------- 5. 检查 ----------
print()
print(" 目录结构:")
total = 0
for item in sorted(build.rglob("*")):
if item.is_file():
total += 1
for item in sorted(build.iterdir()):
if item.is_dir():
count = sum(1 for _ in item.rglob("*") if _.is_file())
print(f" {item.name}/ ({count} 个文件)")
else:
print(f" {item.name}")
print(f" 合计 {total} 个文件")
# 必须有的东西
required = [
agent_dir / "ykxmz.exe",
relay_dir / "relay.py",
relay_dir / "install.sh",
watch_dir / APK_SRC.name,
build / "README.txt",
build / "LICENSE.txt",
build / "BEFORE-INSTALL.txt",
build / "AFTER-INSTALL.txt",
]
missing = [str(p.relative_to(build)) for p in required if not p.is_file()]
if missing:
print(f" [失败] 少了: {missing}")
return 1
print(" 三个部分都在")
# ---------- 6. 打包 ----------
print()
print(" 压缩中…")
zip_path = ROOT / "dist" / f"{NAME}.zip"
if zip_path.exists():
zip_path.unlink()
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED,
compresslevel=6) as archive:
for item in sorted(build.rglob("*")):
if item.is_file():
archive.write(item, item.relative_to(build.parent))
size = zip_path.stat().st_size / 1024 / 1024
print(f" {zip_path.name} {size:.2f} MB")
# ---------- 7. 验证压缩包 ----------
with zipfile.ZipFile(zip_path) as archive:
names = archive.namelist()
bad = archive.testzip()
if bad:
print(f" [失败] 压缩包里有坏文件: {bad}")
return 1
has = {
"被控端": any("ykxmz.exe" in n for n in names),
"服务器端": any("relay.py" in n for n in names),
"控制端": any(".apk" in n for n in names),
"顶层说明": any("README.txt" in n for n in names),
}
for label, ok in has.items():
print(f" {label}: {'✓' if ok else '✗'}")
if not all(has.values()):
return 1
print(f" 共 {len(names)} 个条目,完整")
return 0
if __name__ == "__main__":
sys.exit(main())