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
+212
View File
@@ -0,0 +1,212 @@
# -*- coding: utf-8 -*-
"""目标验收:逐条核对用户提出的每一项要求,每条都给证据。
用户的目标:
"在 Debian 随身WiFi 上部署中转服务器 + Cloudflare Tunnel,实现跨网络远程控制。
交付物为压缩包,内含三部分:被控端(Windows agent)、服务器端(relay)、控制端(Android APK)。
要求:单设备单配对码(设备曾设置过则复用旧码)、服务器端可管理、
面向 512MB 内存做优化。另需为电脑端所有按钮增加回退/撤销能力。"
这个脚本**不重复跑那些已经跑过的测试**,而是逐条给"这条要求达成了"的证据,
并明确指出哪一条还没有完全达成。
"""
from __future__ import annotations
import json
import subprocess
import sys
import urllib.error
import urllib.request
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build"))
from devssh import run as ssh # noqa: E402
RESULTS = []
def item(requirement: str, ok: bool, evidence: str, gap: str = "") -> None:
RESULTS.append({
"requirement": requirement,
"ok": ok,
"evidence": evidence,
"gap": gap,
})
mark = "✓" if ok else ("~" if gap else "✗")
print(f" [{mark}] {requirement}")
print(f" 证据: {evidence}")
if gap:
print(f" 差距: {gap}")
def device(command: str, timeout: int = 60) -> str:
_code, out, _err = ssh(command, timeout=timeout, quiet=True)
return out.strip()
# Cloudflare 的 Browser Integrity Check 会拦 Python 默认 UA
# (403 + error code 1010),所以请求要带上这个。
_UA = {"User-Agent": "LanRemote/1.4.0"}
def http_get(url: str, timeout: int = 15) -> tuple:
try:
req = urllib.request.Request(url, headers=_UA)
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.status, resp.read().decode("utf-8", "replace")
except urllib.error.HTTPError as exc:
return exc.code, exc.read().decode("utf-8", "replace")
except Exception as exc: # noqa: BLE001
return 0, str(exc)
def main() -> int:
print("=" * 70)
print(" 目标验收:逐条核对")
print("=" * 70)
print()
# ---------------- 1. 中转服务器部署 ----------------
print("一、在随身 WiFi 上部署中转服务器")
status = device("systemctl is-active lanremote-relay")
enabled = device("systemctl is-enabled lanremote-relay 2>/dev/null")
health = device("curl -s --max-time 8 http://127.0.0.1:8080/v1/health")
mem_kb = 0
try:
mem_kb = json.loads(health).get("memoryKB", 0)
except Exception: # noqa: BLE001
pass
item("中转服务在设备上运行", status == "active",
f"systemctl={status},开机自启={enabled},Rss={mem_kb}KB ({mem_kb // 1024}MB)")
# ---------------- 2. Cloudflare Tunnel ----------------
print()
print("二、Cloudflare 隧道")
tunnel_active = device("systemctl is-active lanremote-tunnel")
url = device("cat /root/lanremote-relay/public_url.txt 2>/dev/null")
code, _body = http_get(f"{url}/v1/health") if url else (0, "")
item("Cloudflare 隧道已建立且公网可达", tunnel_active == "active" and code == 200,
f"服务={tunnel_active},地址={url or '(无)'},从公网访问 HTTP {code}")
cert = device("test -f /root/.cloudflared/cert.pem && echo yes || echo no")
item("具名隧道(固定域名 relay.xxmxxs.top)",
cert == "yes",
f"cert.pem={'存在' if cert == 'yes' else '不存在'}(授权未完成)",
gap="等待用户在浏览器完成 Cloudflare 授权。"
"已定位并修复了阻塞它的两个问题:"
"① api.cloudflare.com 被 DNS 劫持(已写 hosts 钉真实 IP);"
"② install.sh 的 CRLF 行尾(已改 LF)")
# ---------------- 3. 三件套压缩包 ----------------
print()
print("三、交付压缩包含三部分")
zip_path = ROOT / "dist" / "LanRemote-跨网络版-1.4.0.zip"
parts = {}
if zip_path.is_file():
with zipfile.ZipFile(zip_path) as archive:
names = archive.namelist()
parts = {
"被控端": 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),
}
size = zip_path.stat().st_size / 1024 / 1024 if zip_path.is_file() else 0
item("压缩包含被控端 / 服务器端 / 控制端",
all(parts.values()),
f"{zip_path.name} {size:.2f} MB " +
" ".join(f"{k}={'✓' if v else '✗'}" for k, v in parts.items()))
# ---------------- 4. 单设备单配对码 ----------------
print()
print("四、单设备单配对码(设备设过就复用旧码)")
# 用真实的 relay 跑一次:同 deviceId 报新码 -> 应改回旧码;异设备用同码 -> 应拒绝
result = subprocess.run(
[sys.executable, "-u", str(ROOT / "_tests" / "test_relay_local.py")],
capture_output=True, text=True, encoding="utf-8", errors="replace")
relay_ok = "失败 0 项" in result.stdout
item("单设备单配对码 + 一个码只能一台设备用", relay_ok,
"test_relay_local.py(经真实中转):" +
(result.stdout.strip().splitlines()[-1] if result.stdout.strip() else "无输出"))
# ---------------- 5. 服务器端可管理 ----------------
print()
print("五、服务器端可管理")
if url:
plain = device("cat /root/lanremote-relay/data/admin_password.txt 2>/dev/null")
c_login, _ = http_get(f"{url}/v1/admin/devices")
c_url, url_body = http_get(f"{url}/url")
item("管理页需要登录 + 管理接口 + 可查当前地址",
c_login == 401 and c_url == 200,
f"管理接口未登录={c_login}(应为 401);"
f"/url={c_url} 返回 {url_body.strip()[:60]};"
f"管理密码={plain or '120830'}")
else:
item("管理页需要登录 + 管理接口", False, "没有公网地址,无法测")
# ---------------- 6. 512MB 内存优化 ----------------
print()
print("六、面向 512MB 内存做优化")
freem = device("free -m | awk 'NR==2{print $3\" \"$7}'")
queue_cap = device("grep -o 'RELAY_QUEUE_BYTES=[0-9]*' "
"/etc/systemd/system/lanremote-relay.service")
memmax = device("grep -o 'MemoryMax=[0-9A-Za-z]*' "
"/etc/systemd/system/lanremote-relay.service")
item("内存占用受控且有上限保护",
mem_kb > 0 and mem_kb < 60 * 1024,
f"中转 Rss={mem_kb // 1024}MB;系统已用/可用={freem or '?'}MB;"
f"{memmax or 'MemoryMax 未设'};{queue_cap or '队列上限未设'}")
# ---------------- 7. 所有按钮加回退 ----------------
print()
print("七、电脑端所有按钮加回退/撤销")
result = subprocess.run(
[sys.executable, "-u", str(ROOT / "_tests" / "test_undo.py")],
capture_output=True, text=True, encoding="utf-8", errors="replace")
undo_ok = "失败 0 项" in result.stdout
item("开关类 + 画面类都能回退,不可逆的不进列表", undo_ok,
"test_undo.py:" +
(result.stdout.strip().splitlines()[-1] if result.stdout.strip() else "无输出"))
# ---------------- 8. 跨网络远程控制(端到端)----------------
print()
print("八、跨网络远程控制确实能用")
if url:
admin = device("cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null")
result = subprocess.run(
[sys.executable, "-u",
str(ROOT / "_tests" / "test_relay_e2e.py"), url, admin],
capture_output=True, text=True, encoding="utf-8", errors="replace")
e2e_ok = "失败 0 项" in result.stdout
item("经公网隧道完成注册/接入/收发/鉴权", e2e_ok,
"test_relay_e2e.py:" +
(result.stdout.strip().splitlines()[-1] if result.stdout.strip() else "无输出"))
else:
item("经公网隧道端到端", False, "没有公网地址")
# ---------------- 汇总 ----------------
print()
print("=" * 70)
print(" 验收汇总")
print("=" * 70)
full = sum(1 for r in RESULTS if r["ok"])
partial = sum(1 for r in RESULTS if not r["ok"] and r["gap"])
missing = sum(1 for r in RESULTS if not r["ok"] and not r["gap"])
for r in RESULTS:
mark = "✓ 达成" if r["ok"] else ("~ 部分" if r["gap"] else "✗ 未达成")
print(f" {mark} {r['requirement']}")
print()
print(f" 完全达成 {full} 项 / 部分达成 {partial} 项 / 未达成 {missing} 项")
if partial:
print()
print(" 未完全达成的:")
for r in RESULTS:
if not r["ok"] and r["gap"]:
print(f" · {r['requirement']}")
print(f" {r['gap']}")
return 0 if missing == 0 else 1
if __name__ == "__main__":
sys.exit(main())