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
+166
View File
@@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
r'''让客户端能用"纯 IP + 明文 HTTP"的中转(新服务器就是这种)。
== 背景 ==
2026-10-08 换了服务器:AWS Lightsail,**有公网 IP**(13.54.30.196),
中转直接监听 13245,**不再走 Cloudflare 隧道**。
好处一大堆(延迟从 1000ms 降到 1ms、不再有 7844 阻断、内存 4 倍),
但有个直接后果:**现在是明文 HTTP,没有 HTTPS 证书**。
而两个客户端原来都默认往 https 上走:
1. Android 的 RelayTransport:用户填 `13.54.30.196:13245`
(没写协议头)会被强制加上 `https://` -> 连不上(那台机器没证书)
2. Android 9+ **默认禁止明文 HTTP**。没有
`android:usesCleartextTraffic="true"` 的话,请求会被系统直接掐掉,
报的还是很含糊的 "Cleartext HTTP traffic not permitted"。
Windows 那边已经是对的:relay.py 的 _PRIVATE_HOST 正则里
`\d{1,3}(?:\.\d{1,3}){3}` 把**任意纯 IP(含公网)**都当"用明文",
所以裸 IP 会正确走 http://。
== 这个补丁做什么 ==
1. RelayTransport 的协议猜测改成和 Windows 一致:
纯 IP / localhost / *.local -> http://
域名 -> https://
2. AndroidManifest 加 usesCleartextTraffic="true"
3. 顺手清掉 windows/lanremote/relay.py 里一段**函数内死代码**
(_guess_scheme 的 return 之后还有一份一模一样的拷贝,
缩进仍属于函数体所以不报错,但看着很迷惑)
== 关于 HTTPS ==
明文 HTTP 意味着**同网络的人能看到流量**(虽然配对令牌、内容本来
就是加密的,但 URL、设备名这些会露)。真正的解法是上 HTTPS:
把域名 relay.xxmxxs.top 的 A 记录指到 13.54.30.196,
然后 certbot 签一张 Let's Encrypt 证书,confd 挂上。
那一步要用户动 DNS,所以先让明文能用(这个补丁),
HTTPS 等 DNS 指过来再做。
'''
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
JAVA = (ROOT / "android" / "src" / "com" / "lanremote" / "watch"
/ "RelayTransport.java")
MANIFEST = ROOT / "android" / "AndroidManifest.xml"
WINRELAY = ROOT / "windows" / "lanremote" / "relay.py"
def patch(path: Path, old: str, new: str, what: str, out: list) -> str:
raw = path.read_bytes()
bom = raw.startswith(b"\xef\xbb\xbf")
text = raw.decode("utf-8-sig")
if old not in text:
out.append(("✗", what, "找不到锚点"))
return text
if text.count(old) != 1:
out.append(("!", what, "锚点 %d 处" % text.count(old)))
text = text.replace(old, new, 1)
path.write_bytes((b"\xef\xbb\xbf" if bom else b"")
+ text.encode("utf-8"))
out.append(("✓", what, ""))
return text
# ---------------------------------------------------------------- 1. Java
JAVA_OLD = ''' if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "https://" + url;
}'''
JAVA_NEW = ''' if (!url.startsWith("http://") && !url.startsWith("https://")) {
// 没写协议头时猜一个。规则要**和 Windows 端一致**
// (windows/lanremote/relay.py 的 _guess_scheme):
//
// IP / localhost / *.local -> http://
// 域名 -> https://
//
// 为什么纯 IP 走明文:IP 地址的中转基本都是自己搭的
// (局域网那台,或者像现在这样一台有公网 IP 的服务器),
// 上面往往没有证书。原来一律加 https:// 的结果是**永远连不上**,
// 而且报的是 SSL 错误,会把人带去查证书查半天。
//
// ⚠ 纯 IP 判断包含**公网 IP** —— 2026-10-08 换到 Lightsail
// 之后中转就是 http://13.54.30.196:13245,
// 只把 192.168/10./172.16 当私有的话这里会走错。
String hostOnly = url;
int slash = hostOnly.indexOf('/');
if (slash > 0) {
hostOnly = hostOnly.substring(0, slash);
}
int colon = hostOnly.indexOf(':');
if (colon > 0) {
hostOnly = hostOnly.substring(0, colon);
}
boolean plain = hostOnly.equalsIgnoreCase("localhost")
|| hostOnly.toLowerCase().endsWith(".local")
|| hostOnly.matches("\\\\d{1,3}(\\\\.\\\\d{1,3}){3}");
url = (plain ? "http://" : "https://") + url;
}'''
# ---------------------------------------------------------------- 2. 清单
MAN_OLD = ''' <uses-permission android:name="android.permission.INTERNET" />'''
MAN_NEW = ''' <!-- ⚠ 允许明文 HTTP。
中转可以是"自己搭的、没有证书"的服务:局域网那台,或者
2026-10-08 之后那台有公网 IP 的服务器(http://13.54.30.196:13245)。
Android 9+ 默认**禁止明文流量**,不加这一条的话请求会被系统
直接掐掉,日志里只有一句含糊的
"Cleartext HTTP traffic to ... not permitted"。
安全性说明:中转上跑的内容(配对令牌、画面、文件)本来就是
加密过的,明文只暴露 URL 和设备名。要更严的话后续上 HTTPS
(Let's Encrypt),那时可以把这一条去掉。 -->
<application android:usesCleartextTraffic="true" />
<uses-permission android:name="android.permission.INTERNET" />'''
# ---------------------------------------------------------------- 3. 死代码
DEAD_OLD = ''' host = address.split("/", 1)[0]
return "http://" if _PRIVATE_HOST.match(host) else "https://"
"""用户没写 http/https 时,猜一个。
IP 地址 / localhost -> http://
局域网里的中转服务是明文 HTTP。如果猜成 https,永远连不上,
而且报的是 SSL 错误,很容易误导人去查证书。
域名 -> https://
走公网必须加密(Cloudflare 隧道本来就是 https)。
"""
host = address.split("/", 1)[0]
return "http://" if _PRIVATE_HOST.match(host) else "https://"'''
DEAD_NEW = ''' host = address.split("/", 1)[0]
return "http://" if _PRIVATE_HOST.match(host) else "https://"'''
def main() -> int:
out: list = []
patch(JAVA, JAVA_OLD, JAVA_NEW, "RelayTransport 协议猜测改成和 Windows 一致",
out)
patch(MANIFEST, MAN_OLD, MAN_NEW, "AndroidManifest 允许明文 HTTP", out)
patch(WINRELAY, DEAD_OLD, DEAD_NEW, "清掉 relay.py 里的函数内死代码", out)
print(" ── 结果 ──")
for m, what, why in out:
print(" %s %s%s" % (m, what, (" —— " + why) if why else ""))
return 0 if not any(m == "✗" for m, _w, _y in out) else 1
if __name__ == "__main__":
sys.exit(main())