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
+21 -2
View File
@@ -59,13 +59,27 @@ from collections import deque
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
VERSION = "2.1.2"
VERSION = "2.1.3"
# ---------------- 参数(都能用环境变量覆盖)----------------
PORT = int(os.environ.get("RELAY_PORT", "8080"))
BIND = os.environ.get("RELAY_BIND", "127.0.0.1")
DATA_DIR = Path(os.environ.get("RELAY_DATA", "/root/lanremote-relay"))
# ⚠ 数据目录的默认值曾经**写死成 /root/lanremote-relay**。
# 原来那台机器上没问题(服务就跑在 /root 下、以 root 跑),
# 但 2026-10-08 搬到 Lightsail 之后立刻炸了:
#
# PermissionError: [Errno 13] Permission denied: '/root/lanremote-relay'
#
# 新机器上服务以 admin 普通用户跑(它没理由要 root),
# 而 /root 是 0700 —— mkdir 直接被拒。
#
# 改成本脚本所在目录下的 data/:搬到哪都跟着走,
# 不用每换一台机器就记得设环境变量。
# (要用别的路径照样可以,RELAY_DATA 优先级更高。)
_SCRIPT_DIR = Path(__file__).resolve().parent
DATA_DIR = Path(os.environ.get("RELAY_DATA", str(_SCRIPT_DIR / "data")))
# 长轮询最多挂多久。太长会占着 Cloudflare 的连接,太短则空转请求多。
# 长轮询挂多久。
@@ -375,6 +389,11 @@ def public_url() -> str:
candidates = [
DATA_DIR.parent / "public_url.txt",
DATA_DIR / "public_url.txt",
# 旧那台机器把 public_url.txt 放在 /root/lanremote-relay/。
# 保留这条是为了**从旧机器迁过来时**还能读到已有的地址;
# 新机器上这个路径根本不存在,is_file() 会直接跳过,无害。
# (之前 DATA_DIR 默认值也写死成这个路径,搬迁时踩过 ——
# 见文件顶部 DATA_DIR 那段注释。)
Path("/root/lanremote-relay/public_url.txt"),
]
for path in candidates:
+285
View File
@@ -0,0 +1,285 @@
#!/bin/bash
# ============================================================================
# 紧急预案(Lightsail 版)—— 网站打不开时按阶梯自动抢救
# ============================================================================
#
# 和随身 WiFi 那版的区别:
#
# 那版是**围着 Cloudflare 隧道**转的(第 1 级就是重启隧道、
# 第 3 级重施 MSS/MTU 调优)。现在这台新机器**有公网 IP**,
# 中转直接对外监听 13245,没有任何隧道 —— 所以:
#
# * 第 1 级从"重启隧道"改成"重启中转服务"
# * 去掉 MSS/MTU 那两级(那是 4G 线路的问题,这里没有)
# * 加一级"看端口有没有被占/进程死没死"
#
# 保留下来的核心设计没变:
#
# 1. **先记录现场,再动手**(重启会把证据冲掉)
# 2. **阶梯式,每级都验**:从代价最小到最大,好了就停
# 3. **全失败要留证据**:生成 txt 报告,后台可下载
# 4. **成功的招数记进知识库**:下次同类问题优先试
#
# 谁调用:monitor.sh(连续 N 次不通就调这里)
# 也可以人工跑:bash /opt/ykxmz-relay/rescue.sh
# ============================================================================
set -u
RELAY_DIR="${RELAY_DIR:-/opt/ykxmz-relay}"
PORT="${RELAY_PORT:-13245}"
# 公网健康检查地址。没有域名就填 IP。
PUBLIC_URL="${PUBLIC_URL:-http://13.54.30.196:$PORT}"
REPORTS_DIR="$RELAY_DIR/rescue_reports"
KNOWLEDGE="$RELAY_DIR/rescue_knowledge.md"
LOG="$RELAY_DIR/rescue.log"
LOCKFILE="/tmp/ykxmz-rescue.lock"
SETTLE_FAST=6
SETTLE_SLOW=15
# 是否允许"重启整机"这张底牌。默认关。
RESCUE_REBOOT="${RESCUE_REBOOT:-0}"
mkdir -p "$REPORTS_DIR"
if ! mkdir "$LOCKFILE" 2>/dev/null; then
echo "[rescue] 已经有一个预案在跑,本次退出"
exit 0
fi
trap 'rmdir "$LOCKFILE" 2>/dev/null' EXIT
STAMP="$(date +%Y%m%d-%H%M%S)"
REPORT="$REPORTS_DIR/$STAMP.txt"
say() {
echo "$*"
echo "$(date '+%F %T') $*" >> "$LOG"
}
# 公网健康检查 —— **看公网,不看本机**
is_healthy() {
local code
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 12 \
"$PUBLIC_URL/v1/health" 2>/dev/null)
[ "$code" = "200" ]
}
local_health() {
local code
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 6 \
"http://127.0.0.1:$PORT/v1/health" 2>/dev/null)
[ "$code" = "200" ]
}
# ---------------------------------------------------------------------------
# 现场记录
# ---------------------------------------------------------------------------
collect() {
{
echo "================================================================================"
echo " ykxmz 紧急预案诊断报告"
echo " 时间:$(date '+%F %T %Z')"
echo " 主机:$(hostname) 运行时长:$(uptime -p 2>/dev/null || uptime)"
echo " 触发原因:$1"
echo "================================================================================"
echo
echo "---- 一、结论速览 ----"
echo
echo " 公网 $PUBLIC_URL/v1/health -> $(is_healthy && echo 200-正常 || echo 不通)"
echo " 本机 http://127.0.0.1:$PORT/v1/health -> $(local_health && echo 200-正常 || echo 不通)"
echo
if local_health && ! is_healthy; then
echo " 判断:**本机是好的,问题在公网那一侧**(防火墙规则、安全组、上游线路)。"
elif ! local_health; then
echo " 判断:**本机的中转服务就没起来**,先看第二、三节。"
else
echo " 判断:两边都通(可能是刚才已经自愈,或者只是偶发抖动)。"
fi
echo
echo "---- 二、服务状态 ----"
echo
printf ' %-24s %s\n' "ykxmz-relay" "$(systemctl is-active ykxmz-relay 2>&1)"
printf ' %-24s %s\n' "ykxmz-monitor.timer" "$(systemctl is-active ykxmz-monitor.timer 2>&1)"
echo
echo " --- 最近 20 行服务日志 ---"
journalctl -u ykxmz-relay --no-pager -n 20 2>/dev/null | sed 's/^/ /'
echo
echo "---- 三、进程与端口 ----"
echo
echo " 监听 $PORT 的进程:"
ss -tlnp 2>/dev/null | grep ":$PORT " | sed 's/^/ /' \
|| echo " **没有任何进程在监听 $PORT**"
echo
echo " 相关进程:"
ps -eo pid,rss,etime,cmd --sort=-rss 2>/dev/null \
| grep -E 'relay\.py|python3' | grep -v grep | head -6 | sed 's/^/ /'
echo
echo "---- 四、系统资源 ----"
echo
free -m 2>/dev/null | sed 's/^/ /'
echo
uptime | sed 's/^/ /'
echo
df -h / /opt 2>/dev/null | sed 's/^/ /'
echo
echo "---- 五、网络 ----"
echo
ip -brief addr show 2>/dev/null | sed 's/^/ /'
echo
echo " 默认路由:"
ip route show default 2>/dev/null | sed 's/^/ /'
echo
echo " 本机自测各端口:"
for p in "$PORT" 80 443 22; do
printf ' %-6s ' "$p"
timeout 3 bash -c "</dev/tcp/127.0.0.1/$p" 2>/dev/null \
&& echo "通" || echo "不通"
done
echo
echo "---- 六、日志尾部 ----"
echo
echo "### 中转服务 relay.log"
tail -30 "$RELAY_DIR/relay.log" 2>/dev/null | sed 's/^/ /'
echo
echo "### 监测单元 monitor.log"
tail -20 "$RELAY_DIR/monitor.log" 2>/dev/null | sed 's/^/ /'
echo
echo "### 内核异常(OOM / 硬件)"
dmesg -T 2>/dev/null \
| grep -i -E 'oom|killed process|error|panic' | tail -15 | sed 's/^/ /'
echo
echo "---- 七、最近一次启动 ----"
echo
who -b 2>/dev/null | sed 's/^/ /'
echo " 运行时长:$(cut -d. -f1 /proc/uptime 2>/dev/null) 秒"
echo
echo "================================================================================"
echo " 报告结束。可以在中转站后台的「抢救报告」分区下载。"
echo "================================================================================"
} > "$REPORT" 2>&1
say " 现场已记录 -> $REPORT"
}
# ---------------------------------------------------------------------------
# 阶梯
# ---------------------------------------------------------------------------
step_1_restart_relay() {
say " [1/4] 重启中转服务(最轻的一招,不动系统其他部分)"
systemctl restart ykxmz-relay 2>/dev/null
sleep "$SETTLE_FAST"
}
step_2_kill_and_start() {
say " [2/4] 强杀残留进程再拉起(进程卡死/端口被占时用)"
# 有时候 systemd restart 会因为老进程没退干净而失败,
# 表现为"服务说 active 但端口没人听"。
pkill -f "$RELAY_DIR/relay.py" 2>/dev/null
sleep 3
systemctl reset-failed ykxmz-relay 2>/dev/null
systemctl start ykxmz-relay 2>/dev/null
sleep "$SETTLE_FAST"
}
step_3_check_firewall() {
say " [3/4] 检查本机防火墙有没有挡住 $PORT"
# Lightsail 有两层:网页上的防火墙规则(实例级)+ 系统内的 ufw/iptables。
# 网页那层我们改不了,但系统内这层能看。
if command -v ufw >/dev/null 2>&1; then
echo " ufw 状态:$(ufw status 2>/dev/null | head -1)" >> "$LOG"
ufw status 2>/dev/null | grep -q "$PORT" \
|| say " ⚠ ufw 里没有放行 $PORT 的规则"
fi
local n
n=$(iptables -L INPUT -n 2>/dev/null | grep -c "$PORT")
say " iptables INPUT 里提到 $PORT 的规则:$n 条"
# 重新绑一次端口(有时是监听 socket 坏了)
systemctl restart ykxmz-relay 2>/dev/null
sleep "$SETTLE_SLOW"
}
step_4_reboot() {
if [ "$RESCUE_REBOOT" != "1" ]; then
say " [4/4] 重启整机 —— **已跳过**(默认不开)"
say " 要允许自动重启,把 RESCUE_REBOOT=1 写进 ykxmz-monitor.service。"
return 1
fi
say " [4/4] 重启整机(最后的底牌)"
sync
systemctl reboot 2>/dev/null
return 0
}
remember() {
local step="$1" reason="$2"
{
echo
echo "## $(date '+%F %T') 第 $step 级奏效"
echo
echo "- 症状:$reason"
echo "- 有效步骤:$step"
echo "- 当时公网:$(is_healthy && echo 通 || echo 不通);本机:$(local_health && echo 通 || echo 不通)"
echo "- 详情见 rescue_reports/$(basename "$REPORT")"
} >> "$KNOWLEDGE"
say " 已把「第 $step 级奏效」记进 $KNOWLEDGE"
}
# ===========================================================================
# 主流程
# ===========================================================================
REASON="${1:-monitor 触发}"
say ""
say "================================================================================"
say " 紧急预案启动:$REASON"
say "================================================================================"
# ⚠ 先复查再记录现场。
# collect() 会写一份报告文件;如果网站其实已经好了,
# 那一次也会留下报告,把真正出事时的报告淹掉(后台只显示最近 30 份)。
if is_healthy; then
say " 复查:公网现在是通的,不需要动手 —— 不采现场、不留报告。"
exit 0
fi
collect "$REASON"
STEP_NAMES=("重启中转服务" "强杀残留再拉起" "查防火墙并重绑" "重启整机")
for i in 1 2 3 4; do
case $i in
1) step_1_restart_relay ;;
2) step_2_kill_and_start ;;
3) step_3_check_firewall ;;
4) step_4_reboot || continue ;;
esac
if is_healthy; then
say ""
say " ✅ 第 $i 级(${STEP_NAMES[$((i-1))]})救回来了,公网已恢复。"
remember "$i" "$REASON"
exit 0
fi
say " 第 $i 级没解决,继续下一级"
done
say ""
say " ❌ 四级都没解决。再采一次现场(状态已经变了)"
collect "$REASON(四级抢救后仍不通)"
{
echo "⚠ 这份报告是「自动抢救全部失败」之后生成的。"
echo " 机器自己试过了:重启中转 / 强杀重拉 / 查防火墙。"
echo " (重启整机默认没做。)"
echo " 需要人工看一眼 —— 特别是 Lightsail **网页上的防火墙规则**,"
echo " 那一层系统内部改不了。"
echo
cat "$REPORT"
} > "$REPORT.tmp" && mv "$REPORT.tmp" "$REPORT"
ls -1t "$REPORTS_DIR"/*.txt 2>/dev/null | tail -n +31 | xargs -r rm -f
say " 报告已生成:$REPORT"
exit 1