#!/usr/bin/env bash set -Eeuo pipefail SCRIPT_NAME="tailscale-headscale-linux.sh" SCRIPT_PROJECT="linux-worker" SCRIPT_VERSION="v0.3.1" SCRIPT_UPDATED="2026-07-04" DEFAULT_LOGIN_SERVER="https://mesh.yohan.fun" CONFIG_DIR="/etc/agentmeshos" CONFIG_FILE="${CONFIG_DIR}/tailscale-client.conf" PENDING_REG_FILE="${CONFIG_DIR}/tailscale-pending-registration.txt" STATE_FILE="/var/lib/tailscale/tailscaled.state" SUDO="" if [ "$(id -u)" -ne 0 ]; then if ! command -v sudo >/dev/null 2>&1; then echo "当前不是 root 用户,且系统未安装 sudo。请使用 root 执行本脚本。" exit 1 fi SUDO="sudo" fi LOGIN_SERVER="${DEFAULT_LOGIN_SERVER}" HOSTNAME_VALUE="agentmeshos-worker-$(hostname -s 2>/dev/null || hostname)" if [ -f "${CONFIG_FILE}" ]; then # shellcheck disable=SC1090 . "${CONFIG_FILE}" fi require_tty() { if [ ! -r /dev/tty ]; then echo "未检测到可交互终端。请在 SSH/终端中执行本脚本,不要在无交互环境中运行。" exit 1 fi } read_tty() { local __var_name="$1" IFS= read -r "${__var_name}" /dev/null || true IFS= read -r "${__var_name}" /dev/null || true printf "\n" } pause() { printf "\n按 Enter 返回菜单..." read_tty _ } print_cmd() { printf "\n将执行命令:\n%s\n\n" "$*" } confirm_danger() { local prompt="$1" printf "%s\n请输入 YES 确认:" "${prompt}" read_tty answer [ "${answer}" = "YES" ] } ensure_config_dir() { ${SUDO} mkdir -p "${CONFIG_DIR}" ${SUDO} chmod 755 "${CONFIG_DIR}" } save_config() { ensure_config_dir local tmp tmp="$(mktemp)" { echo "LOGIN_SERVER=\"${LOGIN_SERVER}\"" echo "HOSTNAME_VALUE=\"${HOSTNAME_VALUE}\"" echo "LAST_UPDATED=\"$(date -Iseconds)\"" } > "${tmp}" print_cmd "${SUDO:-sudo} install -m 600 ${tmp} ${CONFIG_FILE}" ${SUDO} install -m 600 "${tmp}" "${CONFIG_FILE}" rm -f "${tmp}" echo "配置已保存到 ${CONFIG_FILE}。接入 key 不会保存到本机配置文件。" } install_curl_if_needed() { if command -v curl >/dev/null 2>&1; then return fi echo "未检测到 curl,正在尝试安装 curl。" if command -v apt-get >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} apt-get update && ${SUDO:-sudo} apt-get install -y curl ca-certificates" ${SUDO} apt-get update ${SUDO} apt-get install -y curl ca-certificates elif command -v dnf >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} dnf install -y curl ca-certificates" ${SUDO} dnf install -y curl ca-certificates elif command -v yum >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} yum install -y curl ca-certificates" ${SUDO} yum install -y curl ca-certificates elif command -v pacman >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} pacman -Sy --noconfirm curl ca-certificates" ${SUDO} pacman -Sy --noconfirm curl ca-certificates elif command -v apk >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} apk add --no-cache curl ca-certificates" ${SUDO} apk add --no-cache curl ca-certificates else echo "无法自动安装 curl。请先手动安装 curl 后重试。" exit 1 fi } install_or_update_tailscale() { install_curl_if_needed print_cmd "curl -fsSL https://tailscale.com/install.sh | ${SUDO:-sudo} sh" curl -fsSL https://tailscale.com/install.sh | ${SUDO} sh if command -v systemctl >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} systemctl enable --now tailscaled" ${SUDO} systemctl enable --now tailscaled elif command -v service >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} service tailscaled start" ${SUDO} service tailscaled start || true fi } prompt_login_server() { printf "控制服务器地址 [%s]:" "${LOGIN_SERVER}" read_tty input if [ -n "${input}" ]; then LOGIN_SERVER="${input}" fi case "${LOGIN_SERVER}" in https://*) ;; *) echo "控制服务器必须使用 https:// 开头。" return 1 ;; esac } prompt_hostname() { printf "本机节点名 [%s]:" "${HOSTNAME_VALUE}" read_tty input if [ -n "${input}" ]; then HOSTNAME_VALUE="${input}" fi } join_with_auth_key() { if ! command -v tailscale >/dev/null 2>&1; then echo "未检测到 tailscale,请先选择 1 安装 / 更新客户端。" return fi prompt_login_server || return prompt_hostname printf "粘贴 Headplane 生成的 auth key(例如 hskey-auth-...):" read_tty_secret AUTH_KEY if [ -z "${AUTH_KEY}" ]; then echo "接入 key 不能为空。" return fi print_cmd "${SUDO:-sudo} tailscale up --login-server=${LOGIN_SERVER} --auth-key=*** --hostname=${HOSTNAME_VALUE}" ${SUDO} tailscale up \ --login-server="${LOGIN_SERVER}" \ --auth-key="${AUTH_KEY}" \ --hostname="${HOSTNAME_VALUE}" save_config } start_manual_registration() { if ! command -v tailscale >/dev/null 2>&1; then echo "未检测到 tailscale,请先选择 1 安装 / 更新客户端。" return fi show_manual_registration_guide prompt_login_server || return prompt_hostname print_cmd "${SUDO:-sudo} tailscale up --login-server=${LOGIN_SERVER} --hostname=${HOSTNAME_VALUE}" local tmp_output tmp_output="$(mktemp)" cat </dev/null 2>&1; then timeout 20s ${SUDO} tailscale up \ --login-server="${LOGIN_SERVER}" \ --hostname="${HOSTNAME_VALUE}" 2>&1 | tee "${tmp_output}" local tailscale_status=${PIPESTATUS[0]} else ${SUDO} tailscale up \ --login-server="${LOGIN_SERVER}" \ --hostname="${HOSTNAME_VALUE}" 2>&1 | tee "${tmp_output}" & local tailscale_pid=$! sleep 20 if kill -0 "${tailscale_pid}" 2>/dev/null; then kill "${tailscale_pid}" 2>/dev/null || true wait "${tailscale_pid}" 2>/dev/null || true local tailscale_status=124 else wait "${tailscale_pid}" local tailscale_status=$? fi fi set -e ensure_config_dir { echo "生成时间:$(date -Iseconds)" echo "控制服务器:${LOGIN_SERVER}" echo "节点名称:${HOSTNAME_VALUE}" echo echo "原始输出:" cat "${tmp_output}" } > "${tmp_output}.saved" print_cmd "${SUDO:-sudo} install -m 600 ${tmp_output}.saved ${PENDING_REG_FILE}" ${SUDO} install -m 600 "${tmp_output}.saved" "${PENDING_REG_FILE}" rm -f "${tmp_output}" "${tmp_output}.saved" cat </dev/null 2>&1; then echo "当前 Tailscale 状态:" tailscale status || true echo echo "当前 Tailscale 偏好摘要:" tailscale debug prefs 2>/dev/null | sed -n '1,80p' || true else echo "未检测到 tailscale,无法读取当前客户端状态。" fi } change_login_server() { prompt_login_server || return prompt_hostname save_config echo "控制服务器已更新。若需要立即重连,请选择 4 更换接入 key 并重新连接。" } change_key_and_reconnect() { join_with_auth_key } show_status() { if ! command -v tailscale >/dev/null 2>&1; then echo "未检测到 tailscale。" return fi print_cmd "tailscale status" tailscale status || true printf "\n" print_cmd "tailscale ip -4" tailscale ip -4 || true } show_config() { echo "脚本配置:" echo "LOGIN_SERVER=${LOGIN_SERVER}" echo "HOSTNAME_VALUE=${HOSTNAME_VALUE}" echo "CONFIG_FILE=${CONFIG_FILE}" printf "\nTailscale 当前偏好:\n" if command -v tailscale >/dev/null 2>&1; then print_cmd "tailscale debug prefs" tailscale debug prefs || true else echo "未检测到 tailscale。" fi } disconnect_node() { if ! command -v tailscale >/dev/null 2>&1; then echo "未检测到 tailscale。" return fi print_cmd "${SUDO:-sudo} tailscale down" ${SUDO} tailscale down } logout_node() { if ! command -v tailscale >/dev/null 2>&1; then echo "未检测到 tailscale。" return fi if confirm_danger "退出当前 tailnet 会清除本机登录状态,确认继续吗?"; then print_cmd "${SUDO:-sudo} tailscale logout" ${SUDO} tailscale logout else echo "已取消。" fi } clear_state() { if ! confirm_danger "清空本机 Tailscale 状态会删除 ${STATE_FILE},节点需要重新接入,确认继续吗?"; then echo "已取消。" return fi if command -v systemctl >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} systemctl stop tailscaled" ${SUDO} systemctl stop tailscaled || true fi print_cmd "${SUDO:-sudo} rm -f ${STATE_FILE}" ${SUDO} rm -f "${STATE_FILE}" if command -v systemctl >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} systemctl start tailscaled" ${SUDO} systemctl start tailscaled fi } restart_service() { if command -v systemctl >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} systemctl restart tailscaled" ${SUDO} systemctl restart tailscaled print_cmd "${SUDO:-sudo} systemctl status tailscaled --no-pager" ${SUDO} systemctl status tailscaled --no-pager || true elif command -v service >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} service tailscaled restart" ${SUDO} service tailscaled restart else echo "未检测到 systemctl 或 service,请手动重启 tailscaled。" fi } uninstall_tailscale() { if ! confirm_danger "卸载会移除 Tailscale 客户端,并可选择清理本机状态,确认继续吗?"; then echo "已取消。" return fi if command -v systemctl >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} systemctl stop tailscaled" ${SUDO} systemctl stop tailscaled || true print_cmd "${SUDO:-sudo} systemctl disable tailscaled" ${SUDO} systemctl disable tailscaled || true fi if command -v apt-get >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} apt-get remove -y tailscale" ${SUDO} apt-get remove -y tailscale elif command -v dnf >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} dnf remove -y tailscale" ${SUDO} dnf remove -y tailscale elif command -v yum >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} yum remove -y tailscale" ${SUDO} yum remove -y tailscale elif command -v pacman >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} pacman -Rns --noconfirm tailscale" ${SUDO} pacman -Rns --noconfirm tailscale elif command -v apk >/dev/null 2>&1; then print_cmd "${SUDO:-sudo} apk del tailscale" ${SUDO} apk del tailscale else echo "未识别包管理器,请手动卸载 tailscale。" fi if confirm_danger "是否同时删除 /var/lib/tailscale 和 ${CONFIG_FILE}?"; then print_cmd "${SUDO:-sudo} rm -rf /var/lib/tailscale ${CONFIG_FILE}" ${SUDO} rm -rf /var/lib/tailscale "${CONFIG_FILE}" fi } show_recommended_commands() { cat <