2.1.3:搬到 Lightsail 真服务器 + 支持"没有证书的中转"

用户买了 AWS Lightsail(2G 内存 / 2 vCPU / 60G SSD / 公网 IP
13.54.30.196),中转从随身 WiFi 那台小机器搬过去。

== 为什么这次搬迁解决了一堆老问题 ==

原机器(Debian aarch64、460MB、走 4G)一直有几件搞不定的:

  * 线路对 Cloudflare 隧道的 **7844 端口间歇性阻断**(443 通、7844 不通),
    隧道每 3~10 分钟掉一次、公网跟着 530 —— 查了很久才从
    cloudflared 自己的 precheck 里看清
  * 只有 460MB 内存
  * 会掉电硬挂(实测 2026-10-08 18:42 一次非正常关机)

新机器**有公网 IP**,中转直接监听,不经隧道、不过 7844。
实测公网延迟从 **1000~2000 ms 降到 1.1 ms**。

== 代码改动 ==

1. **relay.py 的 DATA_DIR 默认值不再写死**
   原来是 /root/lanremote-relay。新机器上服务以 admin 普通用户跑
   (它没理由要 root),而 /root 是 0700 —— 部署时直接
   PermissionError: '/root/lanremote-relay'。
   改成"本脚本所在目录下的 data/",搬到哪都跟着走。

2. **客户端支持"没有证书的中转"**
   新服务器是明文 HTTP(没签证书),而两个客户端原来都往 https 走:
     * 手表端:裸 IP 会被强制补 https:// -> 永远连不上,
       报的还是 SSL 错误,很容易让人白查半天证书
     * Android 9+ 默认禁止明文流量,没 usesCleartextTraffic
       请求会被系统直接掐掉
   Windows 端本来就是对的(正则里把任意纯 IP 都当"用明文")。
   现在手表端规则和它一致,并加了 usesCleartextTraffic。

3. 清掉电脑端一段函数内的重复代码(return 之后还有一份拷贝,
   缩进仍在函数体内所以不报错,但读起来很迷惑)。

== 新服务器上的部署 ==

    /opt/ykxmz-relay/            服务端(relay/admin/landing/showcase)
    ykxmz-relay.service          监听 0.0.0.0:13245
    ykxmz-monitor.service/.timer 每 60 秒查公网,连续 3 次不通就叫预案
    rescue-lightsail.sh          四级阶梯抢救(重启中转 / 强杀重拉 /
                                 查防火墙 / 重启整机[默认关])

  预案针对新架构重写过:原来围着隧道转(重启隧道、重施 MSS/MTU),
  现在没有隧道,改成重启中转 + 查端口/防火墙。

全量回归 46 个阶段全部通过。
This commit is contained in:
xmz7891
2026-10-08 22:29:56 +08:00
parent bc289c5c21
commit 5fc2f97be7
24 changed files with 1349 additions and 46 deletions
+128
View File
@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
r'''连 AWS Lightsail 那台新服务器的 SSH 助手。
== 为什么单独一个模块,不改 devssh.py ==
devssh.py 是连**随身 WiFi 那台 4G 小机器**的(192.168.2.156,密码登录)。
那台还在跑(网站暂时还挂在上面),不能动它的连接逻辑。
新服务器用**密钥**登录、IP 是公网 13.54.30.196,认证方式完全不同,
所以单独一个模块更清楚。
== 用法 ==
from lssh import run, upload, download, probe
run("uname -a") # 跑命令
upload("relay/relay.py", "/opt/ykxmz-relay/relay.py")
'''
from __future__ import annotations
import os
import socket
from pathlib import Path
import paramiko
HOST = os.environ.get("YKXMZ_LS_HOST", "13.54.30.196")
USER = os.environ.get("YKXMZ_LS_USER", "admin")
PORT = int(os.environ.get("YKXMZ_LS_PORT", "22"))
KEY = Path(os.environ.get(
"YKXMZ_LS_KEY", str(Path.home() / ".ssh" / "lightsail-ykxmz.pem")))
_client = None
def _connect(timeout: int = 25):
global _client
if _client is not None:
try:
# 探一下还活着没
_client.exec_command("true", timeout=5)
return _client
except Exception: # noqa: BLE001
_client = None
if not KEY.is_file():
raise RuntimeError(
"找不到 SSH 密钥:%s\n"
"(把 Lightsail 的 .pem 放到那里,或者设 YKXMZ_LS_KEY)" % KEY)
cli = paramiko.SSHClient()
cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
cli.connect(HOST, port=PORT, username=USER,
key_filename=str(KEY), timeout=timeout,
allow_agent=False, look_for_keys=False,
banner_timeout=30, auth_timeout=30)
_client = cli
return cli
def run(command: str, timeout: int = 300, quiet: bool = False) -> tuple:
"""跑一条命令,返回 (退出码, 输出)。"""
cli = _connect()
if not quiet:
print(" [ls] %s" % command.splitlines()[0][:90])
stdin, stdout, stderr = cli.exec_command(command, timeout=timeout,
get_pty=False)
out = stdout.read().decode("utf-8", "replace")
err = stderr.read().decode("utf-8", "replace")
code = stdout.channel.recv_exit_status()
return code, (out + err)
def upload(local: str, remote: str, mode: int = 0o644) -> None:
"""传一个文件上去。"""
cli = _connect()
sftp = cli.open_sftp()
try:
# 先把远端目录建出来
d = os.path.dirname(remote)
if d:
try:
sftp.stat(d)
except IOError:
# 逐级建
parts = d.strip("/").split("/")
cur = ""
for p in parts:
cur += "/" + p
try:
sftp.stat(cur)
except IOError:
try:
sftp.mkdir(cur)
except IOError:
pass
sftp.put(local, remote)
sftp.chmod(remote, mode)
finally:
sftp.close()
print(" 已上传 %s -> %s" % (os.path.basename(local), remote))
def download(remote: str, local: str) -> None:
cli = _connect()
sftp = cli.open_sftp()
try:
Path(local).parent.mkdir(parents=True, exist_ok=True)
sftp.get(remote, local)
finally:
sftp.close()
print(" 已下载 %s -> %s" % (remote, local))
def probe() -> str | None:
"""通不通。返回一句话描述,或 None。"""
try:
code, out = run("uname -sm; nproc; free -m | sed -n 2p",
timeout=30, quiet=True)
if code != 0:
return None
return out.strip().replace("\n", " | ")
except Exception as exc: # noqa: BLE001
return "连接失败:%s" % exc
if __name__ == "__main__":
print(" 目标: %s@%s:%d 密钥 %s" % (USER, HOST, PORT, KEY))
print(" " + (probe() or "不通"))