init xboard node

This commit is contained in:
xboard
2026-03-15 09:27:24 +08:00
commit 37e6c016cb
40 changed files with 11011 additions and 0 deletions
+735
View File
@@ -0,0 +1,735 @@
#!/bin/bash
set -e
# xboard-node Multi-Node Deploy Script
# Supports: Ubuntu 20+, Debian 11+, CentOS 8+, Alpine 3.18+
#
# One-command deploy (non-interactive):
# bash install.sh -a https://panel.example.com -t YOUR_TOKEN -n 1
# bash install.sh -a https://panel.example.com -t YOUR_TOKEN -n 2 -k xray
# bash install.sh -a https://panel.example.com -t YOUR_TOKEN -n 3 --docker
#
# Interactive:
# bash install.sh
#
# Management:
# bash install.sh list
# bash install.sh remove <node_id>
# bash install.sh update
# bash install.sh uninstall
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
INSTALL_DIR="/usr/local/bin"
CONFIG_DIR="/etc/xboard-node"
SERVICE_TEMPLATE="xboard-node@.service"
DOCKER_COMPOSE_FILE="${CONFIG_DIR}/docker-compose.yml"
DOCKER_IMAGE="ghcr.io/cedar2025/xboard-node:latest"
# Parsed parameters (populated by parse_args)
PANEL_URL=""
PANEL_TOKEN=""
NODE_ID=""
NODE_TYPE=""
KERNEL_TYPE="singbox"
DOCKER_MODE=0
SUBCOMMAND=""
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
log_step() { echo -e "${CYAN}[STEP]${NC} ${BOLD}$1${NC}"; }
# ─── Argument Parsing ─────────────────────────────────────────────────
parse_args() {
while [ $# -gt 0 ]; do
case "$1" in
-a|--api) PANEL_URL="$2"; shift 2 ;;
-t|--token) PANEL_TOKEN="$2"; shift 2 ;;
-n|--node-id) NODE_ID="$2"; shift 2 ;;
-T|--node-type) NODE_TYPE="$2"; shift 2 ;;
-k|--kernel) KERNEL_TYPE="$2"; shift 2 ;;
--docker) DOCKER_MODE=1; shift ;;
add|remove|list|update|uninstall|help|--help|-h)
if [ -z "$SUBCOMMAND" ]; then
SUBCOMMAND="$1"
fi
shift
;;
*)
if [ "$SUBCOMMAND" = "remove" ] && [ -z "$NODE_ID" ]; then
NODE_ID="$1"
fi
shift
;;
esac
done
# Normalize kernel type
case "$KERNEL_TYPE" in
xray|Xray|XRAY) KERNEL_TYPE="xray" ;;
*) KERNEL_TYPE="singbox" ;;
esac
}
has_all_params() {
[ -n "$PANEL_URL" ] && [ -n "$PANEL_TOKEN" ] && [ -n "$NODE_ID" ]
}
validate_params() {
if [ -z "$PANEL_URL" ]; then
log_error "Panel URL is required (-a/--api)"
exit 1
fi
if [ -z "$PANEL_TOKEN" ]; then
log_error "Server Token is required (-t/--token)"
exit 1
fi
if [ -z "$NODE_ID" ]; then
log_error "Node ID is required (-n/--node-id)"
exit 1
fi
if ! [[ "$NODE_ID" =~ ^[0-9]+$ ]]; then
log_error "Node ID must be a positive integer, got: $NODE_ID"
exit 1
fi
}
# ─── System Detection ────────────────────────────────────────────────
check_root() {
if [ "$(id -u)" != "0" ]; then
log_error "Please run as root (or with sudo)"
exit 1
fi
}
detect_arch() {
ARCH=$(uname -m)
case "$ARCH" in
x86_64|amd64) ARCH="amd64" ;;
aarch64|arm64) ARCH="arm64" ;;
armv7l) ARCH="armv7" ;;
*)
log_error "Unsupported architecture: $ARCH"
exit 1
;;
esac
}
detect_os() {
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$ID
elif [ -f /etc/alpine-release ]; then
OS="alpine"
else
OS="unknown"
fi
}
install_deps() {
case "$OS" in
ubuntu|debian)
apt-get update -qq
apt-get install -y -qq wget curl tar >/dev/null 2>&1
;;
centos|rhel|rocky|almalinux|fedora)
yum install -y -q wget curl tar >/dev/null 2>&1
;;
alpine)
apk add --no-cache wget curl tar >/dev/null 2>&1
;;
esac
}
# ─── Binary Install ──────────────────────────────────────────────────
is_binary_installed() {
[ -x "${INSTALL_DIR}/xboard-node" ]
}
install_binary() {
if is_binary_installed; then
log_info "xboard-node binary already installed"
return
fi
log_step "Installing xboard-node binary..."
local src=""
if [ -f "./xboard-node" ]; then
src="./xboard-node"
elif [ -f "./xboard-node-linux-${ARCH}" ]; then
src="./xboard-node-linux-${ARCH}"
fi
if [ -n "$src" ]; then
cp "$src" "${INSTALL_DIR}/xboard-node"
log_info "Installed from local file: $src"
else
local url="https://github.com/cedar2025/xboard-node/releases/latest/download/xboard-node-linux-${ARCH}"
log_info "Downloading from GitHub releases..."
if wget -q "$url" -O "${INSTALL_DIR}/xboard-node" 2>/dev/null; then
log_info "Downloaded successfully"
elif curl -fsSL "$url" -o "${INSTALL_DIR}/xboard-node" 2>/dev/null; then
log_info "Downloaded successfully"
else
log_error "Failed to download. Place xboard-node binary in current directory and retry."
exit 1
fi
fi
chmod +x "${INSTALL_DIR}/xboard-node"
log_info "xboard-node installed to ${INSTALL_DIR}/xboard-node"
}
# ─── Systemd Template ────────────────────────────────────────────────
install_systemd_template() {
if ! command -v systemctl >/dev/null 2>&1; then
return
fi
if [ -f "/etc/systemd/system/${SERVICE_TEMPLATE}" ]; then
return
fi
log_step "Installing systemd service template..."
cat > "/etc/systemd/system/${SERVICE_TEMPLATE}" << 'UNIT'
[Unit]
Description=Xboard Node Backend (node %i)
Documentation=https://github.com/cedar2025/xboard-node
After=network.target nss-lookup.target
[Service]
Type=simple
ExecStart=/usr/local/bin/xboard-node -c /etc/xboard-node/%i/config.yml
Restart=always
RestartSec=5
LimitNOFILE=1048576
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
log_info "Systemd template installed: ${SERVICE_TEMPLATE}"
}
# ─── Migrate Legacy Config ───────────────────────────────────────────
migrate_legacy_config() {
if [ -f "${CONFIG_DIR}/config.yml" ] && [ ! -d "${CONFIG_DIR}/config.yml" ]; then
local legacy_id
legacy_id=$(grep -E '^\s*node_id:\s*' "${CONFIG_DIR}/config.yml" 2>/dev/null | head -1 | sed 's/[^0-9]*//g')
if [ -z "$legacy_id" ]; then
legacy_id="default"
fi
if [ ! -d "${CONFIG_DIR}/${legacy_id}" ]; then
log_warn "Migrating legacy config to multi-node layout: node ${legacy_id}"
mkdir -p "${CONFIG_DIR}/${legacy_id}"
mv "${CONFIG_DIR}/config.yml" "${CONFIG_DIR}/${legacy_id}/config.yml"
if command -v systemctl >/dev/null 2>&1; then
systemctl stop xboard-node 2>/dev/null || true
systemctl disable xboard-node 2>/dev/null || true
rm -f /etc/systemd/system/xboard-node.service
install_systemd_template
systemctl enable "xboard-node@${legacy_id}" 2>/dev/null || true
systemctl start "xboard-node@${legacy_id}" 2>/dev/null || true
log_info "Migrated service: xboard-node → xboard-node@${legacy_id}"
fi
fi
fi
}
# ─── Interactive Prompts ──────────────────────────────────────────────
prompt_missing_params() {
echo ""
log_step "=== Node Configuration ==="
echo ""
if [ -z "$PANEL_URL" ]; then
read -rp " Panel URL (e.g. https://panel.example.com): " PANEL_URL
else
echo -e " Panel URL: ${CYAN}${PANEL_URL}${NC}"
fi
if [ -z "$PANEL_TOKEN" ]; then
read -rp " Server Token: " PANEL_TOKEN
else
echo -e " Server Token: ${CYAN}${PANEL_TOKEN:0:8}***${NC}"
fi
if [ -z "$NODE_ID" ]; then
read -rp " Node ID: " NODE_ID
else
echo -e " Node ID: ${CYAN}${NODE_ID}${NC}"
fi
if [ -n "$NODE_TYPE" ]; then
echo -e " Node Type: ${CYAN}${NODE_TYPE}${NC}"
fi
if [ -n "$KERNEL_TYPE" ] && [ "$KERNEL_TYPE" != "singbox" ]; then
echo -e " Kernel: ${CYAN}${KERNEL_TYPE}${NC}"
else
echo ""
echo " Kernel type:"
echo " 1) singbox (default, recommended)"
echo " 2) xray"
read -rp " Choose [1/2]: " KERNEL_CHOICE
case "$KERNEL_CHOICE" in
2) KERNEL_TYPE="xray" ;;
*) KERNEL_TYPE="singbox" ;;
esac
fi
echo ""
validate_params
}
# ─── Node Operations ─────────────────────────────────────────────────
write_node_config() {
local node_id="$1"
local node_dir="${CONFIG_DIR}/${node_id}"
mkdir -p "$node_dir"
local node_type_line=""
if [ -n "$NODE_TYPE" ]; then
node_type_line=" node_type: \"${NODE_TYPE}\""
fi
cat > "${node_dir}/config.yml" << EOF
panel:
url: "${PANEL_URL}"
token: "${PANEL_TOKEN}"
node_id: ${node_id}
${node_type_line}
node:
push_interval: 0
pull_interval: 0
kernel:
type: "${KERNEL_TYPE}"
config_dir: "${node_dir}"
log_level: "warn"
log:
level: "info"
output: "stdout"
EOF
log_info "Config written: ${node_dir}/config.yml"
}
add_node_native() {
local node_id="$1"
if [ -d "${CONFIG_DIR}/${node_id}" ]; then
log_error "Node ${node_id} already exists at ${CONFIG_DIR}/${node_id}/"
log_info "To reconfigure, first remove it: $0 remove ${node_id}"
exit 1
fi
write_node_config "$node_id"
if command -v systemctl >/dev/null 2>&1; then
install_systemd_template
systemctl enable "xboard-node@${node_id}"
systemctl start "xboard-node@${node_id}"
log_info "Service started: xboard-node@${node_id}"
else
log_warn "No systemd found. Start manually:"
echo " xboard-node -c ${CONFIG_DIR}/${node_id}/config.yml"
fi
}
add_node_docker() {
local node_id="$1"
if [ -d "${CONFIG_DIR}/${node_id}" ]; then
log_error "Node ${node_id} already exists at ${CONFIG_DIR}/${node_id}/"
log_info "To reconfigure, first remove it: $0 remove ${node_id}"
exit 1
fi
write_node_config "$node_id"
regenerate_docker_compose
log_info "Docker compose updated: ${DOCKER_COMPOSE_FILE}"
if command -v docker >/dev/null 2>&1; then
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD="docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD="docker-compose"
else
log_warn "docker compose not found. Start manually:"
echo " cd ${CONFIG_DIR} && docker compose up -d"
return
fi
cd "${CONFIG_DIR}"
${COMPOSE_CMD} up -d "node-${node_id}"
log_info "Container started: xboard-node-${node_id}"
else
log_warn "Docker not installed. Install Docker first, then run:"
echo " cd ${CONFIG_DIR} && docker compose up -d"
fi
}
regenerate_docker_compose() {
local nodes=()
for dir in "${CONFIG_DIR}"/*/; do
[ -f "${dir}config.yml" ] || continue
local nid
nid=$(basename "$dir")
nodes+=("$nid")
done
if [ ${#nodes[@]} -eq 0 ]; then
rm -f "${DOCKER_COMPOSE_FILE}"
return
fi
cat > "${DOCKER_COMPOSE_FILE}" << 'HEADER'
# Auto-generated by install.sh — do not edit manually.
# Regenerated each time a node is added or removed.
services:
HEADER
for nid in "${nodes[@]}"; do
cat >> "${DOCKER_COMPOSE_FILE}" << EOF
node-${nid}:
image: ${DOCKER_IMAGE}
container_name: xboard-node-${nid}
restart: always
network_mode: host
volumes:
- ./${nid}/config.yml:/etc/xboard-node/config.yml:ro
- ./${nid}:/etc/xboard-node/data
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
EOF
done
}
# ─── Deploy Node (unified entry) ─────────────────────────────────────
deploy_node() {
if has_all_params; then
validate_params
log_info "Deploying node ${NODE_ID} (${KERNEL_TYPE}) → ${PANEL_URL}"
else
prompt_missing_params
fi
if [ "$DOCKER_MODE" -eq 1 ]; then
add_node_docker "$NODE_ID"
else
add_node_native "$NODE_ID"
fi
echo ""
echo -e "${GREEN}=== Node ${NODE_ID} Deployed ===${NC}"
echo ""
if [ "$DOCKER_MODE" -eq 1 ]; then
echo " Manage:"
echo " Logs: docker logs -f xboard-node-${NODE_ID}"
echo " Stop: cd ${CONFIG_DIR} && docker compose stop node-${NODE_ID}"
echo " Restart: cd ${CONFIG_DIR} && docker compose restart node-${NODE_ID}"
else
echo " Manage:"
echo " Status: systemctl status xboard-node@${NODE_ID}"
echo " Logs: journalctl -u xboard-node@${NODE_ID} -f"
echo " Stop: systemctl stop xboard-node@${NODE_ID}"
echo " Restart: systemctl restart xboard-node@${NODE_ID}"
fi
echo ""
echo " Config: ${CONFIG_DIR}/${NODE_ID}/config.yml"
echo ""
}
# ─── Remove Node ──────────────────────────────────────────────────────
remove_node() {
local node_id="$1"
if [ -z "$node_id" ]; then
log_error "Usage: $0 remove <node_id>"
exit 1
fi
if [ ! -d "${CONFIG_DIR}/${node_id}" ]; then
log_error "Node ${node_id} not found"
exit 1
fi
log_step "Removing node ${node_id}..."
if command -v systemctl >/dev/null 2>&1; then
systemctl stop "xboard-node@${node_id}" 2>/dev/null || true
systemctl disable "xboard-node@${node_id}" 2>/dev/null || true
log_info "Systemd service stopped and disabled"
fi
if command -v docker >/dev/null 2>&1; then
docker rm -f "xboard-node-${node_id}" 2>/dev/null || true
fi
rm -rf "${CONFIG_DIR}/${node_id}"
log_info "Config removed: ${CONFIG_DIR}/${node_id}/"
regenerate_docker_compose
log_info "Node ${node_id} removed"
}
# ─── List Nodes ───────────────────────────────────────────────────────
list_nodes() {
echo ""
echo -e "${BOLD} Deployed Nodes${NC}"
echo -e " ────────────────────────────────────────────"
local found=0
for dir in "${CONFIG_DIR}"/*/; do
[ -f "${dir}config.yml" ] || continue
found=1
local nid
nid=$(basename "$dir")
local panel_url kernel_type
panel_url=$(grep -E '^\s*url:' "${dir}config.yml" 2>/dev/null | head -1 | sed 's/.*url:\s*"\?\([^"]*\)"\?.*/\1/')
kernel_type=$(grep -E '^\s*type:' "${dir}config.yml" 2>/dev/null | head -1 | sed 's/.*type:\s*"\?\([^"]*\)"\?.*/\1/')
local status="${RED}stopped${NC}"
if command -v systemctl >/dev/null 2>&1; then
if systemctl is-active "xboard-node@${nid}" >/dev/null 2>&1; then
status="${GREEN}running (systemd)${NC}"
fi
fi
if command -v docker >/dev/null 2>&1; then
if docker inspect -f '{{.State.Running}}' "xboard-node-${nid}" 2>/dev/null | grep -q true; then
status="${GREEN}running (docker)${NC}"
fi
fi
printf " ${BOLD}Node %-6s${NC} kernel=%-8s panel=%s\n" "$nid" "${kernel_type:-singbox}" "${panel_url:-unknown}"
echo -e " status=${status}"
echo ""
done
if [ "$found" -eq 0 ]; then
echo " No nodes deployed yet."
echo ""
echo " Deploy your first node:"
echo " $0 -a https://panel.example.com -t TOKEN -n 1"
echo " $0 -a https://panel.example.com -t TOKEN -n 1 --docker"
fi
echo ""
}
# ─── Update / Uninstall ──────────────────────────────────────────────
update_binary() {
log_step "Updating xboard-node binary..."
detect_arch
local url="https://github.com/cedar2025/xboard-node/releases/latest/download/xboard-node-linux-${ARCH}"
local tmp="/tmp/xboard-node-update"
if wget -q "$url" -O "$tmp" 2>/dev/null || curl -fsSL "$url" -o "$tmp" 2>/dev/null; then
chmod +x "$tmp"
mv "$tmp" "${INSTALL_DIR}/xboard-node"
log_info "Binary updated"
else
log_error "Failed to download update"
rm -f "$tmp"
exit 1
fi
if command -v systemctl >/dev/null 2>&1; then
for dir in "${CONFIG_DIR}"/*/; do
[ -f "${dir}config.yml" ] || continue
local nid
nid=$(basename "$dir")
if systemctl is-active "xboard-node@${nid}" >/dev/null 2>&1; then
systemctl restart "xboard-node@${nid}"
log_info "Restarted: xboard-node@${nid}"
fi
done
fi
if [ -f "${DOCKER_COMPOSE_FILE}" ] && command -v docker >/dev/null 2>&1; then
log_info "For Docker nodes, pull the latest image and restart:"
echo " cd ${CONFIG_DIR} && docker compose pull && docker compose up -d"
fi
}
do_uninstall() {
log_step "Uninstalling xboard-node..."
if command -v systemctl >/dev/null 2>&1; then
for dir in "${CONFIG_DIR}"/*/; do
[ -f "${dir}config.yml" ] || continue
local nid
nid=$(basename "$dir")
systemctl stop "xboard-node@${nid}" 2>/dev/null || true
systemctl disable "xboard-node@${nid}" 2>/dev/null || true
done
systemctl stop xboard-node 2>/dev/null || true
systemctl disable xboard-node 2>/dev/null || true
rm -f /etc/systemd/system/xboard-node.service
rm -f "/etc/systemd/system/${SERVICE_TEMPLATE}"
systemctl daemon-reload
fi
if command -v docker >/dev/null 2>&1; then
for dir in "${CONFIG_DIR}"/*/; do
[ -f "${dir}config.yml" ] || continue
local nid
nid=$(basename "$dir")
docker rm -f "xboard-node-${nid}" 2>/dev/null || true
done
fi
rm -f "${INSTALL_DIR}/xboard-node"
log_info "Binary removed"
echo ""
read -rp " Delete all configs? (${CONFIG_DIR}) [y/N]: " DELETE_ALL
if [[ "$DELETE_ALL" =~ ^[Yy]$ ]]; then
rm -rf "${CONFIG_DIR}"
log_info "All configs removed"
else
log_info "Configs preserved at ${CONFIG_DIR}/"
fi
log_info "xboard-node uninstalled"
}
# ─── Print Help ───────────────────────────────────────────────────────
print_help() {
cat << 'HELP'
xboard-node Deploy Script
DEPLOY A NODE (one command, repeat for each node):
install.sh -a <url> -t <token> -n <node_id> [-T <node_type>] [-k singbox|xray] [--docker]
OPTIONS:
-a, --api Panel URL (e.g. https://panel.example.com)
-t, --token Server Token (from panel settings)
-n, --node-id Node ID (positive integer)
-T, --node-type Node type (optional, auto-detected from panel)
-k, --kernel Kernel type (singbox or xray, default: singbox)
--docker Use Docker instead of systemd
EXAMPLES:
# Deploy node 1 (sing-box, systemd):
bash install.sh -a https://panel.example.com -t mytoken123 -n 1
# Deploy node 2 (xray, systemd):
bash install.sh -a https://panel.example.com -t mytoken123 -n 2 -k xray
# Deploy node 3 with explicit type (Docker):
bash install.sh -a https://panel.example.com -t mytoken123 -n 3 -T shadowsocks --docker
# Interactive mode (will prompt for all params):
bash install.sh
MANAGEMENT:
bash install.sh list List all deployed nodes
bash install.sh remove <node_id> Remove a node
bash install.sh update Update binary + restart all nodes
bash install.sh uninstall Remove everything
DOCKER (env-var mode, no config file needed):
docker run -d --restart=always --network=host \
-e apiHost=https://panel.example.com \
-e apiKey=YOUR_TOKEN \
-e nodeID=1 \
ghcr.io/cedar2025/xboard-node:latest
HELP
}
# ─── Main ─────────────────────────────────────────────────────────────
main() {
parse_args "$@"
case "$SUBCOMMAND" in
remove)
check_root
remove_node "$NODE_ID"
;;
list)
list_nodes
;;
update)
check_root
update_binary
;;
uninstall)
check_root
do_uninstall
;;
help|--help|-h)
print_help
;;
add|"")
check_root
detect_arch
detect_os
install_deps
mkdir -p "$CONFIG_DIR"
migrate_legacy_config
if [ "$DOCKER_MODE" -eq 0 ]; then
install_binary
install_systemd_template
fi
deploy_node
;;
*)
log_error "Unknown command: $SUBCOMMAND"
print_help
exit 1
;;
esac
}
main "$@"