Files
2026-06-20 14:26:29 +08:00

2819 lines
103 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ============================================================================
# Incudal 节点安装管理脚本
# 将 Ubuntu / Debian 服务器配置为 Incudal 面板管理的 Incus LXC 容器节点
#
# 用法:
# 交互式: sudo bash Incudal.sh
# 命令行: sudo bash Incudal.sh --mode nat --token <TOKEN>
# 卸载: sudo bash Incudal.sh --uninstall
#
# 项目地址: https://incudal.com
# ============================================================================
set -euo pipefail
# ========面板动态注入区========
# 面板在下载时自动注入以下变量,无需手动修改
INJECT_PANEL_URL=""
INJECT_TOKEN=""
INJECT_MODE=""
INJECT_IPV6_SUBNET=""
INJECT_IPV6_IFACE=""
INJECT_AGENT_ID=""
INJECT_AGENT_SECRET=""
INJECT_AGENT_INSTALL_TOKEN=""
INJECT_AGENT_BINARY_URL=""
INJECT_AGENT_ENABLED="true"
# ==============================
# ========================== 全局常量 ==========================
PANEL_URL="${INJECT_PANEL_URL:-}"
PANEL_URL="${PANEL_URL%/}"
readonly PANEL_URL
readonly SCRIPT_VERSION="2.0.0"
readonly BRIDGE_SUBNET="10.10.0.1/22"
readonly BRIDGE_NAME="incusbr0"
readonly PRESEED_FILE="/tmp/.incus-preseed-$$.yaml"
readonly AGENT_ID="${INJECT_AGENT_ID:-}"
readonly AGENT_SECRET="${INJECT_AGENT_SECRET:-}"
readonly AGENT_INSTALL_TOKEN="${INJECT_AGENT_INSTALL_TOKEN:-}"
readonly AGENT_BINARY_URL="${INJECT_AGENT_BINARY_URL:-}"
readonly AGENT_ENABLED="${INJECT_AGENT_ENABLED:-true}"
readonly AGENT_SERVICE_NAME="incudal-agent"
readonly AGENT_CONFIG_FILE="${INCUDAL_AGENT_CONFIG_FILE:-/etc/incudal-agent/config.yaml}"
readonly AGENT_BIN_PATH="${INCUDAL_AGENT_BIN:-/usr/local/bin/incudal-agent}"
# ZFS 预编译模块下载地址(GitHub Release
# 格式: ${ZFS_PREBUILT_URL}/zfs-modules-<内核版本>.tar.gz
readonly ZFS_PREBUILT_URL="https://github.com/0xdabiaoge/Incudal-Debian-ZFS/releases/download/Debian-ZFS"
# ========================== 颜色定义 ==========================
readonly RED='\033[1;31m'
readonly GREEN='\033[1;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[1;34m'
readonly CYAN='\033[1;36m'
readonly BOLD='\033[1m'
readonly DIM='\033[2m'
readonly NC='\033[0m'
# ========================== 运行时变量 ==========================
MODE=""
TOKEN=""
OS_ID=""
OS_VERSION=""
OS_CODENAME=""
ARCH=""
DEFAULT_IFACE=""
IS_PURE_IPV6="false"
AGENT_INSTALL_STATUS="未安装"
AGENT_HEARTBEAT_INTERVAL_SECONDS="30"
# ========================== 工具函数 ==========================
log() { echo -e "${GREEN}[✓]${NC} $1"; }
info() { echo -e "${BLUE}[i]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
error() { echo -e "${RED}[✗]${NC} $1"; }
step() { echo -e "\n${CYAN}[▶]${NC} ${BOLD}$1${NC}"; }
# 分隔线
divider() {
echo -e "${DIM}────────────────────────────────────────────────────${NC}"
}
pause_return() {
if [[ -t 0 ]]; then
echo ""
echo -ne " ${DIM}按回车返回菜单...${NC}"
read -r _
fi
}
mask_value() {
local value="${1:-}"
local length=${#value}
if [[ -z "$value" ]]; then
echo "-"
elif (( length <= 12 )); then
echo "${value:0:4}****"
else
echo "${value:0:8}...${value: -4}"
fi
}
is_http_url() {
local value="${1:-}"
[[ "$value" =~ ^https?://[^[:space:]]+$ ]]
}
read_agent_config_value() {
local key="$1"
if [[ ! -f "$AGENT_CONFIG_FILE" ]]; then
return 0
fi
local line=""
line=$(grep -E "^[[:space:]]*${key}[[:space:]]*:" "$AGENT_CONFIG_FILE" 2>/dev/null | head -n1 || true)
if [[ -z "$line" ]]; then
return 0
fi
local value="${line#*:}"
value=$(printf '%s' "$value" | sed -E "s/[[:space:]]+#.*$//; s/^[[:space:]]+//; s/[[:space:]]+$//; s/^\"(.*)\"$/\1/; s/^'(.*)'$/\1/")
printf '%s\n' "$value"
}
get_agent_panel_url() {
local config_panel_url=""
config_panel_url=$(read_agent_config_value "panel_url" || true)
if [[ -n "$config_panel_url" ]]; then
echo "${config_panel_url%/}"
return 0
fi
if [[ -n "$PANEL_URL" ]]; then
echo "${PANEL_URL%/}"
return 0
fi
return 0
}
extract_agent_install_token() {
local input="$1"
printf '%s\n' "$input" | grep -Eo 'ait_[A-Za-z0-9_-]{32,}' | head -n1 || true
}
extract_agent_panel_url() {
local input="$1"
printf '%s\n' "$input" \
| sed -nE "s#.*(https?://[^[:space:]'\\\"]+)/api/agent/install\\.sh.*#\\1#p" \
| head -n1 || true
}
normalize_agent_interval() {
local value="${1:-30}"
if [[ "$value" =~ ^[0-9]+$ ]] && (( value >= 5 && value <= 3600 )); then
echo "$value"
else
echo "30"
fi
}
prompt_agent_heartbeat_interval() {
local default_interval=""
default_interval=$(normalize_agent_interval "${1:-30}")
if [[ ! -t 0 ]]; then
AGENT_HEARTBEAT_INTERVAL_SECONDS="$default_interval"
return 0
fi
echo -ne " ${BOLD}Agent 上报间隔秒数 [默认 ${default_interval},范围 5-3600]: ${NC}"
local agent_interval=""
read -r agent_interval
if [[ -z "$agent_interval" ]]; then
AGENT_HEARTBEAT_INTERVAL_SECONDS="$default_interval"
elif [[ "$agent_interval" =~ ^[0-9]+$ ]] && (( agent_interval >= 5 && agent_interval <= 3600 )); then
AGENT_HEARTBEAT_INTERVAL_SECONDS="$agent_interval"
else
warn "Agent 上报间隔无效,已使用默认 ${default_interval} 秒"
AGENT_HEARTBEAT_INTERVAL_SECONDS="$default_interval"
fi
}
show_agent_summary_line() {
local label="${DIM}未安装${NC}"
if command -v systemctl &>/dev/null && systemctl is-active --quiet "$AGENT_SERVICE_NAME" 2>/dev/null; then
label="${GREEN}运行中${NC}"
elif [[ -f "$AGENT_CONFIG_FILE" || -x "$AGENT_BIN_PATH" ]]; then
label="${YELLOW}已安装(未运行)${NC}"
fi
echo -e " Agent : ${label}"
}
# 清理临时文件
cleanup() {
rm -f "$PRESEED_FILE" 2>/dev/null || true
}
trap cleanup EXIT
# ========================== 显示横幅 ==========================
show_banner() {
clear 2>/dev/null || true
echo ""
echo -e "${CYAN} ╔══════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN} ║ ║${NC}"
echo -e "${CYAN}${BOLD}Incudal 节点安装管理脚本${NC}${CYAN}${NC}"
echo -e "${CYAN}${DIM}LXC Container Host Setup${NC}${CYAN}${NC}"
echo -e "${CYAN} ║ ║${NC}"
echo -e "${CYAN} ╚══════════════════════════════════════════════════╝${NC}"
echo ""
echo -e " ${DIM}版本: ${SCRIPT_VERSION} | 面板: ${PANEL_URL}${NC}"
echo ""
}
# ========================== 系统检测 ==========================
detect_system() {
# 检测 /etc/os-release 是否存在
if [[ ! -f /etc/os-release ]]; then
error "无法检测操作系统(/etc/os-release 不存在)"
exit 1
fi
source /etc/os-release
OS_ID="${ID:-unknown}"
OS_VERSION="${VERSION_ID:-unknown}"
OS_CODENAME="${VERSION_CODENAME:-unknown}"
ARCH=$(dpkg --print-architecture 2>/dev/null || uname -m)
# 仅支持 Ubuntu 和 Debian
if [[ "$OS_ID" != "ubuntu" && "$OS_ID" != "debian" ]]; then
error "不支持的操作系统: ${OS_ID}"
error "本脚本仅支持 Ubuntu 和 Debian 系统"
exit 1
fi
# 版本兼容性检查
case "$OS_ID" in
ubuntu)
# 建议 Ubuntu 22.04+
local ubuntu_major="${OS_VERSION%%.*}"
if [[ "$ubuntu_major" -lt 22 ]] 2>/dev/null; then
warn "Ubuntu 版本较低 (${OS_VERSION}),建议使用 22.04 或 24.04"
echo -ne " 是否继续?[y/N]: "
read -r confirm
[[ "$confirm" =~ ^[yY]$ ]] || exit 0
fi
;;
debian)
# 最低 Debian 11 (Bullseye)
local deb_major="${OS_VERSION%%.*}"
if [[ "$deb_major" -lt 11 ]] 2>/dev/null; then
error "Debian 版本过低 (${OS_VERSION}),最低要求 Debian 11 (Bullseye)"
exit 1
fi
;;
esac
# 检测默认网络接口
DEFAULT_IFACE=$(ip route get 8.8.8.8 2>/dev/null | awk -F'dev ' '{print $2}' | awk '{print $1}' | head -n1 || true)
if [[ -z "$DEFAULT_IFACE" ]]; then
warn "无法通过默认路由检测网络接口"
# 回退:获取第一个非 lo 的 UP 接口
DEFAULT_IFACE=$(ip -o link show up 2>/dev/null | awk -F': ' '{print $2}' | grep -v lo | head -n1 || true)
if [[ -z "$DEFAULT_IFACE" ]]; then
error "未找到可用的网络接口"
exit 1
fi
info "使用备选接口: ${DEFAULT_IFACE}"
fi
# 纯 IPv6 环境检测 (双通道并发)
if ! curl -4 -s --connect-timeout 2 1.1.1.1 >/dev/null 2>&1; then
if curl -6 -s --connect-timeout 2 "https://[2606:4700:4700::1111]" >/dev/null 2>&1; then
IS_PURE_IPV6="true"
info "检测到当前服务器为【纯 IPv6 (Pure IPv6)】"
fi
fi
}
# ========================== 显示系统信息 ==========================
show_system_info() {
divider
echo -e " ${BOLD}系统信息${NC}"
divider
# 操作系统名称格式化
local os_label=""
case "$OS_ID" in
ubuntu) os_label="Ubuntu" ;;
debian) os_label="Debian" ;;
*) os_label="$OS_ID" ;;
esac
echo -e " 操作系统 : ${GREEN}${os_label} ${OS_VERSION}${NC} (${OS_CODENAME})"
echo -e " 系统架构 : ${GREEN}${ARCH}${NC}"
echo -e " 默认接口 : ${GREEN}${DEFAULT_IFACE}${NC}"
echo -e " 主 机 名 : ${GREEN}$(hostname)${NC}"
if [[ "$IS_PURE_IPV6" == "true" ]]; then
echo -e " 网络环境 : ${YELLOW}纯 IPv6 (Pure IPv6) - 将启用 WARP 出站接力${NC}"
else
echo -e " 网络环境 : ${GREEN}原生 IPv4 可达${NC}"
fi
# 检查 Incus 安装状态
if command -v incus &>/dev/null; then
local incus_ver
incus_ver=$(incus version 2>/dev/null | awk '/Client version/ {print $3}' || echo "未知")
[[ -z "$incus_ver" ]] && incus_ver="未知"
# 判断服务端是否能连通
if ! incus version 2>/dev/null | grep -q -E "Server version: [0-9]"; then
echo -e " Incus : ${YELLOW}客户端残留(服务未在运行)${NC} (${incus_ver})"
else
echo -e " Incus : ${GREEN}已安装并运行${NC} (${incus_ver})"
fi
# 检查网桥
if incus network show "$BRIDGE_NAME" &>/dev/null; then
echo -e " 网桥状态 : ${YELLOW}${BRIDGE_NAME} 已存在${NC}"
else
echo -e " 网桥状态 : ${DIM}未初始化${NC}"
fi
# 检查面板证书
if incus config trust list --format csv 2>/dev/null | grep -q "panel"; then
echo -e " 面板证书 : ${YELLOW}已导入${NC}"
else
echo -e " 面板证书 : ${DIM}未导入${NC}"
fi
else
echo -e " Incus : ${DIM}未安装${NC}"
fi
# 检查 RFW 防火墙状态
if [[ -f /root/rfw/rfw ]]; then
if systemctl is-active --quiet rfw 2>/dev/null; then
local rfw_rules="未知"
if [[ -f /etc/systemd/system/rfw.service ]]; then
local exec_start
exec_start=$(grep "^ExecStart=" /etc/systemd/system/rfw.service 2>/dev/null || true)
if [[ "$exec_start" =~ "--block-all" ]] && [[ ! "$exec_start" =~ "--block-all-from" ]]; then
rfw_rules="全部阻止"
else
rfw_rules=$(echo "$exec_start" | grep -o -- "--block-[a-z0-9-]*" | sed 's/--block-//g' | tr '\n' '/' | sed 's/\/$//')
[[ -z "$rfw_rules" ]] && rfw_rules="无协议过滤"
fi
local geo="无GeoIP"
if [[ "$exec_start" =~ --countries\ ([A-Za-z,]*) ]]; then
geo="黑名单:${BASH_REMATCH[1]}"
elif [[ "$exec_start" =~ --allow-only-countries\ ([A-Za-z,]*) ]]; then
geo="白名单:${BASH_REMATCH[1]}"
elif [[ "$exec_start" =~ --block-all-from\ ([A-Za-z,]*) ]]; then
geo="黑名单:${BASH_REMATCH[1]}"
fi
rfw_rules="${rfw_rules} | ${geo}"
fi
echo -e " RFW 防火墙: ${GREEN}运行中${NC} (${rfw_rules})"
else
echo -e " RFW 防火墙: ${YELLOW}已安装(未运行)${NC}"
fi
else
echo -e " RFW 防火墙: ${DIM}未安装${NC}"
fi
show_agent_summary_line
divider
echo ""
}
# ========================== 交互式菜单 ==========================
show_menu() {
echo -e " ${BOLD}请选择操作:${NC}"
echo ""
echo -e " ${CYAN}1)${NC} 安装节点 ${DIM}─ 仅 IPv4${NC}"
echo -e " ${CYAN}2)${NC} 安装节点 ${DIM}─ IPv4 + IPv6(自动检测配置)${NC}"
echo -e " ${CYAN}3)${NC} 安装 RFW ${DIM}─ 入站流量屏蔽防火墙${NC}"
echo -e " ${CYAN}4)${NC} 查看系统信息"
echo -e " ${CYAN}7)${NC} 网络模式说明(必看说明)"
echo -e " ${CYAN}8)${NC} Agent 管理 ${DIM}─ 安装 / 更新宿主机 Agent${NC}"
echo ""
echo -e " ${RED}5)${NC} 卸载 RFW ${DIM}─ 移除 RFW 防火墙${NC}"
echo -e " ${RED}6)${NC} 卸载节点 ${DIM}─ 彻底清理还原系统${NC}"
echo -e " ${CYAN}0)${NC} 退出"
echo ""
echo -ne " ${BOLD}请输入选项 [0-8]: ${NC}"
}
# ========================== 网络模式说明 ==========================
show_network_mode_help() {
echo ""
divider
echo -e " ${BOLD}面板支持的网络模式说明${NC}"
divider
echo ""
echo -e " ${BOLD}节点安装模式(本脚本选择):${NC}"
echo ""
echo -e " ${CYAN}模式 1:仅 IPv4${NC}"
echo -e " 节点只启用 IPv4 NAT 网桥,容器通过端口映射访问。"
echo -e " 适合没有 IPv6 网段或不需要 IPv6 的场景。"
echo ""
echo -e " ${CYAN}模式 2IPv4 + IPv6${NC}"
echo -e " 节点同时启用 IPv4 NAT 和 IPv6 路由转发。"
echo -e " 安装后可在面板创建以下任意 IPv6 网络模式的实例。"
echo ""
echo -e " ${BOLD}面板实例网络模式(在面板创建实例/套餐时选择):${NC}"
echo ""
echo -e " ${GREEN}IPv4 NAT${NC}"
echo -e " 宿主机仅有 IPv4,通过端口映射给容器提供对外服务。"
echo -e " ${DIM}节点要求:模式 1 或 模式 2 均可${NC}"
echo ""
echo -e " ${GREEN}IPv4 NAT + IPv6${NC}"
echo -e " 宿主机拥有 IPv4 + 独立公网 IPv6 地址(双栈)。"
echo -e " IPv4 通过端口映射,IPv6 根据网段选择独立分配给容器。"
echo -e " ${DIM}节点要求:模式 2 + 配置 IPv6 子网${NC}"
echo ""
echo -e " ${GREEN}IPv4 NAT + IPv6 NAT${NC}"
echo -e " 宿主机拥有 IPv4 + IPv6(均为 NAT 共享宿主机出口)。"
echo -e " 适用于宿主机只有单个 IPv6 地址或者不分配 IPv6 网段,全局共享宿主机的 IPv6。"
echo -e " ${DIM}节点要求:模式 2,无需 IPv6 子网${NC}"
echo ""
echo -e " ${GREEN}IPv6 Only${NC}"
echo -e " 宿主机仅有独立公网 IPv6(无 IPv4)。"
echo -e " ${DIM}节点要求:模式 2 + 配置 IPv6 子网${NC}"
echo ""
echo -e " ${GREEN}IPv6 NAT${NC}"
echo -e " 宿主机仅有单个 IPv6(共享宿主机 IPv6 出口,无 IPv4)。"
echo -e " ${DIM}节点要求:模式 2,无需 IPv6 子网${NC}"
echo ""
echo -e " ${YELLOW}提示:${NC}本脚本选择模式 2 后,会自动检测宿主机 IPv6,"
echo -e " 并帮助计算适合面板使用的 IPv6 子网。安装完成后在面板"
echo -e " 「节点管理」中填入子网信息即可。"
echo ""
divider
echo ""
}
# ========================== 读取 Token ==========================
read_token() {
echo ""
divider
echo -e " ${BOLD}请输入面板 Token${NC}"
echo -e " ${DIM}Token 可在面板「节点管理 → 添加节点」中获取${NC}"
divider
echo ""
while true; do
echo -ne " ${CYAN}Token: ${NC}"
read -r TOKEN
# 非空检查
if [[ -z "$TOKEN" ]]; then
warn "Token 不能为空,请重新输入"
continue
fi
# 去除首尾空格
TOKEN=$(echo "$TOKEN" | xargs)
# Token 格式校验(宽松匹配:允许较长的数字/字母/短横线组合)
if [[ ! "$TOKEN" =~ ^[a-zA-Z0-9_-]{16,}$ ]]; then
warn "Token 格式不符合预期(可能不完整或包含非法字符)"
echo -e " ${DIM}说明: Token 通常是一串较长的包含数字和字母的字符串${NC}"
echo -ne " 是否仍要使用此 Token[y/N]: "
read -r confirm
[[ "$confirm" =~ ^[yY]$ ]] || continue
fi
break
done
}
# ========================== Agent 上报间隔 ==========================
configure_agent_heartbeat_interval() {
if [[ "$AGENT_ENABLED" != "true" || ( -z "$AGENT_INSTALL_TOKEN" && ( -z "$AGENT_ID" || -z "$AGENT_SECRET" ) ) ]]; then
AGENT_HEARTBEAT_INTERVAL_SECONDS="30"
return 0
fi
if [[ ! -t 0 ]]; then
AGENT_HEARTBEAT_INTERVAL_SECONDS="30"
return 0
fi
echo -e "\n${CYAN}==> (可选) Agent 上报间隔${NC}"
echo -e " ${DIM}默认 30 秒;面板宿主机状态卡片会自动跟随此间隔刷新${NC}"
echo -ne " ${BOLD}Agent 上报间隔秒数 [默认 30,范围 5-3600]: ${NC}"
local agent_interval=""
read -r agent_interval
if [[ -z "$agent_interval" ]]; then
AGENT_HEARTBEAT_INTERVAL_SECONDS="30"
elif [[ "$agent_interval" =~ ^[0-9]+$ ]] && (( agent_interval >= 5 && agent_interval <= 3600 )); then
AGENT_HEARTBEAT_INTERVAL_SECONDS="$agent_interval"
else
warn "Agent 上报间隔无效,已使用默认 30 秒"
AGENT_HEARTBEAT_INTERVAL_SECONDS="30"
fi
}
# ========================== 安装确认 ==========================
confirm_install() {
local mode_label=""
case "$MODE" in
nat) mode_label="仅 IPv4" ;;
nat_ipv6) mode_label="IPv4 + IPv6" ;;
esac
local os_label=""
case "$OS_ID" in
ubuntu) os_label="Ubuntu ${OS_VERSION}" ;;
debian) os_label="Debian ${OS_VERSION}" ;;
esac
# Token 脱敏显示(仅显示首8位和末4位)
local token_masked="${TOKEN:0:8}····${TOKEN: -4}"
echo ""
divider
echo -e " ${BOLD}安装确认${NC}"
divider
echo -e " 操作系统 : ${GREEN}${os_label}${NC} (${OS_CODENAME})"
echo -e " 网络模式 : ${GREEN}${mode_label}${NC}"
echo -e " 网桥子网 : ${GREEN}${BRIDGE_SUBNET}${NC}"
if [[ -n "${IPV6_SUBNET:-}" ]]; then
echo -e " IPv6 子网 : ${GREEN}${IPV6_SUBNET}${NC} (${IPV6_IFACE:-auto})"
fi
echo -e " 通信端口 : ${GREEN}[::]:${LISTEN_PORT}${NC}"
echo -e " Token : ${GREEN}${token_masked}${NC}"
if [[ "$AGENT_ENABLED" == "true" && ( -n "$AGENT_INSTALL_TOKEN" || ( -n "$AGENT_ID" && -n "$AGENT_SECRET" ) ) ]]; then
echo -e " Agent : ${GREEN}${AGENT_HEARTBEAT_INTERVAL_SECONDS} 秒上报${NC}"
fi
if [[ "$IS_PURE_IPV6" == "true" ]]; then
if [[ "${SKIP_WARP:-false}" == "true" ]]; then
echo -e " IPv4 出站 : ${YELLOW}已跳过 WARP (保持纯净原生 IPv6)${NC}"
else
echo -e " IPv4 出站 : ${GREEN}Cloudflare WARP 虚拟网络接驳${NC}"
fi
fi
divider
echo ""
echo -ne " ${YELLOW}确认开始安装?${NC}[y/N]: "
read -r confirm
if [[ ! "$confirm" =~ ^[yY]$ ]]; then
info "已取消安装"
exit 0
fi
}
# ========================== IPv6 子网精准探测模块 ==========================
# 单个 IP 可达性探测
# 返回: 0=可达, 1=不可达
probe_single_ip() {
local test_ip="$1"
local iface="$2"
# 临时绑定测试 IP
if ! ip -6 addr add "${test_ip}/128" dev "$iface" 2>/dev/null; then
return 1
fi
# 短暂等待 NDP 生效
sleep 0.5
# 用该 IP 作为源地址 ping 外网(Google DNS IPv6
local ret=1
if ping6 -c 1 -W 3 -I "$test_ip" 2001:4860:4860::8888 &>/dev/null; then
ret=0
fi
# 无论成功失败,立即清理
ip -6 addr del "${test_ip}/128" dev "$iface" 2>/dev/null || true
return $ret
}
# IPv6 子网精准探测主函数
# 输入: $1=宿主机 IPv6 地址(不含前缀) $2=接口掩码前缀 $3=网卡名
# 输出全局变量:
# PROBE_TYPE: "full_64" | "small_block" | "single_ip" | "skip"
# PROBE_CIDR: 安全 CIDR(如 "2604:a880:2:d1::/64"
# PROBE_COUNT: 可用地址数量(数字或 "海量")
PROBE_TYPE=""
PROBE_CIDR=""
PROBE_COUNT="0"
probe_ipv6_subnet() {
local host_ip="$1"
local host_prefix="$2"
local iface="$3"
PROBE_TYPE=""
PROBE_CIDR=""
PROBE_COUNT="0"
# 前缀 > 64 的直接按小段/单 IP 处理(如 /128)
if [[ "$host_prefix" -gt 126 ]]; then
PROBE_TYPE="single_ip"
PROBE_COUNT="0"
return 0
fi
# 确保 python3 可用(用于 IPv6 地址偏移计算)
if ! command -v python3 &>/dev/null; then
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq >/dev/null 2>&1 || true
apt-get install -y -qq python3 >/dev/null 2>&1 || true
fi
if ! command -v python3 &>/dev/null; then
warn "python3 不可用,无法进行精准探测"
PROBE_TYPE="skip"
return 1
fi
echo -e " ${CYAN}▶ 第 1 步:远距探测(验证完整 /64 是否可用)...${NC}"
# ---- 第 1 级:远距探测 ----
# 生成一个距离宿主机 IP 尽可能远的同 /64 地址
local far_ip
far_ip=$(python3 -c "
import ipaddress
addr = ipaddress.IPv6Address('${host_ip}')
# 取 /64 网络前缀
net = ipaddress.IPv6Network(str(addr) + '/64', strict=False)
# 用网络地址 + 反转的 host 部分生成远端 IP
# 如原始 host 部分为 ::19,则远端为 ::ffff:ffff:ffff:fffe
far = net.network_address + (net.num_addresses - 2)
# 确保不与宿主机 IP 重合
if far == addr:
far = net.network_address + (net.num_addresses - 3)
print(str(far))
" 2>/dev/null)
if [[ -z "$far_ip" ]]; then
PROBE_TYPE="skip"
return 1
fi
if probe_single_ip "$far_ip" "$iface"; then
echo -e " ${GREEN}✓ 远距探测通过 → 完整 /64 可用${NC}"
# 计算 /64 网络地址
local net64
net64=$(python3 -c "
import ipaddress
net = ipaddress.IPv6Network('${host_ip}/64', strict=False)
print(str(net))
" 2>/dev/null)
PROBE_TYPE="full_64"
PROBE_CIDR="$net64"
PROBE_COUNT="海量"
return 0
fi
echo -e " ${YELLOW}✗ 远距探测未通过,非完整 /64${NC}"
echo -e " ${CYAN}▶ 第 2 步:近距探测(验证是否有额外可用 IP)...${NC}"
# ---- 第 2 级:近距探测 ----
local near_ip
near_ip=$(python3 -c "
import ipaddress
addr = ipaddress.IPv6Address('${host_ip}')
print(str(addr + 2))
" 2>/dev/null)
if [[ -z "$near_ip" ]]; then
PROBE_TYPE="skip"
return 1
fi
if ! probe_single_ip "$near_ip" "$iface"; then
echo -e " ${YELLOW}✗ 近距探测未通过 → 仅单个 IP 可用${NC}"
PROBE_TYPE="single_ip"
PROBE_COUNT="0"
return 0
fi
echo -e " ${GREEN}✓ 近距探测通过,存在额外可用 IP${NC}"
echo -e " ${CYAN}▶ 第 3 步:边界收敛(精确定位可用范围)...${NC}"
# ---- 第 3 级:指数探测 + 二分收敛 ----
local last_pass=2
local first_fail=0
local offset=4
# 指数递增找到第一个失败的偏移
while [[ $offset -le 65536 ]]; do
local test_ip_exp
test_ip_exp=$(python3 -c "
import ipaddress
addr = ipaddress.IPv6Address('${host_ip}')
print(str(addr + ${offset}))
" 2>/dev/null)
if [[ -z "$test_ip_exp" ]]; then
first_fail=$offset
break
fi
echo -ne " 测试偏移 +${offset}... "
if probe_single_ip "$test_ip_exp" "$iface"; then
echo -e "${GREEN}${NC}"
last_pass=$offset
offset=$((offset * 2))
else
echo -e "${YELLOW}${NC}"
first_fail=$offset
break
fi
done
# 如果 65536 都通过了,当作大段处理
if [[ $first_fail -eq 0 ]]; then
echo -e " ${GREEN}✓ 可用 IP 数量极大(>65536${NC}"
local net64
net64=$(python3 -c "
import ipaddress
net = ipaddress.IPv6Network('${host_ip}/64', strict=False)
print(str(net))
" 2>/dev/null)
PROBE_TYPE="full_64"
PROBE_CIDR="$net64"
PROBE_COUNT="海量"
return 0
fi
# 二分查找精确边界
local lo=$last_pass
local hi=$first_fail
while [[ $((hi - lo)) -gt 1 ]]; do
local mid=$(( (lo + hi) / 2 ))
local test_ip_mid
test_ip_mid=$(python3 -c "
import ipaddress
addr = ipaddress.IPv6Address('${host_ip}')
print(str(addr + ${mid}))
" 2>/dev/null)
echo -ne " 二分测试 +${mid}... "
if probe_single_ip "$test_ip_mid" "$iface"; then
echo -e "${GREEN}${NC}"
lo=$mid
else
echo -e "${YELLOW}${NC}"
hi=$mid
fi
done
# lo 是最后一个通过的偏移,可用总数 = lo + 1(包含偏移 0 即宿主机本身)
# 但宿主机占用 1 个,所以容器可用 = lo
local usable_count=$lo
# 向下取整为最近的安全 CIDR
# 找到满足 2^n <= usable_count 的最大 n
local cidr_prefix
local cidr_count
cidr_prefix=$(python3 -c "
import ipaddress, math
host = ipaddress.IPv6Address('${host_ip}')
count = ${usable_count}
if count <= 0:
print('')
else:
# 找到 2^n <= count 的最大 n
n = int(math.log2(count)) if count > 0 else 0
if n < 1:
n = 1
prefix_len = 128 - n
# 计算对齐的网络地址
net = ipaddress.IPv6Network(str(host) + '/' + str(prefix_len), strict=False)
# 确保宿主机 IP 在此网络内
if host in net:
print(str(net) + '|' + str(2**n))
else:
# 尝试向上偏移一位
prefix_len += 1
n -= 1
net = ipaddress.IPv6Network(str(host) + '/' + str(prefix_len), strict=False)
print(str(net) + '|' + str(2**n))
" 2>/dev/null)
if [[ -z "$cidr_prefix" || "$cidr_prefix" == "" ]]; then
PROBE_TYPE="single_ip"
PROBE_COUNT="0"
return 0
fi
PROBE_TYPE="small_block"
PROBE_CIDR=$(echo "$cidr_prefix" | cut -d'|' -f1)
cidr_count=$(echo "$cidr_prefix" | cut -d'|' -f2)
PROBE_COUNT="$cidr_count"
echo -e " ${GREEN}✓ 检测完成:实际可用约 ${usable_count} 个 IP,安全上报为 ${PROBE_CIDR}${cidr_count} 个)${NC}"
return 0
}
# ========================== 纯 IPv6 支持功能模块 ==========================
setup_temp_network() {
step "临时急救:挂载 DNS64 解析补网..."
# 为能够下载 Github 的代码,临时注入公益 DNS64
if ! grep -q "2a00:1098:2b::1" /etc/resolv.conf; then
# 插入 resolv.conf 顶部
sed -i '1i nameserver 2a00:1098:2b::1\nnameserver 2a01:4f8:c2c:123f::1' /etc/resolv.conf
info "已临时写入 NAT64 路由节点,恢复对纯 IPv4 站点的访问能力"
fi
}
setup_warp_v4() {
step "破壁出击:安装 Cloudflare WARP 提供全局 IPv4 逃生出站隧道..."
# 核心修复一:在建隧道前,抓取并物理备份原生态纯净网关参数
REAL_V6_IFACE=$(ip -6 route show default 2>/dev/null | grep -v wg | grep -oP 'dev \K\S+' | head -1)
REAL_V6_GW=$(ip -6 route show default 2>/dev/null | grep -v wg | grep -oP 'via \K\S+' | head -1)
[ -z "$REAL_V6_IFACE" ] && REAL_V6_IFACE=$(ip -o -6 addr show scope global | grep -v wg | awk '{print $2}' | head -1)
echo "$REAL_V6_IFACE" > /etc/incudal_v6_iface
echo "$REAL_V6_GW" > /etc/incudal_v6_gw
# 强制不干涉内核锁版本依赖
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq 2>/dev/null || true
if ! apt-get install -y -qq wireguard-tools openresolv >/dev/null 2>&1; then
warn "wireguard-tools 安装失败(纯 IPv6 环境 apt 源可能不可达)"
warn "尝试仅安装 wireguard-tools..."
apt-get install -y -qq wireguard-tools >/dev/null 2>&1 || true
fi
# 检查 wireguard 是否可用
if ! command -v wg &>/dev/null; then
warn "WireGuard 工具未安装成功,跳过 WARP 配置"
warn "容器将仅使用 IPv6 出站,可稍后手动安装 WARP"
return 0
fi
local wgcf_url=""
if [[ "$ARCH" == "amd64" || "$ARCH" == "x86_64" ]]; then
wgcf_url="https://github.com/ViRb3/wgcf/releases/download/v2.2.22/wgcf_2.2.22_linux_amd64"
elif [[ "$ARCH" == "arm64" || "$ARCH" == "aarch64" ]]; then
wgcf_url="https://github.com/ViRb3/wgcf/releases/download/v2.2.22/wgcf_2.2.22_linux_arm64"
else
warn "架构不受支持,跳过 WARP 安装"
return 0
fi
info "拉取核心组件 wgcf..."
# 纯 IPv6 下 GitHub 可能不可达,多次重试
local retry=0
local max_retry=3
while [[ $retry -lt $max_retry ]]; do
if curl -sL --connect-timeout 30 --max-time 120 "$wgcf_url" -o /usr/local/bin/wgcf 2>/dev/null; then
# 验证下载是否成功(文件大于 1MB)
local fsize
fsize=$(stat -c%s /usr/local/bin/wgcf 2>/dev/null || echo "0")
if [[ "$fsize" -gt 1048576 ]]; then
break
fi
fi
retry=$((retry + 1))
if [[ $retry -lt $max_retry ]]; then
info "下载失败,${retry}/${max_retry} 次重试中(等待 5s..."
sleep 5
fi
done
if [[ ! -f /usr/local/bin/wgcf ]] || [[ $(stat -c%s /usr/local/bin/wgcf 2>/dev/null || echo "0") -lt 1048576 ]]; then
warn "wgcf 下载失败(纯 IPv6 环境无法访问 GitHub CDN"
warn "跳过 WARP 配置,容器将仅使用 IPv6 出站"
warn "解决方案: 手动下载 wgcf 到 /usr/local/bin/wgcf 后运行安装脚本"
rm -f /usr/local/bin/wgcf 2>/dev/null || true
return 0
fi
chmod +x /usr/local/bin/wgcf
mkdir -p /etc/wireguard
cd /etc/wireguard || return 0
if [[ ! -f wgcf-account.toml ]]; then
info "底层设备匿名注册申请中..."
yes | /usr/local/bin/wgcf register --accept-tos >/dev/null 2>&1 || true
fi
if [[ ! -f wgcf-profile.conf ]]; then
info "生成防污染隧道隔离切片..."
/usr/local/bin/wgcf generate >/dev/null 2>&1 || true
fi
if [[ -f wgcf-profile.conf ]]; then
# 核心切片:剔除接管所有 IPv6,把唯一的源出站权让给宿主机
sed -i '/AllowedIPs = ::\/0/d' wgcf-profile.conf
# 删除 IPv6 隧道地址,只保留 IPv4 隧道地址(防止 wg-quick 为 IPv6 地址添加路由规则)
sed -i 's/Address = \([^,]*\), *fd[0-9a-f:].*$/Address = \1/' wgcf-profile.conf
# 核心切片:将服务端入口转换为无缝直连 IPv6,防止断流握手
sed -i 's/Endpoint.*engage.cloudflareclient.com/Endpoint = \[2606:4700:d0::a29f:c001\]/g' wgcf-profile.conf
# 关键修复:删除 DNS 行,防止 wg-quick 在启停时劫持宿主机 /etc/resolv.conf
# 如果不删除,wg-quick down 会将 resolv.conf 回滚到原始的纯 IPv4 DNS(如 GCP 的 169.254.169.254),
# 导致宿主机和所有容器的 DNS 解析全部瘫痪
sed -i '/^DNS/d' wgcf-profile.conf
cp wgcf-profile.conf wg0.conf
chmod 600 wg0.conf
systemctl enable wg-quick@wg0 >/dev/null 2>&1 || true
systemctl start wg-quick@wg0 >/dev/null 2>&1 || true
info "内核级网卡 wg0 启动完毕,您的服务器已具备虚拟双栈特性!"
else
warn "WARP 配置文件未能生成(wgcf register 可能失败)"
warn "容器将仅使用 IPv6 出站,可稍后手动配置 WARP"
fi
# 注入出站控制器
cat > /usr/local/bin/incus-warp << 'EOF'
#!/bin/bash
if [[ "$1" == "on" ]]; then
systemctl start wg-quick@wg0 2>/dev/null
echo "🌍 [ON] WARP IPv4 抢救通道已激活。容器支持出海更新包下载。"
elif [[ "$1" == "off" ]]; then
systemctl stop wg-quick@wg0 2>/dev/null
# 核心修复二:使用强落地的环境备份读取真实原生网关,拒绝 fe80::1
REAL_V6_IFACE=$(cat /etc/incudal_v6_iface 2>/dev/null)
REAL_V6_GW=$(cat /etc/incudal_v6_gw 2>/dev/null)
[ -z "$REAL_V6_IFACE" ] && REAL_V6_IFACE=$(ip -o -6 addr show scope global | grep -v wg | awk '{print $2}' | head -1)
if [ -n "$REAL_V6_IFACE" ] && ! ip -6 route | grep "^default" | grep -v wg > /dev/null 2>&1; then
if [ -n "$REAL_V6_GW" ]; then
ip -6 route add default via "$REAL_V6_GW" dev "$REAL_V6_IFACE" 2>/dev/null || true
else
ip -6 route add default dev "$REAL_V6_IFACE" 2>/dev/null || true
fi
fi
# 核心修复三:全面压实纯 DNS64 集群阵列防止 0% 超时现象
cat > /etc/resolv.conf <<DNSEOF
# Incudal WARP-OFF 模式 - 纯 NAT64+DNS64 解析器
nameserver 2a00:1098:2b::1
nameserver 2a01:4f8:c2c:123f::1
nameserver 2001:67c:2b0::4
DNSEOF
echo "🧊 [OFF] WARP IPv4 通道已切断,原生真实 IPv6 路由结构已接管。"
else
echo -e "用法: \033[1;36mincus-warp [on/off]\033[0m"
if systemctl is-active wg-quick@wg0 >/dev/null 2>&1; then
echo -e "当前出站分流状态: \033[1;32m[ON] IPv6 原生 + IPv4 WARP 接驳\033[0m"
else
echo -e "当前出站分流状态: \033[1;31m[OFF] 100% 绝对纯净 IPv6 环境回落\033[0m"
fi
fi
EOF
chmod +x /usr/local/bin/incus-warp
info "出站调度指令控制台部署成功: 随时键入 incus-warp 操控"
# 部署 IPv6 路由恢复守护脚本(兜底保险)
cat > /usr/local/bin/ipv6-route-guard.sh << 'GUARD_EOF'
#!/bin/bash
# IPv6 路由恢复守护脚本 - 由 Incudal 安装脚本自动部署
# 每 10 秒检测 IPv6 默认路由,丢失则自动恢复
IPV6_IFACE=$(cat /etc/incudal_v6_iface 2>/dev/null)
IPV6_GATEWAY=$(cat /etc/incudal_v6_gw 2>/dev/null)
if [ -z "$IPV6_IFACE" ]; then
IPV6_IFACE=$(ip -6 route show default 2>/dev/null | grep -v wg | grep -oP 'dev \K\S+' | head -1)
[ -z "$IPV6_IFACE" ] && IPV6_IFACE=$(ip -o -6 addr show scope global | grep -v wg | awk '{print $2}' | head -1)
fi
[ -z "$IPV6_IFACE" ] && exit 0
while true; do
# 检查是否存在非 WireGuard 接口的 IPv6 默认路由
if ! ip -6 route | grep "^default" | grep -v wg > /dev/null 2>&1; then
if [ -n "$IPV6_GATEWAY" ]; then
ip -6 route add default via "$IPV6_GATEWAY" dev "$IPV6_IFACE" 2>/dev/null
else
ip -6 route add default dev "$IPV6_IFACE" 2>/dev/null
fi
fi
sleep 10
done
GUARD_EOF
chmod +x /usr/local/bin/ipv6-route-guard.sh
# 创建 systemd 服务
cat > /etc/systemd/system/ipv6-route-guard.service << 'SVC_EOF'
[Unit]
Description=IPv6 Route Guard - Incudal 自动恢复 IPv6 默认路由
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/ipv6-route-guard.sh
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
SVC_EOF
systemctl daemon-reload
systemctl enable --now ipv6-route-guard > /dev/null 2>&1 || true
info "IPv6 路由守护已部署: 即使 WARP 意外断联,IPv6 也将在 10 秒内自愈"
}
# ========================== 安装步骤 ==========================
# 步骤 1: 配置内核参数
setup_kernel() {
step "步骤 [1/5] 配置内核参数..."
# 加载网桥过滤模块
echo "br_netfilter" > /etc/modules-load.d/br_netfilter.conf
modprobe br_netfilter || true
# 基础 sysctl 参数 + BBR 拥塞控制 + TCP 缓冲区优化
cat > /etc/sysctl.d/99-incus.conf <<EOF
# Incudal 节点内核参数 - 由安装脚本自动生成
# ======== 文件系统 ========
fs.inotify.max_user_instances = 1048576
fs.file-max = 6815744
# ======== 网桥过滤 ========
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-arptables = 1
# ======== IPv4 转发与路由 ========
net.ipv4.ip_forward = 1
net.ipv4.conf.all.route_localnet = 1
net.ipv4.conf.all.forwarding = 1
net.ipv4.conf.default.forwarding = 1
# ======== BBR 拥塞控制 ========
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
# ======== TCP 性能优化 ========
net.ipv4.tcp_no_metrics_save = 1
net.ipv4.tcp_ecn = 0
net.ipv4.tcp_frto = 0
net.ipv4.tcp_mtu_probing = 0
net.ipv4.tcp_rfc1337 = 0
net.ipv4.tcp_sack = 1
net.ipv4.tcp_fack = 1
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_adv_win_scale = 1
net.ipv4.tcp_moderate_rcvbuf = 1
# ======== 网络缓冲区 ========
net.core.rmem_max = 33554432
net.core.wmem_max = 33554432
net.ipv4.tcp_rmem = 4096 87380 33554432
net.ipv4.tcp_wmem = 4096 16384 33554432
net.ipv4.udp_rmem_min = 8192
net.ipv4.udp_wmem_min = 8192
# ======== IPv6 转发(所有模式默认启用) ========
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.default.forwarding = 1
EOF
# IPv6 NAT 模式追加代理与路由通告参数
if [[ "$MODE" == "nat_ipv6" ]]; then
cat >> /etc/sysctl.d/99-incus.conf <<EOF
# IPv6 NAT 模式专用参数
net.ipv6.conf.all.proxy_ndp = 1
net.ipv6.conf.all.accept_ra = 2
net.ipv6.conf.default.accept_ra = 2
net.ipv6.conf.${DEFAULT_IFACE}.accept_ra = 2
net.ipv6.conf.${DEFAULT_IFACE}.proxy_ndp = 1
EOF
fi
sysctl -p >/dev/null 2>&1 || true
sysctl --system >/dev/null 2>&1 || true
# 验证 BBR 是否生效
if lsmod | grep -q bbr 2>/dev/null || sysctl net.ipv4.tcp_congestion_control 2>/dev/null | grep -q bbr; then
log "BBR 拥塞控制已启用 ✓"
else
warn "BBR 未能自动加载(内核可能不支持),TCP 将使用默认拥塞算法"
fi
log "内核参数配置完成(含 BBR + TCP 优化)"
}
# 步骤 2: 安装系统依赖
install_deps() {
step "步骤 [2/5] 安装系统依赖..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq 2>/dev/null
# Debian 系统需要确保 contrib 组件已启用(ZFS 包在 contrib 中)
if [[ "$OS_ID" == "debian" ]]; then
local contrib_enabled=false
# 检查 DEB822 格式源文件(Debian 12+
if [[ -f /etc/apt/sources.list.d/debian.sources ]]; then
if grep -q "contrib" /etc/apt/sources.list.d/debian.sources 2>/dev/null; then
contrib_enabled=true
fi
fi
# 检查传统格式源文件
if [[ -f /etc/apt/sources.list ]]; then
if grep -q "contrib" /etc/apt/sources.list 2>/dev/null; then
contrib_enabled=true
fi
fi
if [[ "$contrib_enabled" == "false" ]]; then
info "Debian 系统:启用 contrib 组件以支持 ZFS..."
if [[ -f /etc/apt/sources.list.d/debian.sources ]]; then
sed -i 's/Components: main$/Components: main contrib/' \
/etc/apt/sources.list.d/debian.sources 2>/dev/null || true
elif [[ -f /etc/apt/sources.list ]]; then
sed -i '/^deb.*main/ { /contrib/! s/main/main contrib/ }' \
/etc/apt/sources.list 2>/dev/null || true
fi
apt-get update -qq 2>/dev/null
fi
fi
# 安装基础依赖
apt-get install -y -qq curl gpg >/dev/null 2>&1
# ---- Debian ZFS 安装策略 ----
# 优先级: 预编译包(秒级) → DKMS 编译(分钟级) → 跳过(使用 dir 存储池)
local debian_zfs_compiled=false
if [[ "$OS_ID" == "debian" ]]; then
local kernel_ver
kernel_ver=$(uname -r)
# 策略 1: 尝试下载预编译 ZFS 模块包
if install_zfs_prebuilt "$kernel_ver"; then
debian_zfs_compiled=false # 预编译模式不需要后续清理编译环境
log "系统依赖安装完成(ZFS 预编译模块,秒级部署)"
# 策略 2: 询问用户是否进行 DKMS 即时编译
else
warn "未找到内核 ${kernel_ver} 的预编译 ZFS 模块包"
echo ""
info "当前内核版本暂无预编译包,可选择 DKMS 即时编译(约 5-10 分钟)"
echo -ne " ${BOLD}是否进行实时编译?${NC}[Y/n]: "
read -r dkms_confirm || true
if [[ "${dkms_confirm:-}" =~ ^[nN]$ ]]; then
warn "已跳过 ZFS 安装,返回主菜单"
return 1
fi
info "开始 DKMS 即时编译..."
install_zfs_dkms
debian_zfs_compiled=true
fi
else
# Ubuntu: 直接安装(预编译模块随内核提供)
if apt-get install -y -qq zfsutils-linux >/dev/null 2>&1; then
log "系统依赖安装完成(含 ZFS"
else
warn "ZFS 工具安装失败,已跳过(面板可使用 dir/btrfs 存储池)"
log "基础依赖安装完成"
fi
fi
# Debian DKMS 编译后清理:删除编译工具链以释放资源
if [[ "$debian_zfs_compiled" == "true" ]]; then
info "ZFS DKMS 编译成功,清理编译环境以释放资源..."
# 锁定当前内核版本,防止自动更新时 DKMS 在无编译环境下重编译失败
local kernel_pkg
kernel_pkg=$(dpkg -l | awk '/^ii.*linux-image-[0-9]/ {print $2}' | head -n1 || true)
if [[ -n "$kernel_pkg" ]]; then
apt-mark hold "$kernel_pkg" >/dev/null 2>&1 || true
info "已锁定内核版本: ${kernel_pkg}(防止自动更新导致 ZFS 失效)"
fi
# 卸载编译工具链(gcc、g++、make 等,约 200-500MB
apt-get purge -y -qq build-essential cpp gcc g++ make dpkg-dev >/dev/null 2>&1 || true
# 卸载内核头文件(约 100-200MB)
apt-get purge -y -qq "linux-headers-$(uname -r)" linux-headers-* >/dev/null 2>&1 || true
# 自动清理不再需要的依赖
apt-get autoremove -y -qq >/dev/null 2>&1 || true
# 清理 APT 下载缓存
apt-get clean 2>/dev/null || true
log "编译环境已清理,磁盘空间已释放"
warn "注意:内核版本已锁定,如需更新内核请先重装编译依赖"
info "更新内核前请运行: apt install build-essential linux-headers-\$(uname -r)"
fi
}
# ---- Debian ZFS 策略 1: 预编译模块安装 ----
install_zfs_prebuilt() {
local kernel_ver="$1"
local prebuilt_url="${ZFS_PREBUILT_URL}/zfs-modules-${kernel_ver}.tar.gz"
local tmp_tar="/tmp/zfs-prebuilt-$$.tar.gz"
local tmp_dir="/tmp/zfs-prebuilt-$$"
info "尝试下载预编译 ZFS 模块 (${kernel_ver})..."
info "下载地址: ${prebuilt_url}"
# 下载预编译包
if ! curl -sSfL --connect-timeout 10 --max-time 60 \
"$prebuilt_url" -o "$tmp_tar" 2>/dev/null; then
info "未找到内核 ${kernel_ver} 的预编译包"
rm -f "$tmp_tar" 2>/dev/null || true
return 1
fi
info "预编译包下载成功,开始安装..."
# 解压
mkdir -p "$tmp_dir"
if ! tar -xzf "$tmp_tar" -C "$tmp_dir" 2>/dev/null; then
error "预编译包解压失败"
rm -rf "$tmp_tar" "$tmp_dir" 2>/dev/null || true
return 1
fi
# 读取元数据,获取模块安装路径
local pack_dir
pack_dir=$(find "$tmp_dir" -name "metadata.txt" -printf '%h\n' 2>/dev/null | head -n1)
if [[ -z "$pack_dir" ]]; then
error "预编译包格式无效(缺少 metadata.txt"
rm -rf "$tmp_tar" "$tmp_dir" 2>/dev/null || true
return 1
fi
# 验证内核版本匹配
local pkg_kernel
pkg_kernel=$(awk -F= '/^kernel_version=/{print $2}' "$pack_dir/metadata.txt" 2>/dev/null)
if [[ "$pkg_kernel" != "$kernel_ver" ]]; then
warn "预编译包内核版本不匹配 (包: ${pkg_kernel}, 本机: ${kernel_ver})"
rm -rf "$tmp_tar" "$tmp_dir" 2>/dev/null || true
return 1
fi
# 获取模块安装路径
local module_path
module_path=$(awk -F= '/^module_path=/{print $2}' "$pack_dir/metadata.txt" 2>/dev/null)
if [[ -z "$module_path" ]]; then
module_path="updates/dkms" # 默认路径
fi
# 复制模块文件到正确位置
local target_dir="/lib/modules/${kernel_ver}/${module_path}"
mkdir -p "$target_dir"
cp "$pack_dir/modules/"*.ko* "$target_dir/" 2>/dev/null
local ko_count
ko_count=$(find "$target_dir" -name "*.ko*" -type f 2>/dev/null | wc -l)
info "已安装 ${ko_count} 个内核模块 → ${target_dir}"
# 更新模块依赖关系
depmod -a 2>/dev/null || true
# 加载 ZFS 模块验证
if ! modprobe zfs 2>/dev/null; then
error "预编译模块加载失败"
rm -rf "$tmp_tar" "$tmp_dir" 2>/dev/null || true
return 1
fi
info "ZFS 内核模块加载成功 ✓"
# 安装 ZFS 用户空间工具(不拉取 DKMS,避免触发编译)
info "安装 ZFS 用户空间工具..."
apt-get install -y -qq --no-install-recommends zfsutils-linux >/dev/null 2>&1 || {
# 如果 --no-install-recommends 不够,强制跳过 dkms
apt-get install -y -qq zfsutils-linux >/dev/null 2>&1 || true
}
# 锁定内核版本
local kernel_pkg
kernel_pkg=$(dpkg -l | awk '/^ii.*linux-image-[0-9]/ {print $2}' | head -n1 || true)
if [[ -n "$kernel_pkg" ]]; then
apt-mark hold "$kernel_pkg" >/dev/null 2>&1 || true
info "已锁定内核版本: ${kernel_pkg}"
fi
# 清理临时文件
rm -rf "$tmp_tar" "$tmp_dir" 2>/dev/null || true
log "ZFS 预编译模块安装成功(内核: ${kernel_ver}"
return 0
}
# ---- Debian ZFS 策略 2: DKMS 即时编译(回退方案)----
install_zfs_dkms() {
info "安装 DKMS 编译依赖(linux-headers、build-essential..."
# 优先安装通用编译核心工具,防止一处失败导致全部跳过
apt-get install -y -qq build-essential dkms >/dev/null 2>&1 || true
# 尝试安装精确版本内核源码头
if ! apt-get install -y -qq "linux-headers-$(uname -r)" >/dev/null 2>&1; then
warn "未找到精确的内核头文件 linux-headers-$(uname -r)"
info "正尝试拉取架构级通用头文件(linux-headers-amd64..."
apt-get install -y -qq linux-headers-amd64 >/dev/null 2>&1 || {
warn "内核头文件均安装失败,ZFS DKMS 编译极可能无法正常进行"
}
fi
info "开始 DKMS 编译 ZFS 模块(CPU 将跑满,请耐心等待)..."
local start_time=$SECONDS
if apt-get install -y -qq zfsutils-linux >/dev/null 2>&1; then
local elapsed=$(( SECONDS - start_time ))
info "编译耗时: ${elapsed} 秒"
# 验证 ZFS 模块
if modprobe zfs 2>/dev/null; then
log "ZFS DKMS 编译成功(模块已加载)"
else
warn "ZFS 工具已安装但内核模块加载失败(DKMS 编译可能不完整)"
info "面板仍可使用 dir/btrfs 存储池,ZFS 可稍后手动修复"
fi
else
warn "ZFS 安装失败,已跳过(面板可使用 dir/btrfs 存储池)"
fi
}
# 步骤 3: 安装 Incus
install_incus() {
step "步骤 [3/5] 安装 Incus..."
# 幂等性:已安装且服务端正常运行则跳过
if incus version 2>/dev/null | grep -q -E "Server version: [0-9]"; then
local current_ver
current_ver=$(incus version 2>/dev/null | awk '/Client version/ {print $3}' || echo "未知")
info "Incus 服务已安装并运行(版本: ${current_ver}),跳过安装"
return 0
fi
# 导入 Zabbly GPG 密钥
mkdir -p /etc/apt/keyrings
curl -fsSL https://pkgs.zabbly.com/key.asc \
| gpg --yes --dearmor -o /etc/apt/keyrings/zabbly.gpg
# 添加 Zabbly APT 源(同时支持 Ubuntu 和 Debian
cat > /etc/apt/sources.list.d/zabbly-incus-stable.sources <<SRC
Enabled: yes
Types: deb
URIs: https://pkgs.zabbly.com/incus/stable
Suites: ${OS_CODENAME}
Components: main
Architectures: ${ARCH}
Signed-By: /etc/apt/keyrings/zabbly.gpg
SRC
apt-get update -qq 2>/dev/null
apt-get install -y -qq incus >/dev/null
log "Incus 安装完成"
}
# 步骤 4: 初始化 Incus
init_incus() {
step "步骤 [4/5] 初始化 Incus..."
# 幂等性:网桥已存在则跳过
if incus network show "$BRIDGE_NAME" &>/dev/null; then
info "网桥 ${BRIDGE_NAME} 已存在,跳过初始化"
return 0
fi
# 生成 preseed 配置
local ipv6_block
if [[ "$MODE" == "nat_ipv6" ]]; then
info "面板受控模式 (IPv4 NAT + IPv6 环境): 准备建立内核转发链路"
if [[ -n "${IPV6_SUBNET:-}" ]]; then
# 有独立子网:routed 模式走 eth1 独立分配 IPv6,但网桥同时启用 IPv6 NAT 作为保底
# 这样 nat_ipv6_nat 模式的实例也能在同一节点上通过桥共享 IPv6 出口
ipv6_block="ipv6.address: auto\n ipv6.nat: \"true\"\n ipv6.dhcp: \"true\""
info "▶ [IPv6 配置分支] 独立路由网段 + 网桥 NAT 双通道模式,routed 直通与 NAT 共享可共存。"
else
# 用户选择单 IP 共享,网桥需要负责分发 ULA (内部 IPv6) 并 NAT 出口
ipv6_block="ipv6.address: auto\n ipv6.nat: \"true\"\n ipv6.dhcp: \"true\""
info "▶ [IPv6 配置分支] 单一公共 IP 模式 (Bridge: Auto+NAT),已激活内部 IPv6 出站共享代理功能。"
fi
# [核心修复区] Debian 默认闭合的内核转发
sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null 2>&1 || true
sysctl -w net.ipv6.conf.default.forwarding=1 >/dev/null 2>&1 || true
sysctl -w net.ipv6.conf.all.proxy_ndp=1 >/dev/null 2>&1 || true
sysctl -w net.ipv6.conf.default.proxy_ndp=1 >/dev/null 2>&1 || true
# 配置 ndppd (邻居发现),这是对非直连路由云主机的保底策略
if [[ -n "${IPV6_SUBNET:-}" && -n "${IPV6_IFACE:-}" ]]; then
export DEBIAN_FRONTEND=noninteractive
apt-get install -y -qq ndppd >/dev/null 2>&1 || true
cat > /etc/ndppd.conf <<EOF
proxy ${IPV6_IFACE} {
rule ${IPV6_SUBNET} {
auto
}
}
EOF
systemctl restart ndppd 2>/dev/null || true
systemctl enable ndppd 2>/dev/null || true
info "NDPPD 路由代理保活已附加配置"
fi
else
# MODE=nat(仅 IPv4):检测是否为纯 IPv6 环境
if [[ "$IS_PURE_IPV6" == "true" ]]; then
# 纯 IPv6 宿主机即使选了"仅 IPv4",也必须启用桥 IPv6 NAT,否则容器完全断网
ipv6_block="ipv6.address: auto\n ipv6.nat: \"true\"\n ipv6.dhcp: \"true\""
info "▶ [IPv6 自动修正] 检测到纯 IPv6 环境,已自动启用网桥 IPv6 NAT 防止容器断网。"
else
ipv6_block="ipv6.address: none"
fi
fi
# 纯 IPv6 环境:网桥必须强制配置 100% 纯 NAT64 提供商,防止容器内的双栈域名引发 IPv4 DNS 污染死锁超时
local dns_block=""
if [[ "$IS_PURE_IPV6" == "true" ]]; then
dns_block="dns.nameservers: 2a00:1098:2b::1,2a01:4f8:c2c:123f::1,2001:67c:2b0::4"
info "纯 IPv6 环境:已为网桥独家配置纯公益 NAT64/DNS64 集群,护航出站"
fi
# 写入文件
cat > "$PRESEED_FILE" <<YAML
config:
core.https_address: '[::]:${LISTEN_PORT}'
networks:
- name: ${BRIDGE_NAME}
type: bridge
config:
ipv4.address: ${BRIDGE_SUBNET}
ipv4.nat: "true"
ipv4.dhcp: "true"
$(echo -e "$ipv6_block")
${dns_block:+$dns_block}
storage_pools: []
profiles: []
cluster: null
YAML
incus admin init --preseed < "$PRESEED_FILE"
log "Incus 初始化完成"
}
# 步骤 5: 导入面板证书
import_cert() {
step "步骤 [5/5] 导入面板信任证书..."
# 幂等性:证书已存在则跳过
if incus config trust list --format csv 2>/dev/null | grep -q "panel"; then
info "面板证书已存在,跳过导入"
return 0
fi
local cert_url="${PANEL_URL}/api/hosts/cert/${TOKEN}"
if ! curl -sSf "$cert_url" \
| incus config trust add-certificate - --name panel >/dev/null 2>&1; then
error "证书导入失败!请检查:"
error " 1. Token 是否正确"
error " 2. 面板 ${PANEL_URL} 是否可达"
error " 3. 网络连接是否正常"
exit 1
fi
log "面板证书导入成功"
}
# 附加步骤:安装宿主机 Agent
install_incudal_agent() {
if [[ "$AGENT_ENABLED" != "true" ]]; then
AGENT_INSTALL_STATUS="已跳过"
info "宿主机 Agent 自动安装已关闭,跳过"
return 0
fi
if [[ -z "$PANEL_URL" || ( -z "$AGENT_INSTALL_TOKEN" && ( -z "$AGENT_ID" || -z "$AGENT_SECRET" ) ) ]]; then
AGENT_INSTALL_STATUS="已跳过"
warn "未注入 Agent 安装 token 或凭据,跳过宿主机 Agent 安装"
return 0
fi
step "附加步骤 安装宿主机 Agent..."
local agent_install_url="${PANEL_URL}/api/agent/install.sh"
if curl -fsSL "$agent_install_url" | \
INCUDAL_PANEL_URL="$PANEL_URL" \
INCUDAL_AGENT_INSTALL_TOKEN="$AGENT_INSTALL_TOKEN" \
INCUDAL_AGENT_ID="$AGENT_ID" \
INCUDAL_AGENT_SECRET="$AGENT_SECRET" \
INCUDAL_AGENT_BINARY_URL="$AGENT_BINARY_URL" \
INCUDAL_HEARTBEAT_INTERVAL_SECONDS="$AGENT_HEARTBEAT_INTERVAL_SECONDS" \
bash; then
AGENT_INSTALL_STATUS="已安装"
log "宿主机 Agent 安装完成"
else
AGENT_INSTALL_STATUS="安装失败"
warn "宿主机 Agent 安装失败,Incus 节点初始化已完成,可稍后在面板重新生成 Agent 安装命令"
fi
return 0
}
# Agent 独立安装器调用
run_incudal_agent_installer() {
local panel_url="${1%/}"
local install_token="${2:-}"
local agent_id="${3:-}"
local agent_secret="${4:-}"
local heartbeat_interval="${5:-30}"
local binary_url="${6:-}"
if [[ -z "$panel_url" ]]; then
error "面板地址不能为空"
return 1
fi
if ! is_http_url "$panel_url"; then
error "面板地址格式无效: ${panel_url}"
return 1
fi
if [[ -z "$install_token" && ( -z "$agent_id" || -z "$agent_secret" ) ]]; then
error "缺少 Agent 安装 token 或本机 Agent 凭据"
return 1
fi
heartbeat_interval=$(normalize_agent_interval "$heartbeat_interval")
local agent_install_url="${panel_url}/api/agent/install.sh"
step "安装 / 更新宿主机 Agent..."
info "面板地址: ${panel_url}"
if [[ -n "$install_token" ]]; then
info "安装 token: $(mask_value "$install_token")"
else
info "Agent ID: $(mask_value "$agent_id")"
fi
if curl -fsSL "$agent_install_url" | \
INCUDAL_PANEL_URL="$panel_url" \
INCUDAL_AGENT_INSTALL_TOKEN="$install_token" \
INCUDAL_AGENT_ID="$agent_id" \
INCUDAL_AGENT_SECRET="$agent_secret" \
INCUDAL_AGENT_BINARY_URL="$binary_url" \
INCUDAL_HEARTBEAT_INTERVAL_SECONDS="$heartbeat_interval" \
bash; then
log "宿主机 Agent 安装 / 更新完成"
return 0
fi
error "宿主机 Agent 安装 / 更新失败"
return 1
}
show_incudal_agent_status() {
divider
echo -e " ${BOLD}Agent 状态${NC}"
divider
local panel_url=""
local agent_id=""
local heartbeat_interval=""
panel_url=$(read_agent_config_value "panel_url" || true)
agent_id=$(read_agent_config_value "agent_id" || true)
heartbeat_interval=$(read_agent_config_value "heartbeat_interval_seconds" || true)
heartbeat_interval=$(normalize_agent_interval "${heartbeat_interval:-30}")
if [[ -f "$AGENT_CONFIG_FILE" ]]; then
echo -e " 配置文件 : ${GREEN}${AGENT_CONFIG_FILE}${NC}"
echo -e " 面板地址 : ${GREEN}${panel_url:-未知}${NC}"
echo -e " Agent ID : ${GREEN}$(mask_value "$agent_id")${NC}"
echo -e " 上报间隔 : ${GREEN}${heartbeat_interval}${NC}"
else
echo -e " 配置文件 : ${DIM}未找到 (${AGENT_CONFIG_FILE})${NC}"
fi
if [[ -x "$AGENT_BIN_PATH" ]]; then
echo -e " 二进制 : ${GREEN}${AGENT_BIN_PATH}${NC}"
else
echo -e " 二进制 : ${DIM}未安装 (${AGENT_BIN_PATH})${NC}"
fi
if command -v systemctl &>/dev/null; then
if systemctl is-active --quiet "$AGENT_SERVICE_NAME" 2>/dev/null; then
echo -e " 服务状态 : ${GREEN}运行中${NC}"
elif systemctl list-unit-files "${AGENT_SERVICE_NAME}.service" --no-legend 2>/dev/null | grep -q "^${AGENT_SERVICE_NAME}.service"; then
echo -e " 服务状态 : ${YELLOW}已安装(未运行)${NC}"
else
echo -e " 服务状态 : ${DIM}未安装${NC}"
fi
else
echo -e " 服务状态 : ${DIM}systemctl 不可用${NC}"
fi
divider
}
prompt_agent_panel_url() {
local default_panel_url="${1:-}"
local panel_url=""
if [[ -t 0 ]]; then
if [[ -n "$default_panel_url" ]]; then
echo -ne " ${BOLD}面板地址 [默认 ${default_panel_url}]: ${NC}" >&2
else
echo -ne " ${BOLD}面板地址 (例如 https://panel.example.com): ${NC}" >&2
fi
read -r panel_url
fi
panel_url="${panel_url:-$default_panel_url}"
panel_url="${panel_url%/}"
if [[ -z "$panel_url" ]]; then
echo -e "${RED}[✗]${NC} 无法确定面板地址,请输入完整 https:// 地址" >&2
return 1
fi
if ! is_http_url "$panel_url"; then
echo -e "${RED}[✗]${NC} 面板地址格式无效: ${panel_url}" >&2
return 1
fi
echo "$panel_url"
}
update_incudal_agent_from_config() {
local panel_url=""
local agent_id=""
local agent_secret=""
local heartbeat_interval=""
panel_url=$(read_agent_config_value "panel_url" || true)
agent_id=$(read_agent_config_value "agent_id" || true)
agent_secret=$(read_agent_config_value "agent_secret" || true)
heartbeat_interval=$(read_agent_config_value "heartbeat_interval_seconds" || true)
heartbeat_interval=$(normalize_agent_interval "${heartbeat_interval:-30}")
if [[ -z "$panel_url" || -z "$agent_id" || -z "$agent_secret" ]]; then
warn "本机没有完整的 Agent 配置,无法直接更新"
info "如需补装,请先在面板为该宿主机生成 Agent 安装命令,再选择 token 补装"
return 1
fi
prompt_agent_heartbeat_interval "$heartbeat_interval"
run_incudal_agent_installer "$panel_url" "" "$agent_id" "$agent_secret" "$AGENT_HEARTBEAT_INTERVAL_SECONDS" "$AGENT_BINARY_URL"
}
install_incudal_agent_with_token() {
echo ""
divider
echo -e " ${BOLD}输入 Agent 一次性安装 token${NC}"
echo -e " ${DIM}可粘贴 ait_... token,也可粘贴面板生成的完整 Agent 安装命令。${NC}"
divider
echo ""
echo -ne " ${CYAN}Token / 命令: ${NC}"
local token_input=""
read -r token_input
local install_token=""
install_token=$(extract_agent_install_token "$token_input")
if [[ -z "$install_token" ]]; then
error "未找到 Agent 安装 token。Agent token 应以 ait_ 开头。"
return 1
fi
local extracted_panel_url=""
local default_panel_url=""
extracted_panel_url=$(extract_agent_panel_url "$token_input" || true)
default_panel_url="${extracted_panel_url:-$(get_agent_panel_url || true)}"
local panel_url=""
panel_url=$(prompt_agent_panel_url "$default_panel_url") || return 1
local current_interval=""
current_interval=$(read_agent_config_value "heartbeat_interval_seconds" || true)
prompt_agent_heartbeat_interval "${current_interval:-30}"
run_incudal_agent_installer "$panel_url" "$install_token" "" "" "$AGENT_HEARTBEAT_INTERVAL_SECONDS" "$AGENT_BINARY_URL"
}
show_incudal_agent_logs() {
step "查看 Agent 最近日志"
if ! command -v journalctl &>/dev/null; then
error "journalctl 不可用"
return 1
fi
journalctl -u "$AGENT_SERVICE_NAME" --no-pager -n 80 || true
}
manage_incudal_agent() {
while true; do
show_incudal_agent_status
echo -e " ${BOLD}请选择 Agent 操作:${NC}"
echo ""
echo -e " ${CYAN}1)${NC} 更新 / 重装已安装 Agent ${DIM}─ 使用本机配置${NC}"
echo -e " ${CYAN}2)${NC} 使用一次性 token 补装 / 重装 Agent"
echo -e " ${CYAN}3)${NC} 查看 Agent 日志"
echo -e " ${CYAN}0)${NC} 返回主菜单"
echo ""
echo -ne " ${BOLD}请输入选项 [0-3]: ${NC}"
local choice=""
read -r choice
echo ""
case "$choice" in
1) update_incudal_agent_from_config || true; pause_return ;;
2) install_incudal_agent_with_token || true; pause_return ;;
3) show_incudal_agent_logs || true; pause_return ;;
0) return 0 ;;
*) warn "无效选项,请重新选择" ;;
esac
done
}
# ========================== 安装结果 ==========================
show_result() {
local mode_label=""
case "$MODE" in
nat) mode_label="NAT(仅 IPv4" ;;
nat_ipv6) mode_label="NAT + IPv6" ;;
esac
# 安装 incudal 快捷命令
local script_path=""
script_path=$(readlink -f "$0" 2>/dev/null || echo "$0")
# 管道执行(curl | bash)时,$0 指向 bash 本身,不能直接复制
if [[ -f "$script_path" && ! "$script_path" =~ (bash|sh)$ ]]; then
# 正常文件执行:直接复制脚本
cp -f "$script_path" /usr/local/bin/incudal 2>/dev/null || true
chmod +x /usr/local/bin/incudal 2>/dev/null || true
elif [[ -f /root/incudal.sh ]]; then
# 回退方案 1:检查常见的下载位置
cp -f /root/incudal.sh /usr/local/bin/incudal 2>/dev/null || true
chmod +x /usr/local/bin/incudal 2>/dev/null || true
else
# 回退方案 2:创建自下载包装器,运行时从面板拉取最新脚本
cat > /usr/local/bin/incudal <<'SHORTCUT'
#!/bin/bash
# Incudal 节点管理快捷入口 - 自动下载最新版本
SCRIPT_CACHE="/root/incudal.sh"
SCRIPT_URL="https://raw.githubusercontent.com/0xdabiaoge/incudal/main/server/templates/install.sh"
echo "正在获取最新的节点管理脚本..."
if curl -sSfL "$SCRIPT_URL" -o "$SCRIPT_CACHE" 2>/dev/null; then
chmod +x "$SCRIPT_CACHE"
exec bash "$SCRIPT_CACHE" "$@"
else
echo "下载失败。如果本机是纯 IPv6 环境,请确保 WARP 已开启后重试。"
echo "或手动从面板获取安装脚本保存到 /root/incudal.sh"
exit 1
fi
SHORTCUT
chmod +x /usr/local/bin/incudal 2>/dev/null || true
fi
echo ""
echo -e "${GREEN} ╔══════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN} ║ ║${NC}"
echo -e "${GREEN} ║ ✓ 安装完成 ║${NC}"
echo -e "${GREEN} ║ ║${NC}"
echo -e "${GREEN} ╚══════════════════════════════════════════════════╝${NC}"
echo ""
echo -e " 网桥名称 : ${GREEN}${BRIDGE_NAME}${NC}"
echo -e " 网桥子网 : ${GREEN}${BRIDGE_SUBNET}${NC}"
echo -e " API 监听 : ${GREEN}[::]:${LISTEN_PORT}${NC}"
echo -e " 网络模式 : ${GREEN}${mode_label}${NC}"
echo -e " Agent : ${GREEN}${AGENT_INSTALL_STATUS}${NC}"
echo -e " 快捷命令 : ${GREEN}incudal${NC}"
echo ""
divider
echo -e " ${BOLD}下一步:${NC}请返回 Incudal 面板,点击「验证并连接」按钮完成注册"
echo -e " ${DIM}随时输入 incudal 可重新进入此管理脚本${NC}"
divider
echo ""
}
# ========================== RFW 防火墙 ==========================
# RFW 下载地址
readonly RFW_RELEASE_URL="https://github.com/0xdabiaoge/incudal-rfw/releases/latest/download"
readonly RFW_INSTALL_DIR="/root/rfw"
readonly RFW_SERVICE_FILE="/etc/systemd/system/rfw.service"
# RFW 交互式配置规则
configure_rfw_rules() {
RFW_ARGS=""
RFW_SUMMARY_RULES=""
RFW_SUMMARY_GEO=""
RFW_SUMMARY_LOG="关闭"
echo ""
divider
echo -e " ${BOLD}配置 RFW 屏蔽规则${NC}"
divider
echo ""
# 1. 协议屏蔽多选
echo -e " ┌─ 协议屏蔽(可多选,输入编号用空格分隔)──────────"
echo -e " │"
echo -e " │ 1) 屏蔽邮件发送 ─ SMTP 25/587/465/2525"
echo -e " │ 2) 屏蔽 HTTP 入站 ─ 明文 HTTP 协议探测"
echo -e " │ 3) 屏蔽 SOCKS5 入站 ─ 代理协议探测"
echo -e " │ 4) 屏蔽全加密流量 ─ SS/V2Ray(严格模式)"
echo -e " │ 5) 屏蔽 WireGuard ─ VPN 协议探测"
echo -e " │ 6) 屏蔽 QUIC/HTTP3 ─ QUIC 协议"
echo -e " │ 7) 屏蔽所有入站 ─ 最激进模式"
echo -e " │"
echo -e " │ A) 全选(1-6) D) 默认(1-5) C) 清空"
echo -e " └──────────────────────────────────────────────────"
echo ""
echo -ne " ${BOLD}请选择 [默认 D]: ${NC}"
read -r rule_choice || true
rule_choice=$(echo "${rule_choice:-D}" | tr '[:lower:]' '[:upper:]')
local selected_rules=()
local rule_names=()
local block_all=false
if [[ "$rule_choice" == "A" ]]; then
rule_choice="1 2 3 4 5 6"
elif [[ "$rule_choice" == "D" ]]; then
rule_choice="1 2 3 4 5"
elif [[ "$rule_choice" == "C" ]]; then
rule_choice=""
fi
for c in $rule_choice; do
case "$c" in
1) selected_rules+=("--block-email"); rule_names+=("Email") ;;
2) selected_rules+=("--block-http"); rule_names+=("HTTP") ;;
3) selected_rules+=("--block-socks5"); rule_names+=("SOCKS5") ;;
4) selected_rules+=("--block-fet-strict"); rule_names+=("FET-Strict") ;;
5) selected_rules+=("--block-wireguard"); rule_names+=("WireGuard") ;;
6) selected_rules+=("--block-quic"); rule_names+=("QUIC") ;;
7) block_all=true ;;
esac
done
if [[ "$block_all" == "true" ]]; then
RFW_ARGS+=" --block-all"
RFW_SUMMARY_RULES="所有入站"
else
if [[ ${#selected_rules[@]} -gt 0 ]]; then
RFW_ARGS+=" ${selected_rules[*]}"
RFW_SUMMARY_RULES=$(IFS=/ ; echo "${rule_names[*]}")
else
RFW_SUMMARY_RULES="无协议屏蔽"
fi
fi
# 2. GeoIP 模式
echo ""
echo -e " ┌─ GeoIP 过滤模式 ──────────────────────────────────"
echo -e " │"
echo -e " │ 1) 黑名单模式 ─ 屏蔽指定国家(推荐)"
echo -e " │ 2) 白名单模式 ─ 仅允许指定国家"
echo -e " │ 3) 不使用 GeoIP ─ 全局协议过滤"
echo -e " │"
echo -e " └──────────────────────────────────────────────────"
echo ""
echo -ne " ${BOLD}请选择 [默认 1]: ${NC}"
read -r geo_choice || true
geo_choice=${geo_choice:-1}
local countries=""
if [[ "$geo_choice" == "1" || "$geo_choice" == "2" ]]; then
echo -ne " ${BOLD}请输入国家代码(逗号分隔)[默认 CN]: ${NC}"
read -r countries || true
countries=${countries:-CN}
# 转换为大写并去除多余空格
countries=$(echo "$countries" | tr '[:lower:]' '[:upper:]' | tr -d ' ')
if [[ "$geo_choice" == "1" ]]; then
# 如果选了 block_all,则使用 --block-all-from 作为快捷方式
if [[ "$block_all" == "true" ]]; then
RFW_ARGS=$(echo "$RFW_ARGS" | sed 's/ --block-all//')
RFW_ARGS+=" --block-all-from $countries"
else
RFW_ARGS+=" --countries $countries"
fi
RFW_SUMMARY_GEO="黑名单 ($countries)"
else
RFW_ARGS+=" --allow-only-countries $countries"
RFW_SUMMARY_GEO="白名单 ($countries)"
fi
else
RFW_SUMMARY_GEO="不使用 GeoIP"
fi
# 3. 端口日志
echo ""
echo -e " ┌─ 其他选项 ──────────────────────────────────────"
echo -e " │"
echo -ne " │ 启用端口访问日志?[y/N]: ${NC}"
read -r log_choice || true
if [[ "${log_choice:-}" =~ ^[yY]$ ]]; then
RFW_ARGS+=" --log-port-access"
RFW_SUMMARY_LOG="开启"
fi
# 4. 配置确认
echo ""
echo -e " ┌─ 配置确认 ──────────────────────────────────────"
echo -e " │ 屏蔽规则 : ${GREEN}${RFW_SUMMARY_RULES}${NC}"
echo -e " │ GeoIP : ${GREEN}${RFW_SUMMARY_GEO}${NC}"
echo -e " │ 端口日志 : ${GREEN}${RFW_SUMMARY_LOG}${NC}"
echo -e " │ "
echo -e " │ 运行参数 : ${DIM}${RFW_ARGS}${NC}"
echo -e " └──────────────────────────────────────────────────"
echo ""
echo -ne " ${YELLOW}确认安装此配置?${NC}[Y/n]: "
read -r confirm || true
if [[ "${confirm:-Y}" =~ ^[nN]$ ]]; then
info "已取消配置"
return 1
fi
return 0
}
# 安装 RFW 防火墙
install_rfw() {
echo ""
divider
echo -e " ${BOLD}安装 RFW 入站流量屏蔽防火墙${NC}"
divider
echo ""
# 检查是否已安装
if [[ -f "${RFW_INSTALL_DIR}/rfw" ]] && systemctl is-active --quiet rfw 2>/dev/null; then
local rfw_status
rfw_status=$(systemctl is-active rfw 2>/dev/null || echo "未知")
warn "RFW 已安装且正在运行(状态: ${rfw_status}"
echo -ne " ${BOLD}是否重新安装?${NC}[y/N]: "
read -r reinstall || true
if [[ ! "${reinstall:-}" =~ ^[yY]$ ]]; then
info "已取消"
return 0
fi
# 先停止旧服务
systemctl stop rfw 2>/dev/null || true
systemctl disable rfw 2>/dev/null || true
fi
# 检测架构
step "检测系统架构..."
local arch_suffix=""
case "$(uname -m)" in
x86_64)
arch_suffix="x86_64"
;;
aarch64|arm64)
arch_suffix="aarch64"
;;
*)
error "不支持的架构: $(uname -m)(仅支持 x86_64 / aarch64"
return 1
;;
esac
log "系统架构: $(uname -m) (${arch_suffix})"
# 选择网络接口
step "选择网络接口..."
local interfaces=()
while IFS= read -r iface; do
[[ -z "$iface" ]] && continue
interfaces+=("$iface")
done < <(ip -o link show | awk -F': ' '{print $2}' | grep -v lo)
if [[ ${#interfaces[@]} -eq 0 ]]; then
error "未找到可用的网络接口"
return 1
fi
local selected_interface=""
if [[ ${#interfaces[@]} -eq 1 ]]; then
# 只有一个接口,自动选择
selected_interface="${interfaces[0]}"
info "自动选择网卡: ${selected_interface}"
else
echo ""
echo -e " 可用的网络接口:"
local i
for i in "${!interfaces[@]}"; do
local num=$((i + 1))
# 获取该接口的 IP
local iface_ip
iface_ip=$(ip -4 addr show "${interfaces[$i]}" 2>/dev/null | awk '/inet / {print $2}' | head -n1 || echo "")
if [[ -n "$iface_ip" ]]; then
echo -e " ${CYAN}${num})${NC} ${interfaces[$i]} ${DIM}(${iface_ip})${NC}"
else
echo -e " ${CYAN}${num})${NC} ${interfaces[$i]}"
fi
done
echo ""
while true; do
echo -ne " ${BOLD}请选择网卡编号 [1-${#interfaces[@]}]: ${NC}"
read -r iface_choice || true
if [[ "${iface_choice:-}" =~ ^[0-9]+$ ]] && \
[[ "$iface_choice" -ge 1 ]] && \
[[ "$iface_choice" -le "${#interfaces[@]}" ]]; then
selected_interface="${interfaces[$((iface_choice - 1))]}"
break
else
warn "无效输入,请重新选择"
fi
done
fi
log "使用网卡: ${selected_interface}"
# 下载 RFW 二进制文件
step "下载 RFW 程序..."
mkdir -p "$RFW_INSTALL_DIR"
local rfw_url="${RFW_RELEASE_URL}/rfw-${arch_suffix}-unknown-linux-musl"
local download_ok=false
local attempt
for attempt in 1 2 3; do
info "下载 RFW (第 ${attempt} 次)..."
if curl -sSfL --connect-timeout 15 --max-time 120 \
"$rfw_url" -o "${RFW_INSTALL_DIR}/rfw" 2>/dev/null; then
download_ok=true
break
else
warn "第 ${attempt} 次下载失败"
[[ "$attempt" -lt 3 ]] && sleep 3
fi
done
if [[ "$download_ok" != "true" ]]; then
error "RFW 下载失败(已重试 3 次)"
error "下载地址: ${rfw_url}"
return 1
fi
chmod +x "${RFW_INSTALL_DIR}/rfw"
log "RFW 下载完成"
# 交互式配置 RFW 规则
if ! configure_rfw_rules; then
return 0
fi
# 创建 systemd 服务
step "配置 RFW 服务..."
cat > "$RFW_SERVICE_FILE" <<EOF
[Unit]
Description=RFW Firewall Service
After=network.target
[Service]
Type=simple
User=root
Environment=RUST_LOG=info
ExecStart=${RFW_INSTALL_DIR}/rfw --iface ${selected_interface}${RFW_ARGS}
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
# 启动服务
step "启动 RFW 服务..."
systemctl start rfw
systemctl enable rfw 2>/dev/null || true
# 验证
sleep 2
if systemctl is-active --quiet rfw 2>/dev/null; then
echo ""
echo -e "${GREEN} ╔══════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN} ║ ║${NC}"
echo -e "${GREEN} ║ ✓ RFW 防火墙安装完成 ║${NC}"
echo -e "${GREEN} ║ ║${NC}"
echo -e "${GREEN} ╚══════════════════════════════════════════════════╝${NC}"
echo ""
echo -e " 监听网卡 : ${GREEN}${selected_interface}${NC}"
echo -e " 服务状态 : ${GREEN}运行中${NC}"
echo -e " 屏蔽规则 : ${GREEN}${RFW_SUMMARY_RULES}${NC}"
echo -e " GeoIP配置 : ${GREEN}${RFW_SUMMARY_GEO}${NC}"
echo -e " 端口日志 : ${GREEN}${RFW_SUMMARY_LOG}${NC}"
echo ""
divider
echo -e " ${DIM}查看日志: journalctl -u rfw -f${NC}"
echo -e " ${DIM}查看状态: systemctl status rfw${NC}"
if [[ "$RFW_SUMMARY_LOG" == "开启" ]]; then
echo -e " ${DIM}查看拦截: ${RFW_INSTALL_DIR}/rfw stats${NC}"
fi
divider
echo ""
else
error "RFW 服务启动失败"
error "请运行 journalctl -u rfw -n 20 查看日志"
fi
}
# 卸载 RFW 防火墙
uninstall_rfw() {
echo ""
# 检查是否安装
if [[ ! -f "${RFW_INSTALL_DIR}/rfw" ]] && \
! systemctl list-unit-files 2>/dev/null | grep -q "rfw.service"; then
warn "RFW 未安装,无需卸载"
return 0
fi
divider
echo -e " ${RED}${BOLD}卸载 RFW 防火墙${NC}"
divider
echo ""
echo -e " ${RED}将删除以下内容:${NC}"
echo -e " ${RED}${NC} RFW 二进制文件 (${RFW_INSTALL_DIR}/)"
echo -e " ${RED}${NC} RFW systemd 服务文件"
echo ""
echo -ne " ${BOLD}确认卸载 RFW${NC}[y/N]: "
read -r rfw_confirm || true
if [[ ! "${rfw_confirm:-}" =~ ^[yY]$ ]]; then
info "已取消"
return 0
fi
do_rfw_cleanup
}
# RFW 清理(供独立卸载和主卸载共用)
do_rfw_cleanup() {
info "停止 RFW 服务..."
systemctl stop rfw 2>/dev/null || true
systemctl disable rfw 2>/dev/null || true
# 删除服务文件
rm -f /etc/systemd/system/rfw.service 2>/dev/null || true
rm -f /usr/lib/systemd/system/rfw.service 2>/dev/null || true
rm -f /lib/systemd/system/rfw.service 2>/dev/null || true
# 删除程序目录
rm -rf "$RFW_INSTALL_DIR" 2>/dev/null || true
systemctl daemon-reload 2>/dev/null || true
log "RFW 防火墙已卸载"
}
# ========================== 卸载功能 ==========================
# 卸载确认(双重确认,防止误操作)
confirm_uninstall() {
echo ""
echo -e " ${RED}${BOLD}╔══════════════════════════════════════════════════╗${NC}"
echo -e " ${RED}${BOLD}║ ⚠ 卸载警告 ║${NC}"
echo -e " ${RED}${BOLD}╚══════════════════════════════════════════════════╝${NC}"
echo ""
echo -e " ${RED}此操作将彻底删除以下内容:${NC}"
echo ""
echo -e " ${RED}${NC} 所有 LXC 容器及其数据(不可恢复)"
echo -e " ${RED}${NC} 所有容器镜像和快照"
echo -e " ${RED}${NC} 所有存储池及数据"
echo -e " ${RED}${NC} 网桥 ${BRIDGE_NAME} 及网络配置"
echo -e " ${RED}${NC} Incus 软件包及配置"
echo -e " ${RED}${NC} Zabbly APT 源和 GPG 密钥"
echo -e " ${RED}${NC} 本脚本添加的内核参数"
echo ""
divider
echo -ne " ${RED}${BOLD}确认要彻底卸载 Incus 节点?${NC}[y/N]: "
read -r confirm1
if [[ ! "$confirm1" =~ ^[yY]$ ]]; then
info "已取消卸载"
exit 0
fi
echo ""
echo -ne " ${RED}${BOLD}再次确认:所有容器数据将永久丢失!${NC}输入 ${YELLOW}YES${NC} 继续: "
read -r confirm2
if [[ "$confirm2" != "YES" ]]; then
info "已取消卸载(需要输入大写 YES 确认)"
exit 0
fi
}
# 执行卸载
do_uninstall() {
confirm_uninstall
echo ""
divider
echo -e " ${BOLD}开始卸载...${NC}"
divider
# ---- 步骤 1: 停止并删除所有容器 ----
step "步骤 [1/8] 停止并删除所有容器..."
if command -v incus &>/dev/null; then
# 获取所有容器列表
local containers
containers=$(incus list --format csv -c n 2>/dev/null || true)
if [[ -n "$containers" ]]; then
while IFS= read -r cname; do
[[ -z "$cname" ]] && continue
info "停止容器: ${cname}"
incus stop "$cname" --force 2>/dev/null || true
info "删除容器: ${cname}"
incus delete "$cname" --force 2>/dev/null || true
done <<< "$containers"
log "所有容器已删除"
else
info "无运行中的容器"
fi
else
info "Incus 未安装,跳过容器清理"
fi
# ---- 步骤 2: 删除所有镜像 ----
step "步骤 [2/8] 清理容器镜像..."
if command -v incus &>/dev/null; then
local images
images=$(incus image list --format csv -c f 2>/dev/null || true)
if [[ -n "$images" ]]; then
while IFS= read -r fingerprint; do
[[ -z "$fingerprint" ]] && continue
incus image delete "$fingerprint" 2>/dev/null || true
done <<< "$images"
log "所有镜像已删除"
else
info "无缓存镜像"
fi
fi
# ---- 步骤 3: 删除网桥和存储池 ----
step "步骤 [3/8] 删除网络和存储池..."
if command -v incus &>/dev/null; then
# 删除面板信任证书
if incus config trust list --format csv 2>/dev/null | grep -q "panel"; then
info "移除面板信任证书"
incus config trust remove panel 2>/dev/null || true
fi
# 删除自定义 profile(保留 default
local profiles
profiles=$(incus profile list --format csv -c n 2>/dev/null | grep -v '^default$' || true)
if [[ -n "$profiles" ]]; then
while IFS= read -r pname; do
[[ -z "$pname" ]] && continue
info "删除 Profile: ${pname}"
incus profile delete "$pname" 2>/dev/null || true
done <<< "$profiles"
fi
# 删除网桥
if incus network show "$BRIDGE_NAME" &>/dev/null; then
info "删除网桥: ${BRIDGE_NAME}"
incus network delete "$BRIDGE_NAME" 2>/dev/null || true
fi
# 删除所有其他托管网络
local networks
networks=$(incus network list --format csv -c n 2>/dev/null || true)
if [[ -n "$networks" ]]; then
while IFS= read -r nname; do
[[ -z "$nname" ]] && continue
info "删除网络: ${nname}"
incus network delete "$nname" 2>/dev/null || true
done <<< "$networks"
fi
# 删除所有存储池
local pools
pools=$(incus storage list --format csv -c n 2>/dev/null || true)
if [[ -n "$pools" ]]; then
while IFS= read -r pool; do
[[ -z "$pool" ]] && continue
info "删除存储池: ${pool}"
# 先删除池中的存储卷
local volumes
volumes=$(incus storage volume list "$pool" --format csv -c n 2>/dev/null || true)
if [[ -n "$volumes" ]]; then
while IFS= read -r vol; do
[[ -z "$vol" ]] && continue
incus storage volume delete "$pool" "$vol" 2>/dev/null || true
done <<< "$volumes"
fi
incus storage delete "$pool" 2>/dev/null || true
done <<< "$pools"
fi
log "网络和存储池已清理"
fi
# ---- 步骤 4: 停止 Incus 服务 ----
step "步骤 [4/8] 停止 Incus 服务..."
systemctl stop incus.service 2>/dev/null || true
systemctl stop incus.socket 2>/dev/null || true
systemctl stop incus-user.service 2>/dev/null || true
systemctl stop incus-user.socket 2>/dev/null || true
systemctl stop incus-startup.service 2>/dev/null || true
systemctl disable incus.service 2>/dev/null || true
systemctl disable incus.socket 2>/dev/null || true
systemctl disable incus-user.service 2>/dev/null || true
systemctl disable incus-user.socket 2>/dev/null || true
systemctl disable incus-startup.service 2>/dev/null || true
log "Incus 服务已停止"
# ---- 步骤 5: 卸载软件包 ----
step "步骤 [5/8] 卸载软件包..."
export DEBIAN_FRONTEND=noninteractive
# 1. 强制停止服务和进程防卡死
systemctl stop incus incus.socket incus-lxcfs incus-startup 2>/dev/null || true
systemctl disable incus incus.socket incus-lxcfs incus-startup 2>/dev/null || true
pkill -9 -f "incus" 2>/dev/null || true
# 2. 直接强制尝试卸载所有相关的包(使用正则覆盖包名)
apt-get purge -y -qq "^incus.*" lxcfs 2>/dev/null || true
# 3. 兜底彻底斩断二进制文件(防止包管理器 Broken 状态导致系统内指令幽灵残留)
rm -rf /opt/incus 2>/dev/null || true
rm -f /usr/bin/incus* /usr/sbin/incus* /usr/local/bin/incus* 2>/dev/null || true
log "Incus 相关软件包及其幽灵残留已强制清除"
# 清理不再需要的依赖
apt-get autoremove -y -qq 2>/dev/null || true
log "依赖清理完成"
# ---- 步骤 6: 清理 APT 源和密钥 ----
step "步骤 [6/8] 清理 APT 源和密钥..."
local cleaned=false
if [[ -f /etc/apt/sources.list.d/zabbly-incus-stable.sources ]]; then
rm -f /etc/apt/sources.list.d/zabbly-incus-stable.sources
info "已删除: zabbly-incus-stable.sources"
cleaned=true
fi
if [[ -f /etc/apt/keyrings/zabbly.gpg ]]; then
rm -f /etc/apt/keyrings/zabbly.gpg
info "已删除: zabbly.gpg"
cleaned=true
fi
if [[ "$cleaned" == "true" ]]; then
apt-get update -qq 2>/dev/null || true
log "APT 源和密钥已清理"
else
info "APT 源和密钥不存在,跳过"
fi
# ---- 步骤 7: 清理外挂服务(RFW / WARP / 守护进程) ----
step "步骤 [7/8] 清理外挂防护与网络代理服务..."
# 1. 清理 RFW
if [[ -f "${RFW_INSTALL_DIR}/rfw" ]] || \
systemctl list-unit-files 2>/dev/null | grep -q "rfw.service"; then
do_rfw_cleanup
else
info "RFW 未安装,跳过"
fi
# 2. 清理 WARP 和 IPv6 路由守护神
if ip link show wg0 >/dev/null 2>&1 || [[ -f /usr/local/bin/wgcf ]] || [[ -d /etc/wireguard ]]; then
systemctl stop wg-quick@wg0 2>/dev/null || true
systemctl disable wg-quick@wg0 2>/dev/null || true
rm -rf /etc/wireguard/ 2>/dev/null || true
rm -f /usr/local/bin/wgcf 2>/dev/null || true
rm -f /usr/local/bin/incus-warp 2>/dev/null || true
systemctl stop ipv6-route-guard 2>/dev/null || true
systemctl disable ipv6-route-guard 2>/dev/null || true
rm -f /etc/systemd/system/ipv6-route-guard.service 2>/dev/null || true
rm -f /usr/local/bin/ipv6-route-guard.sh 2>/dev/null || true
systemctl daemon-reload
info "已清理: Cloudflare WARP 出海通道及路由守护服务"
else
info "WARP 网络组件未安装,跳过"
fi
# 3. 清除 IPv6 同步守护神服务
if systemctl list-unit-files 2>/dev/null | grep -q "incus-v6-guardian.service"; then
systemctl stop incus-v6-guardian 2>/dev/null || true
systemctl disable incus-v6-guardian 2>/dev/null || true
rm -f /etc/systemd/system/incus-v6-guardian.service
rm -f /usr/local/bin/incus-v6-guardian.sh
systemctl daemon-reload
info "已清理: IPv6 双栈同步守护神 (Guardian Daemon)"
fi
# ---- 步骤 8: 清理配置文件和数据目录 ----
step "步骤 [8/8] 清理配置和数据文件..."
# 内核参数配置
if [[ -f /etc/sysctl.d/99-incus.conf ]]; then
rm -f /etc/sysctl.d/99-incus.conf
info "已删除: /etc/sysctl.d/99-incus.conf"
fi
if [[ -f /etc/modules-load.d/br_netfilter.conf ]]; then
rm -f /etc/modules-load.d/br_netfilter.conf
info "已删除: /etc/modules-load.d/br_netfilter.conf"
fi
# 重新加载 sysctl(移除自定义参数)
sysctl --system >/dev/null 2>&1 || true
# Incus 数据目录
if [[ -d /var/lib/incus ]]; then
rm -rf /var/lib/incus
info "已删除: /var/lib/incus/"
fi
# Incus 日志目录
if [[ -d /var/log/incus ]]; then
rm -rf /var/log/incus
info "已删除: /var/log/incus/"
fi
# Incus 运行时目录
if [[ -d /run/incus ]]; then
rm -rf /run/incus
info "已删除: /run/incus/"
fi
# Incus 用户配置目录
if [[ -d /root/.config/incus ]]; then
rm -rf /root/.config/incus
info "已删除: /root/.config/incus/"
fi
# 用户子 UID/GID 映射(Incus 可能添加的条目)
if grep -q "incus" /etc/subuid 2>/dev/null; then
sed -i '/incus/d' /etc/subuid 2>/dev/null || true
info "已清理: /etc/subuid 中的 incus 条目"
fi
if grep -q "incus" /etc/subgid 2>/dev/null; then
sed -i '/incus/d' /etc/subgid 2>/dev/null || true
info "已清理: /etc/subgid 中的 incus 条目"
fi
# 清除安装脚本、日志文件、下载的包缓存等
rm -f /root/log.txt /root/zfs-modules-*.tar.gz 2>/dev/null || true
local script_path
script_path=$(realpath "$0" 2>/dev/null || echo "$0")
if [[ -f "$script_path" && ! "$script_path" =~ (bash|sh)$ ]]; then
rm -f "$script_path"
info "已清理安装脚本自身: $script_path"
fi
log "配置和数据文件清理完成"
# ---- 卸载完成 ----
echo ""
echo -e "${GREEN} ╔══════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN} ║ ║${NC}"
echo -e "${GREEN} ║ ✓ 卸载完成 ║${NC}"
echo -e "${GREEN} ║ ║${NC}"
echo -e "${GREEN} ╚══════════════════════════════════════════════════╝${NC}"
echo ""
echo -e " 已清理的内容:"
echo -e " ${GREEN}${NC} 所有 LXC 容器和镜像"
echo -e " ${GREEN}${NC} 网桥和存储池"
echo -e " ${GREEN}${NC} Incus 服务和软件包"
echo -e " ${GREEN}${NC} APT 源和 GPG 密钥"
echo -e " ${GREEN}${NC} RFW 防火墙"
echo -e " ${GREEN}${NC} 内核参数配置"
echo -e " ${GREEN}${NC} 数据和日志目录"
echo -e " ${GREEN}${NC} 安装脚本自身及缓存"
echo ""
divider
echo -e " ${DIM}系统已还原。如需重新安装,请再次运行此脚本。${NC}"
divider
echo ""
# 删除自身脚本及任何下载的面板脚本残留
rm -f "$0" 2>/dev/null || true
rm -f incudal-install.sh *.install.sh install.sh incudal.sh 2>/dev/null || true
rm -f /root/install.sh /root/incudal.sh 2>/dev/null || true
}
# -----------------------------------------------------------------------------
# IPv6 Guardian Daemon (双栈端口同步补丁)
# -----------------------------------------------------------------------------
setup_v6_guardian() {
log "正在部署 IPv6 双栈端口同步守护进程 (Guardian Daemon)..."
local guardian_script="/usr/local/bin/incus-v6-guardian.sh"
local guardian_service="/etc/systemd/system/incus-v6-guardian.service"
cat > "$guardian_script" << 'EOF'
#!/bin/bash
# Incudal - IPv6 Dual-Stack Guardian Daemon
# 每 15 秒轮询,将新建的单栈 IPv4 端口映射同步生成一份 IPv6 双栈跨协议互通映射。
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
while true; do
# 防止云服务商 NetworkManager 因网络波动重置内核参数,导致 proxy_ndp 失效
if [[ -f /etc/sysctl.d/99-incus.conf ]]; then
sysctl -p /etc/sysctl.d/99-incus.conf >/dev/null 2>&1 || true
fi
for c in $(incus list -c n,s --format=csv 2>/dev/null | awk -F, '$2=="RUNNING" {print $1}'); do
devices=$(incus config device show "$c" 2>/dev/null) || continue
ip_v4=$(incus list "$c" -c 4 --format=csv 2>/dev/null | grep -E -o '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || echo "127.0.0.1")
# TCP 新增同步
v4_tcp_proxies=$(echo "$devices" | awk -F: '/^proxy-tcp-[0-9]+/ {print $1}' | tr -d ' ')
for proxy in $v4_tcp_proxies; do
port=$(echo "$proxy" | awk -F'-' '{print $3}')
if ! echo "$devices" | grep -q "^proxy-v6-tcp-${port}:$"; then
original_connect=$(incus config device get "$c" "$proxy" connect 2>/dev/null)
target_port=$(echo "$original_connect" | awk -F: '{print $NF}')
[[ -z "$target_port" ]] && target_port="$port"
incus config device add "$c" proxy-v6-tcp-${port} proxy listen=tcp:[::]:${port} connect=tcp:${ip_v4}:${target_port} >/dev/null 2>&1
fi
done
# UDP 新增同步
v4_udp_proxies=$(echo "$devices" | awk -F: '/^proxy-udp-[0-9]+/ {print $1}' | tr -d ' ')
for proxy in $v4_udp_proxies; do
port=$(echo "$proxy" | awk -F'-' '{print $3}')
if ! echo "$devices" | grep -q "^proxy-v6-udp-${port}:$"; then
original_connect=$(incus config device get "$c" "$proxy" connect 2>/dev/null)
target_port=$(echo "$original_connect" | awk -F: '{print $NF}')
[[ -z "$target_port" ]] && target_port="$port"
incus config device add "$c" proxy-v6-udp-${port} proxy listen=udp:[::]:${port} connect=udp:${ip_v4}:${target_port} >/dev/null 2>&1
fi
done
# TCP 孤儿清理
v6_tcp_proxies=$(echo "$devices" | awk -F: '/^proxy-v6-tcp-[0-9]+/ {print $1}' | tr -d ' ')
for proxy in $v6_tcp_proxies; do
port=$(echo "$proxy" | awk -F'-' '{print $4}')
if ! echo "$devices" | grep -q "^proxy-tcp-${port}:$"; then
incus config device remove "$c" proxy-v6-tcp-${port} >/dev/null 2>&1
fi
done
# UDP 孤儿清理
v6_udp_proxies=$(echo "$devices" | awk -F: '/^proxy-v6-udp-[0-9]+/ {print $1}' | tr -d ' ')
for proxy in $v6_udp_proxies; do
port=$(echo "$proxy" | awk -F'-' '{print $4}')
if ! echo "$devices" | grep -q "^proxy-udp-${port}:$"; then
incus config device remove "$c" proxy-v6-udp-${port} >/dev/null 2>&1
fi
done
done
sleep 15
done
EOF
chmod +x "$guardian_script"
cat > "$guardian_service" << EOF
[Unit]
Description=Incudal IPv6 Dual-Stack Guardian Daemon
After=network.target
[Service]
Type=simple
ExecStart=$guardian_script
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload 2>/dev/null || true
systemctl enable incus-v6-guardian 2>/dev/null || true
systemctl restart incus-v6-guardian 2>/dev/null || true
info "守护神服务已启动:每隔 15 秒自动打通新增容器的反向 IPv6 映射"
}
# ========================== 主流程入口 ==========================
main() {
# 显示横幅
show_banner
# Root 权限检查
if [[ "$EUID" -ne 0 ]]; then
error "请以 root 权限运行此脚本"
echo -e " ${DIM}用法: sudo bash $0${NC}"
exit 1
fi
# 检测系统环境
detect_system
# ---- 解析命令行参数(兼容非交互模式)----
local ACTION="install" # 默认动作为安装
while [[ $# -gt 0 ]]; do
case "$1" in
--mode)
MODE="$2"; shift 2 ;;
--token)
TOKEN="$2"; shift 2 ;;
--ipv6-subnet)
IPV6_SUBNET="$2"; shift 2 ;;
--ipv6-iface)
IPV6_IFACE="$2"; shift 2 ;;
--port|-p)
LISTEN_PORT="$2"; shift 2 ;;
--uninstall)
ACTION="uninstall"; shift ;;
--agent|--agent-menu)
ACTION="agent"; shift ;;
--help|-h)
echo "用法: $0 [选项]"
echo ""
echo "选项:"
echo " --mode <nat|nat_ipv6> 网络模式(不指定则交互选择)"
echo " --token <TOKEN> 面板认证 Token"
echo " --ipv6-subnet <CIDR> IPv6 子网段(例如 2001:db8::/64"
echo " --ipv6-iface <IFACE> IPv6 路由父网卡(例如 eth0"
echo " --port <PORT> 自定义 Incus 运行端口 (默认 8443)"
echo " --uninstall 卸载 Incus 节点并还原系统"
echo " --agent 打开 Agent 安装 / 更新菜单"
echo " --help, -h 显示帮助信息"
echo ""
echo "交互模式: sudo bash $0"
echo "命令行: sudo bash $0 --mode nat --token <YOUR_TOKEN> --port 10001"
echo "卸载: sudo bash $0 --uninstall"
exit 0
;;
*)
error "未知参数: $1"
echo -e " ${DIM}使用 --help 查看帮助${NC}"
exit 1
;;
esac
done
# 如果通过命令行指定了卸载,直接执行
if [[ "$ACTION" == "uninstall" ]]; then
do_uninstall
exit 0
fi
if [[ "$ACTION" == "agent" ]]; then
manage_incudal_agent
exit 0
fi
# ---- 交互式流程 ----
# 如果 stdin 不是终端且缺少参数,给出提示
if [[ ! -t 0 ]]; then
if [[ -z "$MODE" || -z "$TOKEN" ]]; then
error "通过管道执行时,必须提供 --mode 和 --token 参数"
echo -e " ${DIM}示例: curl -sL <URL> | sudo bash -s -- --mode nat --token <TOKEN>${NC}"
exit 1
fi
fi
# 合并注入变量与 CLI 参数
TOKEN="${INJECT_TOKEN:-${TOKEN:-}}"
MODE="${INJECT_MODE:-${MODE:-}}"
IPV6_SUBNET="${INJECT_IPV6_SUBNET:-${IPV6_SUBNET:-}}"
IPV6_IFACE="${INJECT_IPV6_IFACE:-${IPV6_IFACE:-}}"
# 模式选择(未通过 CLI 或面板注入指定时进入菜单)
if [[ -z "$MODE" ]]; then
show_system_info
while true; do
show_menu
read -r choice
echo ""
case "$choice" in
1) MODE="nat"; break ;;
2)
MODE="nat_ipv6"
echo -e "\n${CYAN}正在检测宿主机 IPv6 网络配置...${NC}"
local detect_iface=""
local detect_ip=""
detect_iface=$(ip -6 route show default 2>/dev/null | awk '/dev/ {for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -n1 || true)
if [[ -z "$detect_iface" ]]; then
detect_iface=$(ip route show default 2>/dev/null | awk '/dev/ {for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -n1 || true)
fi
if [[ -n "$detect_iface" ]]; then
detect_ip=$(ip -6 addr show dev "$detect_iface" scope global 2>/dev/null | awk '/inet6/ {print $2}' | grep -vEi "^(fd|fe80)" | head -n1 || true)
fi
echo ""
echo -e " ${CYAN}==> IPv6 实例附加子网配置${NC}"
if [[ -n "$detect_ip" ]]; then
echo -e " 检测到当前服务器分配的 IPv6 全段为: ${GREEN}${detect_ip}${NC},网卡: ${GREEN}${detect_iface}${NC}。"
else
echo -e " ${YELLOW}未检测到公网 IPv6 地址${NC}"
fi
echo -e " ${DIM}若要开启真实 IPv6 入站/出站路由分配,请直接回车采用默认值即可,或手动输入。${NC}"
echo -e " ${DIM}格式须带有网络前缀长度(例如 2a01:4f9.../64),若不需要额外分发 IPv6 请留空跳过。${NC}"
echo -ne " ${BOLD}请输入分配给虚拟机的 IPv6 子网 [回车默认: ${detect_ip:-留空}]: ${NC}"
read -r IPV6_SUBNET
if [[ -z "$IPV6_SUBNET" ]]; then
IPV6_SUBNET="$detect_ip"
fi
if [[ -n "$IPV6_SUBNET" ]]; then
echo -ne " ${BOLD}请输入此 IPv6 使用的物理网卡名 [回车默认: ${detect_iface:-}]: ${NC}"
read -r IPV6_IFACE
if [[ -z "$IPV6_IFACE" ]]; then
IPV6_IFACE="$detect_iface"
fi
fi
break
;;
3) install_rfw; continue ;;
4) show_system_info; continue ;;
5) uninstall_rfw; continue ;;
6) do_uninstall; exit 0 ;;
7) show_network_mode_help; continue ;;
8) manage_incudal_agent; continue ;;
0) info "再见!"; exit 0 ;;
*) warn "无效选项,请重新选择"; continue ;;
esac
done
fi
# 模式参数合法性验证
if [[ "$MODE" != "nat" && "$MODE" != "nat_ipv6" ]]; then
error "--mode 必须为 'nat' 或 'nat_ipv6'"
exit 1
fi
# Token 输入(未通过 CLI 指定或面板注入时交互输入)
if [[ -z "$TOKEN" ]]; then
read_token
else
local token_masked="${TOKEN:0:8}${TOKEN: -4}"
info "Token 已自动配置: ${token_masked}"
fi
# 端口修改逻辑
if [[ -z "${LISTEN_PORT:-}" ]]; then
echo -e "\n${CYAN}==> (可选) 自定义通信端口${NC}"
echo -e " ${DIM}默认 8443,若被防火墙屏蔽可改为 10000+ 端口${NC}"
echo -ne " ${BOLD}通信端口 [默认 8443]: ${NC}"
read -r USER_PORT
if [[ -n "$USER_PORT" && "$USER_PORT" =~ ^[0-9]+$ ]]; then
LISTEN_PORT="$USER_PORT"
else
LISTEN_PORT="8443"
fi
fi
configure_agent_heartbeat_interval
# WARP 询问逻辑(纯 IPv6 专供)
if [[ "$IS_PURE_IPV6" == "true" ]]; then
if [[ -t 0 ]]; then
echo -e "\n${CYAN}==> 检测到系统为【纯 IPv6 出站】环境${NC}"
echo -e " 基于预设,脚本将默认请求 Cloudflare WARP 提供 IPv4 虚拟接驳支持。"
echo -e " 若您的业务需求须使用纯净原生 IPv6,可选择跳过。"
echo -ne " ${BOLD}是否为目前宿主机接驳安装 WARP IPv4 出站通道?[Y/n]: ${NC}"
read -r WARP_CHOICE
if [[ "${WARP_CHOICE:-Y}" =~ ^[nN]$ ]]; then
SKIP_WARP="true"
else
SKIP_WARP="false"
fi
else
SKIP_WARP="false"
fi
else
SKIP_WARP="false"
fi
# 安装前确认
confirm_install
# ---- 执行安装 ----
echo ""
divider
echo -e " ${BOLD}开始安装...${NC}"
divider
if [[ "$IS_PURE_IPV6" == "true" ]]; then
setup_temp_network # 接入临时出海通道
if [[ "${SKIP_WARP:-false}" != "true" ]]; then
setup_warp_v4 # 搭建底层双规双栈通道
else
info "用户已选择跳过 WARP 安装,保留原生纯 IPv6 环境出站"
fi
fi
setup_kernel # 1/5 内核参数
# 2/5 系统依赖(用户可能拒绝 DKMS 编译并返回主菜单)
if ! install_deps; then
warn "安装已中断,返回主菜单..."
echo ""
exec "$0" # 重新启动脚本回到主菜单
exit 0
fi
install_incus # 3/5 安装 Incus
init_incus # 4/5 初始化 Incus
import_cert # 5/5 导入证书
# 仅当启用了 IPv6 相关功能时,挂载 IPv6 双栈同步守护神
if [[ "$MODE" == "nat_ipv6" ]]; then
setup_v6_guardian
fi
install_incudal_agent
show_result
}
main "$@"