#!/bin/bash set -Eeuo pipefail echo "======================================" echo " ZeroTier AIO 离线快速部署脚本 v2" echo " 支持备份恢复 / 新服务器迁移" echo "======================================" [ "$EUID" -ne 0 ] && { echo "请用 root 执行"; exit 1; } INSTALL_DIR="/opt/zerotier-aio" IMAGE_TAR="$INSTALL_DIR/zerotier-aio-zh.tar" IMAGE_REF="niliaerith/zerotier-aio-zh:latest" CONTAINER_NAME="zerotier-aio" FORCE_REGEN_MOON="${FORCE_REGEN_MOON:-0}" if ! command -v apt >/dev/null 2>&1; then echo "当前脚本仅支持 Debian/Ubuntu(需要 apt)" exit 1 fi # 安装依赖(离线环境至少要有 docker + compose + curl + ip) if ! command -v docker >/dev/null 2>&1; then apt update -y apt install -y docker.io docker-compose-plugin curl iproute2 systemctl enable --now docker fi if ! docker compose version >/dev/null 2>&1; then apt update -y apt install -y docker-compose-plugin fi command -v curl >/dev/null 2>&1 || apt install -y curl command -v ip >/dev/null 2>&1 || apt install -y iproute2 # 检查 TUN modprobe tun 2>/dev/null || true [ -c /dev/net/tun ] || { echo "TUN 不可用"; exit 1; } # 获取公网 IP PUBLIC_IP=$(curl -fsS --connect-timeout 2 --max-time 5 http://100.100.100.200/latest/meta-data/eipv4 || true) [ -z "$PUBLIC_IP" ] && PUBLIC_IP=$(ip route get 1.1.1.1 2>/dev/null | awk '/src/ {for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}') [ -z "$PUBLIC_IP" ] && read -r -p "无法自动获取公网IP,请手动输入: " PUBLIC_IP [ -n "$PUBLIC_IP" ] || { echo "未获取到公网 IP,退出"; exit 1; } echo "公网 IP: $PUBLIC_IP" mkdir -p "$INSTALL_DIR" cd "$INSTALL_DIR" # 如果有备份包,优先恢复 if ls zerotier-aio-backup*.tar.gz 1>/dev/null 2>&1; then BACKUP_FILE=$(ls -t zerotier-aio-backup*.tar.gz | head -n1) echo "检测到备份包,正在恢复: $BACKUP_FILE" tar -xzf "$BACKUP_FILE" -C /opt elif ls zerotier-aio-essential*.tar.gz 1>/dev/null 2>&1; then BACKUP_FILE=$(ls -t zerotier-aio-essential*.tar.gz | head -n1) echo "检测到 essential 包,正在恢复: $BACKUP_FILE" tar -xzf "$BACKUP_FILE" -C /opt elif ls zerotier-aio-[0-9]*.tar.gz 1>/dev/null 2>&1; then # 兼容旧版文档中的备份命名 BACKUP_FILE=$(ls -t zerotier-aio-[0-9]*.tar.gz | head -n1) echo "检测到旧版命名备份,正在恢复: $BACKUP_FILE" tar -xzf "$BACKUP_FILE" -C /opt fi # 加载镜像(离线强依赖) [ -f "$IMAGE_TAR" ] || { echo "未找到离线镜像包: $IMAGE_TAR" echo "请先上传 zerotier-aio-zh.tar 到 $INSTALL_DIR" exit 1 } echo "加载本地镜像..." docker load -i "$IMAGE_TAR" docker image inspect "$IMAGE_REF" >/dev/null 2>&1 || { echo "镜像加载后未找到: $IMAGE_REF" echo "请确认 tar 内镜像标签与 docker-compose.yml 一致" exit 1 } # 生成或使用 docker-compose.yml cat > docker-compose.yml </dev/null 2>&1; then break fi sleep 1 done docker exec "$CONTAINER_NAME" test -f /var/lib/zerotier-one/identity.public >/dev/null 2>&1 || { echo "容器未完成初始化,未找到 identity.public" exit 1 } # 检查 Moon(如果 moons.d 为空则生成) MOONS_DIR="./zerotier-one/moons.d" if [ "$FORCE_REGEN_MOON" = "1" ]; then rm -f "$MOONS_DIR"/*.moon 2>/dev/null || true fi if [ ! -d "$MOONS_DIR" ] || [ -z "$(find "$MOONS_DIR" -maxdepth 1 -name '*.moon' -print -quit 2>/dev/null)" ]; then echo "生成 Moon..." docker exec "$CONTAINER_NAME" sh -c "zerotier-idtool initmoon /var/lib/zerotier-one/identity.public > /tmp/moon.json" docker exec "$CONTAINER_NAME" sh -c "sed -i 's|\"stableEndpoints\": \\[\\]|\"stableEndpoints\": [\"$PUBLIC_IP/9993\"]|' /tmp/moon.json" docker exec "$CONTAINER_NAME" sh -c "cd /tmp && zerotier-idtool genmoon moon.json" MOON_FILE=$(docker exec "$CONTAINER_NAME" sh -c "find /tmp -maxdepth 1 -name '*.moon' | head -n1") [ -n "$MOON_FILE" ] || { echo "Moon 生成失败"; exit 1; } mkdir -p "$MOONS_DIR" docker cp "$CONTAINER_NAME:$MOON_FILE" "$MOONS_DIR/" MOON_ID=$(basename "$MOON_FILE" .moon) docker restart "$CONTAINER_NAME" >/dev/null else MOON_ID=$(find "$MOONS_DIR" -maxdepth 1 -name '*.moon' | head -n1 | xargs basename | cut -d. -f1) fi # 防火墙(ufw) command -v ufw &>/dev/null && { ufw allow 9993/udp || true ufw allow 3000/tcp || true ufw allow 3443/tcp || true ufw reload || true } cat <