ykxmz 1.5.0锛氫慨澶嶆墜琛ㄧ TLS 璇佷功鏍¢獙 + MTU 淇缃戠粶绋冲畾鎬?

琚帶绔?- 淇 MTU 闂锛氬す MSS + 璺緞 MTU 鎺㈡祴锛堝叕缃戝彲鐢ㄧ巼 48% -> 100%锛?- 澶嶇敤 HTTP 杩炴帴锛堝厤鎺夋瘡娆℃柊寤?TLS 鐨?30% 鎻℃墜澶辫触锛?- 姣忓垎閽熷洖澶存壘灞€鍩熺綉锛岃嚜鍔ㄥ垏鍥炲揩 400 鍊嶇殑璺緞
- 鐢婚潰蹇冭烦甯э細闈欐鏃剁渷娴侀噺锛屽鎴风浠嶇煡閬撴祦鏄椿鐨?- 閿佸睆鏃剁粰鍑哄彲鎿嶄綔鐨勬彁绀?
鎵嬭〃绔?- 鍐呯疆 GTS Root R4 / GlobalSign Root CA锛屼慨 SSLHandshakeException
- 杩炴帴閿欒缈昏瘧鎴愪汉璇濓紙璇佷功/鏃堕棿/瑙f瀽/瓒呮椂鍒嗗埆鎻愮ず锛?- 缁忎腑杞繛涓婂悗鐢ㄦ湇鍔″櫒鎶ョ殑鍦板潃鍒锋柊宸查厤瀵硅褰?- 鏂板灞€鍩熺綉杩炴帴椤甸潰銆佹湇鍔″櫒寤惰繜妫€娴?
鏈嶅姟鍣ㄧ
- 鏀寔 chunked 璇锋眰浣擄紙淇瘡 16 绉掍竴娆$殑杩炴帴閿欎贡涓庡ぇ闈㈢Н 502锛?- 缃戠粶璋冧紭鑴氭湰 net-tune.sh锛堝紑鏈鸿嚜鍔ㄥ簲鐢級
- 绠$悊椤碉細鎵归噺鍒犻櫎銆佺綉缁滀綋妫€銆佷笅杞藉尯
- 闅ч亾鏀?HTTP/2 + 鍙岃繛鎺?
鏂囨。
- 璁稿彲鍗忚 / 瀹夎鍓嶅繀璇?/ 瀹夎鍚庨』鐭ワ紙浣滆€咃細灏忛害瀛愶級
This commit is contained in:
xmz7891
2026-10-06 19:01:05 +08:00
commit b705216e4a
3195 changed files with 203292 additions and 0 deletions
+652
View File
@@ -0,0 +1,652 @@
# -*- coding: utf-8 -*-
"""被控端启动入口。
双击 EXE 的完整流程:
第一次运行
弹出一个窗口「第一次使用」→ 输入 6 位配对码
可勾选:设置开机自启 / 顺便放行防火墙
↓ 点确定
黑终端一闪(提权跑一键修复防火墙,会先弹一次 UAC)
↓
窗口消失,程序转入后台静默运行 —— 屏幕上再没有任何东西
之后每次开机
直接后台静默运行,一个窗口都没有
手表
输那 6 位数字就连上
要重新设置:ykxmz.exe --setup
命令行参数:
--setup 重新打开设置窗口
--silent 直接静默运行(开机自启用的就是这个)
--add-device [分钟] 静默运行 + 临时允许新设备配对
--autostart on|off 开关开机自启
--status 打印状态后退出
--port 9000 指定端口
--new-code 换一组配对码
--reset-devices 清空已配对设备
"""
from __future__ import annotations
import argparse
import os
import sys
from lanremote import APP_NAME, __version__
from lanremote.config import Store, app_dir, data_dir
from lanremote.instance import InstanceLock
def parse_args(argv=None):
parser = argparse.ArgumentParser(
prog="lanremote-agent", description=f"{APP_NAME} v{__version__}")
parser.add_argument("--port", type=int, help="监听端口(默认 8765)")
parser.add_argument("--new-code", action="store_true", help="换一组配对码")
parser.add_argument("--reset-devices", action="store_true", help="清空已配对设备")
parser.add_argument("--silent", action="store_true",
help="全静默运行:没有任何窗口,也没有终端")
parser.add_argument("--add-device", nargs="?", type=int, const=10,
default=None, metavar="分钟",
help="静默运行,但临时允许新设备配对(默认 10 分钟)")
parser.add_argument("--autostart", choices=["on", "off"],
help="开关开机自启")
parser.add_argument("--status", action="store_true", help="打印状态后退出")
parser.add_argument("--setup", action="store_true",
help="重新打开设置窗口")
parser.add_argument("--netcheck", nargs="?", type=int, const=90,
default=None, metavar="秒",
help="网络自检(自开控制台,不需要装 Python)")
parser.add_argument("--fix-firewall", action="store_true",
help="提权修复防火墙")
parser.add_argument("--relay", default=None, metavar="地址",
help="设置中转服务器地址(跨网络用)。"
"可用分号分隔多个,按顺序尝试,例如:"
"http://192.168.2.156:8080;https://relay.xxmxxs.top"
" —— 局域网优先,断了自动切隧道。"
"传空字符串关闭")
parser.add_argument("--relay-status", action="store_true",
help="查看中转连接状态")
parser.add_argument("--uninstall", action="store_true",
help="卸载:停进程、删开机自启、删数据目录")
return parser.parse_args(argv)
# ======================================================================
# 命令行小工具
# ======================================================================
def cli_autostart(action: str) -> int:
from lanremote.autostart import enable, disable, status
try:
print(enable() if action == "on" else disable())
except ValueError as exc:
print(f"失败: {exc}")
return 1
info = status()
print(f"当前: {'已开启' if info['enabled'] else '未开启'}")
return 0
def cli_uninstall() -> int:
"""卸载。会开一个控制台让用户看到过程。"""
attach_console()
from lanremote.uninstall import run as do_uninstall
try:
print(do_uninstall())
except Exception as exc: # noqa: BLE001
print(f"卸载出错:{exc}")
return 1
print()
input("按回车关闭…")
return 0
def attach_console() -> None:
"""给 --windowed 程序自己开一个控制台窗口。
打包成 --windowed 后进程没有控制台,直接 print 什么都看不到。
AllocConsole 可以临时开一个,并把 stdout/stderr 接过去。
"""
if os.name != "nt":
return
try:
import ctypes
# 从 .bat 双击启动时已经有控制台了,再 AllocConsole 会失败并且输出丢失
if ctypes.windll.kernel32.GetConsoleWindow():
return
ctypes.windll.kernel32.AllocConsole()
sys.stdout = open("CONOUT$", "w", encoding="utf-8", buffering=1)
sys.stderr = open("CONOUT$", "w", encoding="utf-8", buffering=1)
try:
sys.stdin = open("CONIN$", "r", encoding="utf-8")
except OSError:
pass
except Exception: # noqa: BLE001
pass
def wait_key() -> None:
try:
print()
input("按回车键关闭这个窗口…")
except Exception: # noqa: BLE001
pass
class _Tee:
"""把输出同时送到控制台和文件。"""
def __init__(self, *streams):
self.streams = streams
def write(self, text):
for stream in self.streams:
try:
stream.write(text)
stream.flush()
except Exception: # noqa: BLE001
pass
def flush(self):
for stream in self.streams:
try:
stream.flush()
except Exception: # noqa: BLE001
pass
def cli_netcheck(seconds: int) -> int:
"""网络自检:监听端口,看手表的包到底有没有到这台电脑。
EXE 是 --windowed 打包的,本来没有控制台。这里自己开一个,
同时把结果写一份到「网络自检结果.txt」——
控制台万一没显示出来(被杀软拦、权限受限等),
用户还能从文件里看到结论。wlzj.bat 跑完会自动打开这个文件。
"""
attach_console()
original = sys.stdout
path = app_dir() / "网络自检结果.txt"
handle = None
try:
handle = open(path, "w", encoding="utf-8")
except OSError:
handle = None
if handle is not None:
sys.stdout = _Tee(original, handle)
code = 1
try:
from lanremote import netcheck
code = netcheck.run_check(int(seconds or 90))
except Exception as exc: # noqa: BLE001
print(f"自检运行失败: {exc}")
finally:
if handle is not None:
try:
sys.stdout = original
handle.close()
except Exception: # noqa: BLE001
pass
try:
print()
print(f"结果已保存到:{path}")
except Exception: # noqa: BLE001
pass
wait_key()
return code
def cli_fix_firewall() -> int:
"""提权修复防火墙(会弹一次 UAC,然后黑终端一闪)。"""
store = Store(data_dir())
ok, detail = fix_firewall_with_flash(store)
attach_console()
print(("已修复:" if ok else "未完成:") + str(detail))
wait_key()
return 0 if ok else 1
def cli_relay(url: str) -> int:
"""设置中转地址。传空字符串就关掉。"""
store = Store(data_dir())
url = (url or "").strip().rstrip("/")
store.set("relay_url", url)
attach_console()
if not url:
print("已关闭中转(以后只在局域网内可用)")
wait_key()
return 0
if not url.startswith(("http://", "https://")):
url = "https://" + url
store.set("relay_url", url)
print(f"中转地址已设为:{url}")
print()
print("正在测试能不能连上…")
try:
from lanremote.relay import RelayLink
link = RelayLink(store)
if not link.start():
print(" 启动失败")
wait_key()
return 1
# 等它登记成功
import time as _t
for _ in range(30):
_t.sleep(1)
status = link.status()
if status.get("online"):
print(f" ✓ 已连上中转(设备 {status.get('deviceId')},"
f"配对码 {status.get('code')})")
link.stop()
wait_key()
return 0
if status.get("error"):
print(f" … {status['error']}")
print(f" ✗ 30 秒内没能连上 最后错误:{link.status().get('error')}")
link.stop()
except Exception as exc: # noqa: BLE001
print(f" ✗ 测试失败:{exc}")
wait_key()
return 1
def cli_relay_status() -> int:
"""看现在的中转状态。"""
from lanremote.relay import RelayLink
store = Store(data_dir())
url = str(store.get("relay_url") or "") or "192.168.2.156:8080;https://relay.xxmxxs.top"
attach_console()
if not url:
print("没有配置中转(只在局域网内可用)")
print("要跨网络请运行:ykxmz.exe --relay https://你的中转地址")
wait_key()
return 0
print(f"中转地址:{url}")
print("正在测…")
link = RelayLink(store)
link.start()
import time as _t
for _ in range(30):
_t.sleep(1)
status = link.status()
if status.get("online"):
print(f" ✓ 在线")
print(f" 设备标识 {status.get('deviceId')}")
print(f" 配对码 {status.get('code')}")
print(f" 本次已服务 {status.get('sessions')} 个手表会话")
link.stop()
wait_key()
return 0
print(f" ✗ 不在线 最后错误:{link.status().get('error')}")
link.stop()
wait_key()
return 1
def cli_status() -> int:
from lanremote.autostart import status as auto_status
store = Store(data_dir())
auto = auto_status()
print(f"{APP_NAME} v{__version__}")
print(f" 数据目录: {data_dir()}")
print(f" 监听端口: {store.get('port', 8765)}")
print(f" 配对码: {store.pairing_code() or '(还没生成)'}")
print(f" 已配设备: {len(store.device_list())} 台")
print(f" 允许新配对: {'是' if store.get('allow_new_pairing', True) else '否'}")
print(f" 开机自启: {'已开启' if auto['enabled'] else '未开启'}")
if auto["enabled"]:
print(f" 命令行: {auto['command']}")
return 0
# 设完配对码之后留多久给手表来配对
SETUP_PAIR_MINUTES = 60
# 一台设备都没配对过时,自动开多久的配对窗口(自愈用)
FIRST_PAIR_MINUTES = 120
# ======================================================================
# 黑终端一闪:提权修防火墙
# ======================================================================
def fix_firewall_with_flash(store) -> tuple[bool, str]:
"""提权执行防火墙修复。
会先弹一次 UAC;确认之后黑色终端窗口一闪,script 跑完自动关掉。
这就是设置流程里那个"闪过的黑窗口"。
"""
from lanremote import firewall
from lanremote.discovery import DISCOVERY_PORT
tcp_port = int(store.get("port", 8765) or 8765)
udp_port = int(DISCOVERY_PORT)
try:
return firewall.apply_elevated(tcp_port, udp_port)
except Exception as exc: # noqa: BLE001
return False, f"防火墙修复失败: {exc}"
# ======================================================================
# 静默运行
# ======================================================================
def run_silent(store, allow_pair_minutes: int | None) -> int:
"""全静默运行:没有窗口,没有终端,只在后台监听。"""
import time
from lanremote.actions import Actions
from lanremote.headless import HeadlessUI
from lanremote.server import RemoteServer
log_file = data_dir() / "silent.log"
# 静默模式下默认**不接受新配对**(防止局域网里任何人接进来)。
# 需要加设备时用 --add-device 临时开一个时间窗。
#
# 自愈:如果一台设备都还没配对过,就必须把配对打开 ——
# 否则用户设完码会永远连不上(报"被控端已关闭新设备配对")。
# 配对码仍然是门槛,所以不影响安全;第一台配对成功后自动关闭。
if not store.device_list():
allow_pair_minutes = max(allow_pair_minutes or 0,
FIRST_PAIR_MINUTES)
store.set("close_pair_after_first", True)
if allow_pair_minutes:
store.set("allow_new_pairing", True)
deadline = time.time() + max(1, allow_pair_minutes) * 60
else:
store.set("allow_new_pairing", False)
deadline = 0.0
ui = HeadlessUI(store, log_file=log_file)
ui.log(f"{APP_NAME} v{__version__} 静默启动")
if not store.get("setup_done", False):
ui.log("注意:还没有设置过配对码。请先运行:ykxmz.exe --setup")
ui.log(f"当前临时配对码是 {store.pairing_code()}")
lock = InstanceLock()
if not lock.try_claim():
ui.log("已有实例在运行,退出")
return 0
server = RemoteServer(store, None, ui=ui)
server.actions = Actions(ui, store)
from lanremote.features import Features
server.features = Features(server.actions)
server.pairing_enabled = bool(store.get("allow_new_pairing", True))
ui.server = server
try:
server.start()
except (OSError, OverflowError, ValueError) as exc:
ui.log(f"启动失败:{exc}")
lock.release()
return 1
code = store.pairing_code()
if allow_pair_minutes and code:
ui.log(f"配对窗口已开 {allow_pair_minutes} 分钟,配对码 {code}")
ui.log(f"正在监听 TCP {server.port} / UDP 8766")
import threading
def watchdog():
time.sleep(1.0)
while not ui._stopping:
time.sleep(5.0)
if allow_pair_minutes and deadline and time.time() > deadline:
store.set("allow_new_pairing", False)
server.pairing_enabled = False
ui.log("配对窗口已关闭")
threading.Thread(target=watchdog, daemon=True, name="pair-window").start()
try:
ui.run_forever()
except KeyboardInterrupt:
pass
finally:
try:
server.stop()
except Exception: # noqa: BLE001
pass
ui.stop()
lock.release()
# 必须用 os._exit 结束。
# 界面线程拥有 Tk 解释器,正常走解释器退出会报
# "Tcl_AsyncDelete: async handler deleted by the wrong thread",
# 而且进程退出码会变成 -2147483645(崩溃码)。
# os._exit 跳过解释器收尾,干净退出。
sys.stdout.flush()
sys.stderr.flush()
os._exit(0)
# ======================================================================
# 首次设置
# ======================================================================
def check_code_available(store, code: str):
"""这个配对码能用吗?
能用(或没法判断)返回 None;被别的电脑占了返回一段说明文字。
检查走中转的只读接口 /v1/code/check —— 不注册,所以不会误占。
连不上中转时返回 None(放行):不能因为服务器连不上就装不了,
真有问题等转静默之后还会再提示一次。
"""
if not code or len(code) != 6 or not code.isdigit():
return "配对码必须是 6 位数字"
target = str(store.get("relay_url") or "").strip()
if not target:
# 没配过就用内置默认值(定义在 server.py)
from lanremote.server import DEFAULT_RELAY_URL
target = DEFAULT_RELAY_URL
if not target.strip():
return None # 没配中转,没法检查
try:
from lanremote.relay import RelayLink
link = RelayLink(store, log=lambda _m: None)
# start() 会去连中转并登记 —— 但我们只用它的 candidate 解析,
# 不用它登记(登记会占用配对码)。
link.candidates = []
for piece in target.replace(",", ";").split(";"):
piece = piece.strip().rstrip("/")
if not piece:
continue
if not piece.startswith(("http://", "https://")):
from lanremote.relay import _guess_scheme
piece = _guess_scheme(piece) + piece
link.candidates.append(piece)
link.url = link.candidates[0] if link.candidates else ""
reply = link._check_code(code)
except Exception: # noqa: BLE001
return None # 检查失败就当没问题
if not reply.get("ok"):
return None # 服务器没回应,放行
if not reply.get("taken"):
return None # 没人用,可以用
where = reply.get("deviceName") or "另一台电脑"
return (
f"配对码 {code} 已经被「{where}」占用了。\n\n"
f"一个配对码同时只能给一台电脑用。\n"
f"请换一个别的 6 位数字。\n\n"
f"(想继续用这个码的话,可以去中转管理页把占用它的那台设备删掉)"
)
def _tell_firewall_problem(detail: str) -> None:
"""防火墙没放行成功,说明为什么这次没生效。"""
message = (
"防火墙没有放行成功,所以这次**没有**把配对码交给服务器。\n\n"
f"原因:{detail or '未知'}\n\n"
"该怎么办:\n"
" 1. 右键点 ykxmz.exe →「以管理员身份运行」,重新走一遍设置\n"
" 2. 或者手动放行:控制面板 → Windows Defender 防火墙 →\n"
" 允许应用通过防火墙 → 找到 ykxmz → 勾上「专用网络」\n\n"
"放行之后再打开 ykxmz.exe 就行了。"
)
try:
import tkinter as tk
from tkinter import messagebox
root = tk.Tk()
root.withdraw()
root.attributes("-topmost", True)
messagebox.showerror("还没生效:防火墙没放行", message)
root.destroy()
return
except Exception: # noqa: BLE001
pass
print("=" * 56)
print("防火墙没放行,配对码未提交到服务器")
print(message)
print("=" * 56)
def _tell_code_problem(message: str) -> None:
"""把配对码的问题告诉用户。有界面就弹窗,没有就打印。"""
try:
import tkinter as tk
from tkinter import messagebox
root = tk.Tk()
root.withdraw()
root.attributes("-topmost", True)
messagebox.showerror("配对码不能用", message)
root.destroy()
return
except Exception: # noqa: BLE001
pass
print("=" * 56)
print("配对码不能用")
print(message)
print("=" * 56)
def run_setup(store) -> int:
"""弹设置窗口 → 存码 → 黑终端一闪修防火墙 → 转静默。"""
from lanremote.autostart import enable, is_enabled
from lanremote.ui import SetupWindow
# 先检查配对码能不能用,**通过了才去改防火墙**。
#
# 踩过的坑:以前是先存码、开自启、修防火墙,最后才转静默——
# 而"码被别的电脑占了"这件事要等转静默之后才发现。
# 那时防火墙已经改完了,用户在设置窗口里也没机会换码。
result = None
while True:
window = SetupWindow(store).show()
if window is None:
return 0 # 用户关掉了窗口(放弃安装)
code = window.get("code", "")
problem = check_code_available(store, code)
if problem is None:
result = window
break
# 被占了:说清楚,让他回去换一个
_tell_code_problem(problem)
store.set_pairing_code(result["code"])
store.set("setup_done", True)
# 开机自启
if result["autostart"]:
try:
if not is_enabled():
enable()
except Exception: # noqa: BLE001
pass
# 黑终端一闪:修防火墙
firewall_ok = True
firewall_note = ""
if result["firewall"]:
firewall_ok, firewall_note = fix_firewall_with_flash(store)
# 放行防火墙之前,配对码**不加入服务器**。
#
# 为什么:注册(= 配对码挂到服务器上)如果发生在防火墙之前,
# 那么防火墙没放行成功时,服务器上会留着一个谁也连不上的码,
# 而且它会把码占住,用户重装都换不了。
# 所以这里卡一道:防火墙没成功就不注册。
if not firewall_ok:
_tell_firewall_problem(firewall_note)
return 3
# 关键:设完码必须开一个配对窗口。
# 之前这里传 None(= 不开窗口 → allow_new_pairing=False),
# 结果用户刚设完码,被控端立刻就把门锁上,手表第一次配对必然失败,
# 报"被控端已关闭新设备配对"。
# 第一台设备配对成功后服务端会自动把窗口关上。
store.set("close_pair_after_first", True)
return run_silent(store, SETUP_PAIR_MINUTES)
def main(argv=None) -> int:
try:
sys.stdout.reconfigure(encoding="utf-8") # type: ignore[union-attr]
except Exception:
pass
args = parse_args(argv)
if args.autostart:
return cli_autostart(args.autostart)
if args.status:
return cli_status()
# 这两个是给 .bat 调的:EXE 自带功能,不需要系统装 Python
if args.netcheck is not None:
return cli_netcheck(args.netcheck)
if args.fix_firewall:
return cli_fix_firewall()
if args.relay is not None:
return cli_relay(args.relay)
if args.relay_status:
return cli_relay_status()
if args.uninstall:
return cli_uninstall()
store = Store(data_dir())
if args.port:
if not (1 <= int(args.port) <= 65535):
print(f"端口 {args.port} 不合法,应在 1-65535 之间")
return 2
store.set("port", int(args.port))
if args.reset_devices:
store.devices = {}
store.save_devices()
if args.new_code or not store.pairing_code():
store.new_pairing_code()
# 显式要求静默 / 临时开配对窗口
if args.silent or args.add_device is not None:
return run_silent(store, args.add_device)
# --setup:重新设置
if args.setup:
return run_setup(store)
# 第一次运行:设置窗口 → 修防火墙 → 静默
if not store.get("setup_done", False):
return run_setup(store)
# 已经设置过:直接静默,屏幕上什么都不出现
return run_silent(store, None)
if __name__ == "__main__":
sys.exit(main())