#!/bin/bash # 局域网遥控 · 中转服务器 —— 安装脚本 # # 在一台能上外网的 Linux 机器(这里是随身 WiFi / Debian 12)上跑: # bash install.sh # # 做完这些: # 1. 把 relay.py 装到 /root/lanremote-relay # 2. 注册两个 systemd 服务,开机自启: # lanremote-relay 中转服务 # lanremote-tunnel Cloudflare 隧道(公网入口) # 3. 打印公网地址和管理页地址 # # 面向 512MB 内存的小机器:两个进程加起来约 60MB。 set -eu RELAY_DIR=/root/lanremote-relay SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) echo "============================================================" echo " 局域网遥控 · 中转服务器 安装" echo "============================================================" echo # ---------- 0. 环境检查 ---------- echo "[0/5] 检查环境" if [ "$(id -u)" != "0" ]; then echo " 需要 root 权限运行" exit 1 fi command -v python3 >/dev/null || { echo " 没有 python3"; exit 1; } echo " python3 $(python3 -V 2>&1 | awk '{print $2}')" if ! command -v cloudflared >/dev/null; then echo " 没有 cloudflared,尝试安装…" ARCH=$(uname -m) case "$ARCH" in aarch64) CF=cloudflared-linux-arm64 ;; armv7l) CF=cloudflared-linux-arm ;; x86_64) CF=cloudflared-linux-amd64 ;; *) echo " 不认识的架构 $ARCH"; exit 1 ;; esac curl -fsSL --http1.1 -o /usr/local/bin/cloudflared \ "https://github.com/cloudflare/cloudflared/releases/latest/download/$CF" chmod +x /usr/local/bin/cloudflared fi echo " cloudflared $(cloudflared --version 2>&1 | head -1)" echo " 内存: $(free -m | awk 'NR==2{print $2" MB 总量 / "$7" MB 可用"}')" # ---------- 0b. 检查 Cloudflare API 有没有被 DNS 劫持 ---------- # 踩过的坑:这条 4G 线路把 api.cloudflare.com 解析到运营商设备 # (141.193.154.210,自签名证书)。cloudflared 下载登录证书必须走这个域名, # 于是永远报 Failed to fetch resource,具名隧道根本建不起来。 echo echo "[0b] 检查 Cloudflare API" CF_HOSTS=/etc/hosts CF_MARK="# LanRemote: 钉住 Cloudflare API(网络会 DNS 劫持)" resolve_cf() { getent ahostsv4 api.cloudflare.com 2>/dev/null | awk 'NR==1{print $1}' } is_real_cf() { case "$1" in 104.16.*|104.17.*|104.18.*|104.19.*|104.20.*|104.21.*|172.64.*|172.66.*|172.67.*|162.159.*|188.114.*) return 0 ;; *) return 1 ;; esac } CF_IP=$(resolve_cf) echo " api.cloudflare.com 现在解析到: ${CF_IP:-(解析不出来)}" CF_CODE=$(timeout 12 curl -4 -s -o /dev/null -w '%{http_code}' \ https://api.cloudflare.com/client/v4/user 2>/dev/null) CF_CODE=${CF_CODE:-000} echo " 实测 HTTP: ${CF_CODE}" CF_BODY=$(timeout 12 curl -4 -s https://api.cloudflare.com/client/v4/user 2>/dev/null | head -c 200) if is_real_cf "$CF_IP" && echo "$CF_BODY" | grep -q '"success"'; then echo " ✓ 正常(返回的是真正的 API JSON)" else echo " ⚠ 检测到 Cloudflare API 被 DNS 劫持或不可达" echo " 具名隧道需要访问它来下载证书,先修掉。" cp -n "$CF_HOSTS" "${CF_HOSTS}.lanremote.bak" 2>/dev/null || true # 清掉所有和本程序相关的 hosts 记录(含早期版本留下的不同文案) sed -i '/LanRemote.*Cloudflare API/d' "$CF_HOSTS" 2>/dev/null || true sed -i '/api\.cloudflare\.com/d' "$CF_HOSTS" 2>/dev/null || true # 顺手删掉清理后可能剩下的空行 sed -i '/^[[:space:]]*$/d' "$CF_HOSTS" 2>/dev/null || true echo "" >> "$CF_HOSTS" { echo "" echo "$CF_MARK" # 这几个是用公共 DNS(1.1.1.1 / 8.8.8.8 / 223.5.5.5)查出来的 # 真实地址。之前随手挑的 104.16.132.229 是错的 —— # 它返回 Cloudflare 错误页(error code: 1034)而不是 API, # 会导致 cloudflared tunnel create 报 unauthorized。 echo "104.19.192.174 api.cloudflare.com" echo "104.19.192.175 api.cloudflare.com" echo "104.19.192.176 api.cloudflare.com" echo "104.19.192.177 api.cloudflare.com" echo "104.17.0.1 api.cloudflare.com" echo "104.18.0.1 api.cloudflare.com" } >> "$CF_HOSTS" sleep 1 CF_IP2=$(resolve_cf) CF_CODE2=$(timeout 12 curl -4 -s -o /dev/null -w '%{http_code}' \ https://api.cloudflare.com/client/v4/user 2>/dev/null) CF_CODE2=${CF_CODE2:-000} echo " 修完解析到: ${CF_IP2:-?} HTTP: ${CF_CODE2}" CF_BODY2=$(timeout 12 curl -4 -s https://api.cloudflare.com/client/v4/user 2>/dev/null | head -c 200) if echo "$CF_BODY2" | grep -q '"success"'; then echo " ✓ 修好了(返回的是真正的 API JSON)" else echo " ✗ 还是不通 —— 具名隧道可能建不起来," echo " 但匿名隧道仍然可用(它走的是另一条链路)" fi fi # ---------- 1. 放文件 ---------- echo echo "[1/5] 安装文件到 $RELAY_DIR" mkdir -p "$RELAY_DIR/data" # 脚本可能就在 RELAY_DIR 里(自己拷自己 cp 会报错),所以先比一下真实路径 SRC="$SCRIPT_DIR/relay.py" DST="$RELAY_DIR/relay.py" SAME=no if [ -f "$SRC" ] && [ -f "$DST" ]; then if [ "$(readlink -f "$SRC")" = "$(readlink -f "$DST")" ]; then SAME=yes fi fi if [ "$SAME" = "yes" ]; then echo " relay.py 已在位(源和目标同一个文件)" elif [ -f "$SRC" ]; then cp -f "$SRC" "$DST" echo " 已复制 relay.py" elif [ -f "$DST" ]; then echo " relay.py 已在位" else echo " 找不到 relay.py" exit 1 fi python3 -m py_compile "$RELAY_DIR/relay.py" echo " 语法检查通过" # ---------- 2. 停掉旧的 ---------- echo echo "[2/5] 停掉旧的实例" systemctl stop lanremote-tunnel lanremote-relay 2>/dev/null || true pkill -f 'relay.py' 2>/dev/null || true pkill -f 'cloudflared tunnel' 2>/dev/null || true sleep 2 # ---------- 3. systemd ---------- echo echo "[3/5] 注册开机自启服务" cat > /etc/systemd/system/lanremote-relay.service < /etc/systemd/system/lanremote-tunnel.service </dev/null 2>&1 systemctl start lanremote-relay sleep 3 systemctl start lanremote-tunnel echo " 两个服务已注册并启动" # ---------- 3b. 装隧道看门狗 ---------- # 匿名隧道建立之后中途失效没人管,用户只能自己发现、自己上来重启。 # 看门狗每 5 分钟检查一次公网可达性,连续 3 次不通就重启隧道。 echo echo "[3b] 装隧道看门狗" if [ -f "$RELAY_DIR/watchdog.sh" ] || [ -f "$SCRIPT_DIR/watchdog.sh" ]; then if [ -f "$SCRIPT_DIR/watchdog.sh" ] && \ [ "$(readlink -f "$SCRIPT_DIR/watchdog.sh")" != \ "$(readlink -f "$RELAY_DIR/watchdog.sh" 2>/dev/null || echo x)" ]; then cp -f "$SCRIPT_DIR/watchdog.sh" "$RELAY_DIR/watchdog.sh" fi chmod +x "$RELAY_DIR/watchdog.sh" cat > /etc/systemd/system/lanremote-watchdog.service <<'WDUNIT' [Unit] Description=LanRemote 隧道看门狗(检查公网可达性) After=lanremote-relay.service lanremote-tunnel.service [Service] Type=oneshot ExecStart=/bin/bash /root/lanremote-relay/watchdog.sh MemoryMax=64M StandardOutput=append:/root/lanremote-relay/watchdog.log StandardError=append:/root/lanremote-relay/watchdog.log WDUNIT cat > /etc/systemd/system/lanremote-watchdog.timer <<'WDTIMER' [Unit] Description=每 5 分钟检查一次隧道 [Timer] OnBootSec=3min OnUnitActiveSec=5min AccuracySec=30s Unit=lanremote-watchdog.service [Install] WantedBy=timers.target WDTIMER systemctl daemon-reload systemctl enable lanremote-watchdog.timer >/dev/null 2>&1 systemctl start lanremote-watchdog.timer echo " 看门狗已启用(每 5 分钟检查一次)" else echo " [跳过] 没找到 watchdog.sh" fi # ---------- 4. 取地址 ---------- echo echo echo "[3c] 网络调优(修 MTU —— 这是可用率从 48% 提到 90% 的关键)" # 实测:接口 MTU 1500 时,ping 包 1488 就丢、1500 全丢。 # TLS 握手要发大证书包 -> 被丢 -> 新建连接约 30% 失败。 # 夹 MSS 到 1360 + 打开路径 MTU 探测之后,可用率 48%~75% -> 90%~92%。 # 注意:**不要改接口 MTU**,实测降到 1400 反而掉到 50%。 if [ -f "$SCRIPT_DIR/net-tune.sh" ]; then cp -f "$SCRIPT_DIR/net-tune.sh" "$RELAY_DIR/net-tune.sh" elif [ -f "$RELAY_DIR/net-tune.sh" ]; then : # 已经在位 fi if [ -f "$RELAY_DIR/net-tune.sh" ]; then chmod +x "$RELAY_DIR/net-tune.sh" # 内核参数持久化 cat > /etc/sysctl.d/99-lanremote-mtu.conf <<'MTUEOF' # LanRemote: 修路径 MTU 问题。 # 接口 MTU 1500 时大包会被丢(1488/1500 全丢),导致 TLS 握手约 30% 失败。 net.ipv4.tcp_mtu_probing = 1 MTUEOF sysctl -p /etc/sysctl.d/99-lanremote-mtu.conf >/dev/null 2>&1 # 开机自动夹 MSS cat > /etc/systemd/system/lanremote-nettune.service </dev/null 2>&1 systemctl start lanremote-nettune.service sleep 1 echo " tcp_mtu_probing = $(cat /proc/sys/net/ipv4/tcp_mtu_probing)" echo " MSS 规则 $(iptables -t mangle -L -n 2>/dev/null | grep -ci mss) 条" echo " 已注册 lanremote-nettune.service(开机自动应用)" else echo " 跳过:找不到 net-tune.sh" fi echo "[4/5] 等隧道拿到公网地址" URL="" for i in $(seq 1 45); do sleep 2 URL=$(grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' \ "$RELAY_DIR/tunnel.log" 2>/dev/null | tail -1) [ -n "$URL" ] && break done if [ -z "$URL" ]; then echo " 还没拿到地址,隧道日志:" tail -15 "$RELAY_DIR/tunnel.log" 2>/dev/null || true exit 1 fi echo "$URL" > "$RELAY_DIR/public_url.txt" # 便捷命令 cat > /usr/local/bin/lanremote-url <<'EOF' #!/bin/bash # 打印中转服务器的公网地址(隧道地址每次重启会变) cat /root/lanremote-relay/public_url.txt 2>/dev/null || echo "(还没有地址)" EOF chmod +x /usr/local/bin/lanremote-url cat > /usr/local/bin/lanremote-key <<'EOF' #!/bin/bash # 打印管理页钥匙 cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null || echo "(还没有钥匙)" EOF chmod +x /usr/local/bin/lanremote-key cat > /usr/local/bin/lanremote-status <<'EOF' #!/bin/bash # 一眼看清中转 + 隧道的状态 echo "=== 服务 ===" for s in lanremote-relay lanremote-tunnel lanremote-watchdog.timer; do printf " %-28s %s\n" "$s" "$(systemctl is-active "$s" 2>/dev/null)" done echo echo "=== 公网地址 ===" cat /root/lanremote-relay/public_url.txt 2>/dev/null || echo " (还没有)" echo echo "=== 可达性 ===" URL=$(cat /root/lanremote-relay/public_url.txt 2>/dev/null) if [ -n "$URL" ]; then C=$(timeout 20 curl -4 -s -o /dev/null -w '%{http_code}' "$URL/v1/health" 2>/dev/null) echo " HTTP ${C:-超时}" fi echo echo "=== 看门狗最近记录 ===" tail -3 /root/lanremote-relay/watchdog.log 2>/dev/null | sed 's/^/ /' || echo " (还没有)" echo echo "=== 内存 ===" free -m | awk 'NR==2{printf " 已用 %s MB / 可用 %s MB\n", $3, $7}' EOF chmod +x /usr/local/bin/lanremote-status # ---------- 5. 自检 ---------- echo echo "[5/5] 自检" sleep 3 if curl -s --max-time 8 http://127.0.0.1:8080/v1/health | grep -q '"ok"'; then echo " 本机健康检查: 通过" else echo " 本机健康检查: 失败" systemctl status lanremote-relay --no-pager -l | tail -10 fi if curl -s --max-time 20 "$URL/v1/health" | grep -q '"ok"'; then echo " 公网访问: 通过" else echo " 公网访问: 还没通(隧道可能要再等一会)" fi KEY=$(cat "$RELAY_DIR/data/admin_key.txt" 2>/dev/null || echo "") echo echo "============================================================" echo " 装好了" echo "============================================================" echo echo " 公网地址 $URL" echo " 管理页 $URL/?key=$KEY" echo echo " 服务状态 systemctl status lanremote-relay" echo " systemctl status lanremote-tunnel" echo " 看地址 lanremote-url" echo " 看钥匙 lanremote-key" echo echo " 注意:快速隧道的地址**每次重启会变**。" echo " 想固定地址要用带域名的具名隧道,见 使用说明。" echo free -m | head -2 echo ps -eo rss,comm --sort=-rss 2>/dev/null | head -4 | awk '{printf " %-8s %s\n", $1, $2}'