mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 17:10:43 +00:00
用户买了 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 个阶段全部通过。
167 lines
6.8 KiB
Python
167 lines
6.8 KiB
Python
# -*- 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())
|