├── tailscale-openwrt-scripts.tar.gz ├── scripts ├── github_direct_ctl.sh ├── uninstall.sh ├── update_ctl.sh ├── setup_cron.sh ├── test_mirrors.sh ├── setup_service.sh ├── notify_ctl.sh ├── autoupdate.sh ├── tailscale_up_generater.sh ├── fetch_and_install.sh ├── tools.sh ├── setup.sh └── helper.sh ├── .github └── workflows │ ├── tar.yml │ └── main.yml ├── README.md ├── pretest_mirrors.sh └── install.sh /tailscale-openwrt-scripts.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CH3NGYZ/small-tailscale-openwrt/HEAD/tailscale-openwrt-scripts.tar.gz -------------------------------------------------------------------------------- /scripts/github_direct_ctl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ -f /etc/tailscale/tools.sh ] && . /etc/tailscale/tools.sh && safe_source "$INST_CONF" 4 | 5 | 6 | # 如果没有传参,提示用户输入数字 7 | if [ $# -eq 0 ]; then 8 | if [ "$GITHUB_DIRECT" = "true" ]; then 9 | log_info "🔄 当前直连GITHUB状态: 🟢" # 绿色 10 | else 11 | log_info "🔄 当前直连GITHUB状态: 🔴" # 红色 12 | fi 13 | 14 | log_info "🎛️ 请选择操作:" 15 | log_info " 1). 使用直连 🟢" 16 | log_info " 2). 使用代理 🔴" 17 | log_info "⏳ 请输入数字 [1/2] 或 [on/off], 输入其他为退出: " 1 18 | read -r choice 19 | else 20 | choice="$1" 21 | fi 22 | 23 | case "$choice" in 24 | 1 | on) 25 | sed -i 's/^GITHUB_DIRECT=.*/GITHUB_DIRECT=true/' "$INST_CONF" 26 | log_info "🟢 已设置使用直连" 27 | ;; 28 | 2 | off) 29 | sed -i 's/^GITHUB_DIRECT=.*/GITHUB_DIRECT=false/' "$INST_CONF" 30 | log_info "🔴 已设置使用代理" 31 | ;; 32 | *) 33 | exit 1 34 | ;; 35 | esac 36 | -------------------------------------------------------------------------------- /scripts/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | [ -f /etc/tailscale/tools.sh ] && . /etc/tailscale/tools.sh 5 | 6 | log_info "🛑 开始卸载Tailscale..." 7 | 8 | # 停止并禁用 Tailscale 服务 9 | [ -f /etc/init.d/tailscale ] && { 10 | /etc/init.d/tailscale stop 11 | /etc/init.d/tailscale disable 12 | rm -f /etc/init.d/tailscale 13 | } 14 | 15 | log_info "🗑️ 删除所有相关文件..." 16 | # 删除所有可能的文件和目录 17 | rm -rf \ 18 | /etc/config/tailscale* \ 19 | /etc/init.d/tailscale* \ 20 | /usr/bin/tailscale \ 21 | /usr/bin/tailscaled \ 22 | /usr/local/bin/tailscale* \ 23 | /tmp/tailscaled \ 24 | /var/lib/tailscale* 25 | 26 | # 删除 Tailscale 网络接口 27 | ip link delete tailscale0 2>/dev/null || true 28 | 29 | # 清理定时任务 30 | log_info "🧹 清理定时任务..." 31 | sed -i "\|$CONFIG_DIR/|d" /etc/crontabs/root 32 | /etc/init.d/cron restart 33 | 34 | log_info "🎉 Tailscale卸载完成!" 35 | log_info "🎉 重装Tailscale , 请运行 tailscale-helper" 36 | log_info "🎉 如需删除安装脚本,请运行 rm -rf $CONFIG_DIR" -------------------------------------------------------------------------------- /scripts/update_ctl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ -f /etc/tailscale/tools.sh ] && . /etc/tailscale/tools.sh && safe_source "$INST_CONF" 4 | 5 | # 如果没有传参,提示用户输入数字 6 | if [ $# -eq 0 ]; then 7 | log_info "🔄 当前自动更新状态: $AUTO_UPDATE" 8 | log_info "📮 当前 Tailscale 版本: $(cat "$VERSION_FILE" 2>/dev/null || log_info "未知")" 9 | log_info "♻️ 最新 Tailscale 版本: $("$CONFIG_DIR/fetch_and_install.sh" --dry-run)" 10 | log_info "🎛️ 请选择操作:" 11 | log_info " 1). 启用自动更新 🟢" 12 | log_info " 2). 禁用自动更新 🔴" 13 | log_info "⏳ 请输入数字 [1/2] 或 [on/off], 输入其他为退出: " 1 14 | read -r choice 15 | else 16 | choice="$1" 17 | fi 18 | 19 | case "$choice" in 20 | 1 | on) 21 | sed -i 's/^AUTO_UPDATE=.*/AUTO_UPDATE=true/' "$INST_CONF" 22 | log_info "🟢 自动更新已启用" 23 | ;; 24 | 2 | off) 25 | sed -i 's/^AUTO_UPDATE=.*/AUTO_UPDATE=false/' "$INST_CONF" 26 | log_info "🔴 自动更新已禁用" 27 | ;; 28 | *) 29 | exit 1 30 | ;; 31 | esac 32 | -------------------------------------------------------------------------------- /.github/workflows/tar.yml: -------------------------------------------------------------------------------- 1 | name: Auto Package Tailscale Scripts 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - "scripts/*" 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | package_and_push: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout current repo 18 | uses: actions/checkout@v4 19 | 20 | - name: Create tarball and checksums 21 | run: | 22 | mkdir -p package 23 | cp scripts/* package/ 24 | tar -czvf tailscale-openwrt-scripts.tar.gz -C package . 25 | 26 | # 生成 SHA256 和 MD5 文件 27 | SHA256=$(sha256sum tailscale-openwrt-scripts.tar.gz | awk '{print $1}') 28 | MD5=$(md5sum tailscale-openwrt-scripts.tar.gz | awk '{print $1}') 29 | 30 | # 替换 install.sh 中的 SHA256 和 MD5 校验值 31 | sed -i "s|EXPECTED_CHECKSUM_SHA256=.*|EXPECTED_CHECKSUM_SHA256=\"$SHA256\"|" install.sh 32 | sed -i "s|EXPECTED_CHECKSUM_MD5=.*|EXPECTED_CHECKSUM_MD5=\"$MD5\"|" install.sh 33 | 34 | - name: Copy tarball to openwrt repo 35 | run: | 36 | git config --global user.email "actions@github.com" 37 | git config --global user.name "GitHub Actions" 38 | git add tailscale-openwrt-scripts.tar.gz install.sh 39 | git commit -m "$(date -u +'%Y-%m-%d %H:%M:%S' -d '+8 hours')" 40 | git push origin HEAD:main 41 | -------------------------------------------------------------------------------- /scripts/setup_cron.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | [ -f /etc/tailscale/tools.sh ] && . /etc/tailscale/tools.sh 5 | 6 | # 参数解析 7 | AUTO_UPDATE=false 8 | while [ $# -gt 0 ]; do 9 | case "$1" in 10 | --auto-update=*) 11 | AUTO_UPDATE="${1#*=}" 12 | if [ "$AUTO_UPDATE" != "true" ] && [ "$AUTO_UPDATE" != "false" ]; then 13 | log_error "❌ 错误:--auto-update 参数值应为 true 或 false" 14 | exit 1 15 | fi 16 | shift 17 | ;; 18 | *) 19 | log_error "❌ 错误:未知参数: $1" 20 | exit 1 21 | ;; 22 | esac 23 | done 24 | 25 | 26 | # 清除旧配置 27 | log_info "⏰ 清除旧的定时任务配置..." 28 | if [ -f /etc/crontabs/root ]; then 29 | sed -i "\|$CONFIG_DIR/|d" /etc/crontabs/root || { log_error "❌ 清除旧配置失败"; exit 1; } 30 | else 31 | log_info "⚠️ /etc/crontabs/root 文件不存在,跳过清除操作" 32 | fi 33 | 34 | # 生成随机小时(2~3) 35 | # 生成随机分钟(0~59) 36 | RANDOM_HOUR=$((2 + $(awk -v seed=$(date +%s) 'BEGIN{srand(seed); print int(rand()*2)}'))) 37 | RANDOM_MIN=$(awk -v seed=$(date +%s) 'BEGIN{srand(seed+1000); print int(rand()*60)}') 38 | 39 | # 添加镜像维护任务 40 | log_info "⏰ 添加镜像维护任务..." 41 | echo "$RANDOM_MIN $RANDOM_HOUR * * * $CONFIG_DIR/test_mirrors.sh" >> /etc/crontabs/root || { log_error "❌ 添加镜像维护任务失败"; exit 1; } 42 | log_info "⏰ 镜像维护任务运行时间已设定为 $RANDOM_HOUR 点 $RANDOM_MIN 分" 43 | 44 | 45 | # 自动更新任务(4~6) 46 | UPDATE_HOUR=$((4 + $(awk -v seed=$(date +%s) 'BEGIN{srand(seed+2000); print int(rand()*3)}'))) 47 | UPDATE_MIN=$(awk -v seed=$(date +%s) 'BEGIN{srand(seed+3000); print int(rand()*60)}') 48 | while [ "$RANDOM_MIN" -eq "$UPDATE_MIN" ]; do 49 | UPDATE_MIN=$(awk -v seed=$(date +%s) 'BEGIN{srand(seed+3000); print int(rand()*60)}') 50 | done 51 | 52 | # 添加自动更新任务 53 | log_info "⏰ 添加自动更新任务..." 54 | echo "$UPDATE_MIN $UPDATE_HOUR * * * $CONFIG_DIR/autoupdate.sh" >> /etc/crontabs/root || { log_error "❌ 添加自动更新任务失败"; exit 1; } 55 | log_info "⏰ 自动更新任务运行时间已设定为 $UPDATE_HOUR 点 $UPDATE_MIN 分" 56 | 57 | # 重启cron服务 58 | log_info "⏰ 重启cron服务..." 59 | /etc/init.d/cron restart || { log_error "❌ 重启cron服务失败"; exit 1; } 60 | 61 | log_info "⏰ 定时任务配置完成!" 62 | -------------------------------------------------------------------------------- /scripts/test_mirrors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | [ -f /etc/tailscale/tools.sh ] && . /etc/tailscale/tools.sh 5 | 6 | safe_source "$INST_CONF" 7 | if [ "$GITHUB_DIRECT" = "true" ]; then 8 | log_info "🌐 不测速代理池..." 9 | exit 0 10 | fi 11 | set_proxy_mode 12 | 13 | TIME_OUT=20 14 | SUM_FILE_NAME="SHA256SUMS.txt" 15 | BIN_FILE_NAME="tailscaled-linux-amd64" 16 | SUM_FILE_PATH="/tmp/$SUM_FILE_NAME" 17 | BIN_FILE_PATH="/tmp/$BIN_FILE_NAME" 18 | MIRROR_FILE_URL_SUFFIX="CH3NGYZ/test-github-proxies/raw/refs/heads/main/proxies.txt" 19 | SUM_FILE_SUFFIX="CH3NGYZ/small-tailscale-openwrt/releases/latest/download/$SUM_FILE_NAME" 20 | BIN_FILE_SUFFIX="CH3NGYZ/small-tailscale-openwrt/releases/latest/download/$BIN_FILE_NAME" 21 | 22 | rm -f "$TMP_VALID_MIRRORS" 23 | 24 | # 提前下载校验文件 25 | SUM_URL_PROXY="${CUSTOM_RELEASE_PROXY}/${SUM_FILE_SUFFIX}" 26 | 27 | if ! webget "$SUM_FILE_PATH" "$SUM_URL_PROXY" "echooff"; then 28 | log_error "❌ 无法下载校验文件" 29 | exit 1 30 | fi 31 | 32 | sha_expected=$(grep "$BIN_FILE_NAME" "$SUM_FILE_PATH" | grep -v "$BIN_FILE_NAME.build" | awk '{print $1}') 33 | 34 | # 镜像测试函数(下载并验证 tailscaled) 35 | test_mirror() { 36 | local mirror=$(echo "$1" | sed 's|/*$|/|') 37 | local progress="$2" # 当前/总数 38 | log_info "⏳ 测试[$progress] $mirror" 39 | 40 | local start=$(date +%s.%N) 41 | 42 | if webget "$BIN_FILE_PATH" "${mirror}$BIN_FILE_SUFFIX" "echooff" ; then 43 | sha_actual=$(sha256sum "$BIN_FILE_PATH" | awk '{print $1}') 44 | if [ "$sha_expected" = "$sha_actual" ]; then 45 | local end=$(date +%s.%N) 46 | local dl_time=$(awk "BEGIN {printf \"%.2f\", $end - $start}") 47 | log_info "✅ 用时 ${dl_time}s" 48 | echo "$dl_time $mirror" >> "$TMP_VALID_MIRRORS" 49 | else 50 | log_warn "❌ 校验失败" 51 | fi 52 | else 53 | log_warn "❌ 下载失败" 54 | fi 55 | rm -f "$BIN_FILE_PATH" "$SUM_FILE_PATH" 56 | } 57 | 58 | # 下载镜像列表 59 | MIRROR_FILE_URL="${CUSTOM_RAW_PROXY}/${MIRROR_FILE_URL_SUFFIX}" 60 | 61 | log_info "🛠️ 正在下载镜像列表,请耐心等待..." 62 | 63 | if webget "$MIRROR_LIST" "$MIRROR_FILE_URL" "echooff"; then 64 | log_info "✅ 已下载镜像列表" 65 | else 66 | log_warn "⚠️ 无法下载镜像列表" 67 | send_notify "⚠️ 下载远程镜像列表失败,已使用本地存在的镜像列表" 68 | fi 69 | 70 | log_warn "⚠️ 测试代理下载tailscale可执行文件花费的时间中, 每个代理最长需要 $TIME_OUT 秒, 请耐心等待......" 71 | # 主流程:测试所有镜像 72 | total=$(grep -cve '^\s*$' "$MIRROR_LIST") # 排除空行 73 | index=0 74 | while read -r mirror; do 75 | [ -n "$mirror" ] || continue 76 | index=$((index + 1)) 77 | test_mirror "$mirror" "$index/$total" 78 | done < "$MIRROR_LIST" 79 | 80 | # 排序并保存最佳镜像 81 | if [ -s "$TMP_VALID_MIRRORS" ]; then 82 | sort -n "$TMP_VALID_MIRRORS" | awk '{print $2}' > "$VALID_MIRRORS" 83 | log_info "🏆 最佳镜像: $(head -n1 "$VALID_MIRRORS")" 84 | else 85 | if should_notify_mirror_fail; then 86 | send_notify "❌ 所有镜像均失效" "请手动配置代理" 87 | fi 88 | fi 89 | 90 | rm -f "$TMP_VALID_MIRRORS" -------------------------------------------------------------------------------- /scripts/setup_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | [ -f /etc/tailscale/tools.sh ] && . /etc/tailscale/tools.sh 4 | 5 | # 加载配置文件 6 | log_info "🛠️ 加载配置文件..." 7 | safe_source "$INST_CONF" || { log_error "❌ 无法加载配置文件 $INST_CONF"; exit 1; } 8 | 9 | # 确保配置文件中有 MODE 设置 10 | if [ -z "$MODE" ]; then 11 | log_error "❌ 错误:未在配置文件中找到 MODE 设置" 12 | exit 1 13 | fi 14 | 15 | log_info "🛠️ 当前的 MODE 设置为: $MODE" 16 | 17 | # 生成服务文件 18 | log_info "🛠️ 生成服务文件..." 19 | cat > /etc/init.d/tailscale <<"EOF" 20 | #!/bin/sh /etc/rc.common 21 | USE_PROCD=1 22 | START=90 23 | STOP=1 24 | 25 | [ -f /etc/tailscale/tools.sh ] && . /etc/tailscale/tools.sh 26 | 27 | # 通用 procd 启动函数 28 | start_tailscaled() { 29 | local bin_path=$1 30 | $bin_path --cleanup > /dev/null 2>&1 31 | procd_open_instance 32 | procd_set_param name tailscale 33 | procd_set_param env TS_DEBUG_FIREWALL_MODE=auto 34 | procd_set_param command $bin_path 35 | procd_append_param command --port 41641 36 | procd_append_param command --state /etc/config/tailscaled.state 37 | procd_set_param respawn 38 | procd_set_param stdout 1 39 | procd_set_param stderr 1 40 | procd_close_instance 41 | } 42 | 43 | start_service() { 44 | log_info "🛠️ 加载服务启动配置..." 45 | safe_source "$INST_CONF" 46 | log_info "🛠️ 当前的 MODE 为: $MODE" 47 | 48 | if [ "$MODE" = "local" ]; then 49 | log_info "🛠️ 启动 Tailscale (本地模式)..." 50 | start_tailscaled /usr/bin/tailscaled 51 | log_info "🛠️ 本地模式已启动, Tailscale服务日志文件:/var/log/tailscale.log" 52 | log_info "🛠️ 本地模式, 检测更新中, 日志:/tmp/tailscale_update.log" 53 | "$CONFIG_DIR/autoupdate.sh" 2>&1 | tee -a /tmp/tailscale_update.log 54 | 55 | elif [ "$MODE" = "tmp" ]; then 56 | log_info "🛠️ 启动 Tailscale (临时模式)..." 57 | if [ -x /tmp/tailscaled ]; then 58 | log_info "✅ tmp模式, 文件已存在, 直接启动 tailscaled..." 59 | start_tailscaled /tmp/tailscaled 60 | log_info "🛠️ 临时模式已启动, Tailscale服务日志文件:/var/log/tailscale.log" 61 | else 62 | log_info "🛠️ tmp模式, 文件不存在, 正在下载 tailscaled, 日志:/tmp/tailscale_update.log" 63 | "$CONFIG_DIR/autoupdate.sh" 2>&1 | tee -a /tmp/tailscale_update.log 64 | if [ -x /tmp/tailscaled ]; then 65 | log_info "✅ 检测到文件已下载, 直接启动 tailscaled..." 66 | start_tailscaled /tmp/tailscaled 67 | log_info "🛠️ 临时模式已启动, Tailscale服务日志文件:/var/log/tailscale.log" 68 | else 69 | log_error "❌ 错误:下载失败, 未找到文件, 无法启动." 70 | fi 71 | fi 72 | else 73 | log_error "❌ 错误:未知模式 $MODE" 74 | exit 1 75 | fi 76 | } 77 | 78 | stop_service() { 79 | log_info "🛑 停止服务..." 80 | [ -x "/usr/local/bin/tailscaled" ] && /usr/local/bin/tailscaled --cleanup >/dev/null 2>&1 || true 81 | [ -x "/tmp/tailscaled" ] && /tmp/tailscaled --cleanup >/dev/null 2>&1 || true 82 | 83 | if pgrep tailscaled >/dev/null 2>&1; then 84 | killall tailscaled >/dev/null 2>&1 || log_warn "⚠️ 未能停止 tailscaled 服务" 85 | else 86 | log_info "✅ tailscaled 已停止" 87 | fi 88 | } 89 | EOF 90 | 91 | # 设置权限 92 | log_info "🛠️ 设置服务文件权限..." 93 | chmod +x /etc/init.d/tailscale 94 | 95 | # 启用服务 96 | log_info "🛠️ 启用 Tailscale 服务..." 97 | /etc/init.d/tailscale enable || { log_error "❌ 启用服务失败"; exit 1; } 98 | 99 | # 启动服务并不显示任何状态输出 100 | log_info "🛠️ 启动服务..." 101 | /etc/init.d/tailscale restart || { log_error "❌ 重启服务失败, 将启动服务"; /etc/init.d/tailscale start > /dev/null 2>&1; } 102 | 103 | # 完成 104 | log_info "🎉 Tailscale 服务已启动!" 105 | -------------------------------------------------------------------------------- /scripts/notify_ctl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 检查并引入 /etc/tailscale/tools.sh 文件 4 | [ -f /etc/tailscale/tools.sh ] && . /etc/tailscale/tools.sh 5 | 6 | # 如果配置文件不存在,初始化 7 | if [ ! -f "$NTF_CONF" ]; then 8 | log_warn "⚠️ 未找到通知配置文件, 新建一个" 9 | mkdir -p "$(dirname "$NTF_CONF")" 10 | cat > "$NTF_CONF" <> "$NTF_CONF" 69 | fi 70 | } 71 | 72 | # 设置Bark的设备码 73 | edit_bark() { 74 | log_info "🔑 请输入 Bark 推送地址 (格式: https://自建或官方api.day.app/KEYxxxxxxx): " 1 75 | read bark_key 76 | if grep -q "^BARK_KEY=" "$NTF_CONF"; then 77 | sed -i "s|^BARK_KEY=.*|BARK_KEY=\"$bark_key\"|" "$NTF_CONF" 78 | else 79 | echo "BARK_KEY=\"$bark_key\"" >> "$NTF_CONF" 80 | fi 81 | } 82 | 83 | # 设置ntfy的订阅码 84 | edit_ntfy() { 85 | log_info "🔑 请输入 NTFY 订阅码: " 1 86 | read ntfy_key 87 | if grep -q "^NTFY_KEY=" "$NTF_CONF"; then 88 | sed -i "s|^NTFY_KEY=.*|NTFY_KEY=\"$ntfy_key\"|" "$NTF_CONF" 89 | else 90 | echo "NTFY_KEY=\"$ntfy_key\"" >> "$NTF_CONF" 91 | fi 92 | } 93 | 94 | # 切换配置开关(通用函数) 95 | toggle_setting() { 96 | local setting=$1 97 | local show_log=${2:-false} 98 | if grep -q "^$setting=" "$NTF_CONF"; then 99 | current=$(grep "^$setting=" "$NTF_CONF" | cut -d= -f2) 100 | new_value=$([ "$current" = "1" ] && echo "0" || echo "1") 101 | sed -i "s|^$setting=.*|$setting=$new_value|" "$NTF_CONF" 102 | [ "$show_log" = "true" ] && log_info "$setting 已切换为 $new_value" 103 | else 104 | # 如果配置项不存在,则默认设置为开启(1) 105 | echo "$setting=1" >> "$NTF_CONF" 106 | [ "$show_log" = "true" ] && log_info "$setting 设置为开启 (1)" 107 | fi 108 | } 109 | 110 | 111 | # 测试通知 112 | test_notify() { 113 | send_notify "✅ 这是测试消息" "时间: $(date '+%F %T')" 114 | } 115 | 116 | # 主菜单 117 | while :; do 118 | show_menu 119 | log_info "📝 请选择 [1-10]: " 1 120 | read choice 121 | case $choice in 122 | 0) log_info "🚪 退出脚本" && exit 0 ;; 123 | 1) edit_key ;; 124 | 2) edit_bark ;; 125 | 3) edit_ntfy ;; 126 | 4) toggle_setting "NOTIFY_SERVERCHAN" ;; 127 | 5) toggle_setting "NOTIFY_BARK" ;; 128 | 6) toggle_setting "NOTIFY_NTFY" ;; 129 | 7) toggle_setting "NOTIFY_UPDATE" true ;; 130 | 8) toggle_setting "NOTIFY_MIRROR_FAIL" true ;; 131 | 9) toggle_setting "NOTIFY_EMERGENCY" true ;; 132 | 10) test_notify ;; 133 | *) log_warn "❌ 无效选择,请重新输入" ;; 134 | esac 135 | done 136 | -------------------------------------------------------------------------------- /scripts/autoupdate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ -f /etc/tailscale/tools.sh ] && . /etc/tailscale/tools.sh 4 | 5 | # 默认变量 6 | MODE="" 7 | ARCH="" 8 | current="" 9 | remote="" 10 | # 加载安装配置 11 | safe_source "$INST_CONF" 12 | 13 | [ -z "$MODE" ] && log_error "❌ 缺少 MODE 配置" && exit 1 14 | [ -z "$ARCH" ] && ARCH="$(uname -m)" 15 | [ -z "$current" ] && current="latest" 16 | 17 | [ "$AUTO_UPDATE" = "true" ] && auto_update_enabled=1 || auto_update_enabled=0 18 | 19 | # 查询远程最新版本 20 | remote=$("$CONFIG_DIR/fetch_and_install.sh" --dry-run) 21 | 22 | # 本地记录的版本 23 | recorded="" 24 | [ -f "$VERSION_FILE" ] && recorded=$(cat "$VERSION_FILE") 25 | 26 | # 加载通知配置 27 | [ -f $CONFIG_DIR/notify.conf ] && . $CONFIG_DIR/notify.conf 28 | 29 | 30 | # 检查是否需要发送通知的函数 31 | should_notify() { 32 | local notify_type=$1 33 | local notify_var 34 | case "$notify_type" in 35 | "update") notify_var="$NOTIFY_UPDATE" ;; 36 | "mirror_fail") notify_var="$NOTIFY_MIRROR_FAIL" ;; 37 | "emergency") notify_var="$NOTIFY_EMERGENCY" ;; 38 | *) 39 | log_error "❌ 未知通知类型: $notify_type" 40 | return 1 41 | ;; 42 | esac 43 | # 检查是否启用通知 44 | if [ "$notify_var" = "1" ]; then 45 | return 0 46 | else 47 | return 1 48 | fi 49 | } 50 | 51 | # local 模式逻辑 52 | if [ "$MODE" = "local" ]; then 53 | if [ "$AUTO_UPDATE" = "true" ]; then 54 | if [ "$remote" = "$recorded" ]; then 55 | log_info "✅ 本地已是最新版 $remote, 无需更新" 56 | exit 0 57 | fi 58 | 59 | if "$CONFIG_DIR/fetch_and_install.sh" --version="$remote" --mode="local" --mirror-list="$VALID_MIRRORS"; then 60 | echo "$remote" > "$VERSION_FILE" 61 | log_info "✅ 更新成功至版本 $remote" 62 | log_info "🛠️ 重启以应用最新版..." 63 | /etc/init.d/tailscale restart || { log_error "❌ 重启服务失败, 将启动服务"; /etc/init.d/tailscale start >/dev/null 2>&1 & } 64 | # 如果启用更新通知,发送通知 65 | if should_notify "update"; then 66 | send_notify "✅ Tailscale 已更新" "版本更新至 $remote" 67 | fi 68 | else 69 | log_error "❌ 更新失败" 70 | # 如果启用紧急通知,发送通知 71 | if should_notify "emergency"; then 72 | send_notify "❌ Tailscale 更新失败" "版本更新失败,请检查日志" 73 | fi 74 | exit 1 75 | fi 76 | else 77 | if [ ! -x "/usr/local/bin/tailscaled" ]; then 78 | log_info "⚙️ 未检测到 tailscaled,尝试安装默认版本 $current..." 79 | if "$CONFIG_DIR/fetch_and_install.sh" --version="$current" --mode="local" --mirror-list="$VALID_MIRRORS"; then 80 | echo "$current" > "$VERSION_FILE" 81 | else 82 | log_error "❌ 安装失败" 83 | # 如果启用紧急通知,发送通知 84 | if should_notify "emergency"; then 85 | send_notify "❌ Tailscale 安装失败" "默认版本 $current 安装失败" "" 86 | fi 87 | exit 1 88 | fi 89 | else 90 | log_info "✅ 自动更新已关闭, 本地已存在 tailscaled, 跳过安装" 91 | fi 92 | fi 93 | 94 | elif [ "$MODE" = "tmp" ]; then 95 | version_to_use="$([ "$current" = "latest" ] && echo "$remote" || echo "$current")" 96 | 97 | 98 | if [ "$AUTO_UPDATE" = "true" ]; then 99 | # 如果启用自动更新,且版本与本地记录不一致,才进行更新 100 | if [ "$version_to_use" != "$recorded" ]; then 101 | # 开机和第一次安装时 102 | log_info "🌐 检测到新版本 $version_to_use, 开始更新..." 103 | if "$CONFIG_DIR/fetch_and_install.sh" --version="$version_to_use" --mode="tmp" --mirror-list="$VALID_MIRRORS"; then 104 | echo "$version_to_use" > "$VERSION_FILE" 105 | log_info "✅ 更新成功至版本 $version_to_use" 106 | log_info "🛠️ 重启以应用最新版..." 107 | /etc/init.d/tailscale restart || { log_error "❌ 重启服务失败, 将启动服务"; /etc/init.d/tailscale start >/dev/null 2>&1 & } 108 | 109 | # 发送更新通知 110 | if should_notify "update"; then 111 | send_notify "✅ Tailscale TMP 模式已更新" "版本更新至 $version_to_use" 112 | fi 113 | else 114 | log_error "❌ TMP 更新失败" 115 | # 发送紧急通知 116 | if should_notify "emergency"; then 117 | send_notify "❌ Tailscale TMP 更新失败" "版本更新失败,请检查日志" 118 | fi 119 | exit 1 120 | fi 121 | else 122 | log_info "✅ TMP 当前版本 $version_to_use 已是最新" 123 | fi 124 | else 125 | # 如果不启用自动更新,先检测文件是否存在, 文件存在则直接跳过, (第一次安装) 文件不存在则使用指定版本进行安装 (开机时) 126 | if [ ! -x "/tmp/tailscaled" ]; then 127 | log_info "⚙️ 不启用自动更新, TMP 模式不存在 tailscaled, 安装指定版本 $recorded..." 128 | if "$CONFIG_DIR/fetch_and_install.sh" --version="$recorded" --mode="tmp" --mirror-list="$VALID_MIRRORS"; then 129 | echo "$recorded" > "$VERSION_FILE" 130 | else 131 | log_error "❌ TMP 安装失败" 132 | # 发送紧急通知 133 | if should_notify "emergency"; then 134 | send_notify "❌ Tailscale TMP 安装失败" "指定版本 $version_to_use 安装失败" 135 | fi 136 | exit 1 137 | fi 138 | else 139 | log_info "⚙️ 不启用自动更新, TMP 模式已存在 tailscaled, 跳过安装" 140 | fi 141 | fi 142 | fi 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 专用于 OpenWrt 的小型 Tailscale 一键安装工具 2 | 3 | [![Release](https://img.shields.io/github/release/CH3NGYZ/small-tailscale-openwrt)](https://github.com/CH3NGYZ/small-tailscale-openwrt/releases/latest) 4 | [![Downloads](https://img.shields.io/github/downloads/CH3NGYZ/small-tailscale-openwrt/latest/total)](https://github.com/CH3NGYZ/small-tailscale-openwrt/releases/latest) 5 | [![Visitors](https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Fgithub.com%2FCH3NGYZ%2Fsmall-tailscale-openwrt&label=views&countColor=%23263759&style=flat)](https://github.com/CH3NGYZ/small-tailscale-openwrt) 6 | [![Stars](https://img.shields.io/github/stars/CH3NGYZ/small-tailscale-openwrt)](https://github.com/CH3NGYZ/small-tailscale-openwrt/stargazers) 7 | 8 | ### 脚本目前只在下列系统上测试通过, 其他系统请自测, 可能还有点小 bug, 如发现问题请及时提 [issue](https://github.com/CH3NGYZ/small-tailscale-openwrt/issues/new) 反馈~ 9 | 10 | - x86_64 11 | - [iStoreOS-24.10.4-2025102410](https://site.istoreos.com/firmware/download?devicename=x86_64&firmware=iStoreOS) 12 | - [EzOpWrt-Vip-Super-202510010716-6.6.106](https://github.com/sirpdboy/openwrt?tab=readme-ov-file#%E5%9B%BA%E4%BB%B6%E4%B8%8B%E8%BD%BD) 13 | 14 | ## 📦 仓库文件结构 15 | 16 | ``` 17 | ├── install.sh # 安装脚本包到本地 18 | ├── pretest_mirrors.sh # 第一次安装测速代理池的脚本 19 | ├── tailscale-openwrt-scripts.tar.gz # 本仓库的Scripts目录下的脚本压缩包 20 | └── scripts 21 | ├── autoupdate.sh # 自动更新脚本 22 | ├── fetch_and_install.sh # 获取并安装脚本 23 | ├── github_direct_ctl.sh # 切换直连或代理设置脚本 24 | ├── helper.sh # 辅助脚本 25 | ├── notify_ctl.sh # 通知设置脚本 26 | ├── setup_cron.sh # 设置定时任务脚本 27 | ├── setup_service.sh # 设置服务脚本 28 | ├── setup.sh # 安装tailscale脚本 29 | ├── tailscale_up_generater.sh # 生成tailscale up命令脚本 30 | ├── test_mirrors.sh # 测试镜像脚本 31 | ├── tools.sh # 公共脚本 32 | └── uninstall.sh # 卸载脚本 33 | └── update_ctl.sh # 自动更新设置脚本 34 | 35 | ``` 36 | 37 | ## 🚀 快速安装 38 | 39 | ### 1.下载管理工具 & 排序代理池 40 | 41 | ```bash 42 | # 代理版 43 | rm -rf /etc/tailscale /tmp/tailscale-use-direct /tmp/install.sh 44 | URL="https://gh.ch3ng.top/CH3NGYZ/small-tailscale-openwrt/raw/refs/heads/main/install.sh" 45 | (command -v curl >/dev/null && curl -fSL "$URL" -o /tmp/install.sh || wget "$URL" -O /tmp/install.sh) || { echo 下载失败; exit 1; } 46 | sh /tmp/install.sh || { echo 执行失败; exit 1; } 47 | 48 | ``` 49 | 50 | ##### 请注意,如果您开启了代理,下载及更新可能会出现网络问题,可以尝试使用下方的直连命令安装: 51 | 52 | ```bash 53 | # 直连版 54 | rm -rf /etc/tailscale /tmp/install.sh 55 | touch /tmp/tailscale-use-direct 56 | URL="https://github.com/CH3NGYZ/small-tailscale-openwrt/raw/refs/heads/main/install.sh" 57 | (command -v curl >/dev/null && curl -fSL "$URL" -o /tmp/install.sh || wget "$URL" -O /tmp/install.sh) || { echo 下载失败; exit 1; } 58 | sh /tmp/install.sh || { echo 执行失败; exit 1; } 59 | 60 | ``` 61 | 62 | ### 2.启动管理工具 63 | 64 | ```bash 65 | tailscale-helper 66 | ``` 67 | 68 | ### 流程图: 69 | 70 |
代理版流程图 71 | 72 | ```mermaid 73 | graph TD 74 | A[开始安装] --> B[下载脚本包] 75 | B --> D{下载并校验通过?} 76 | D -->|是| E[解压脚本] 77 | D -->|否| F[尝试直连下载] 78 | F --> H{下载并校验通过?} 79 | H -->|是| E 80 | H -->|否| X[❌ 安装失败] 81 | 82 | E --> I[创建快捷命令] 83 | I --> J[初始化配置] 84 | J --> K[测速代理] 85 | K --> L[生成可用镜像] 86 | L --> Y[✅ 安装完成] 87 | 88 | X --> Z[结束] 89 | ``` 90 | 91 |
直连版流程图 92 | 93 | ```mermaid 94 | graph TD 95 | A[开始安装] --> B[下载脚本包] 96 | B --> D{下载并校验通过?} 97 | D -->|是| E[解压脚本] 98 | D -->|否| X[❌ 安装失败] 99 | 100 | E --> I[创建快捷命令] 101 | I --> J[初始化配置] 102 | J --> Y[✅ 安装完成] 103 | X --> Z[结束] 104 | 105 | ``` 106 | 107 |
108 | 109 | ## 🛠️ 管理工具说明 110 | 111 | `tailscale-helper` 命令进入交互式管理界面,提供以下功能: 112 | 113 | 1. 💾 **安装 / 重装 Tailscale**:运行安装脚本来安装或重装 Tailscale。 114 | 2. 📥 **登录 Tailscale**:执行 `tailscale up` 命令并监听登录 URL 输出。 115 | 3. 📝 **生成启动命令**:交互式生成所需的参数和 `tailscale up` 命令。 116 | 4. 📤 **登出 Tailscale**:执行 `tailscale logout` 并检查状态。 117 | 5. ❌ **卸载 Tailscale**:清理并卸载 Tailscale。 118 | 6. 🔄 **管理自动更新**:配置本地或临时模式的自动更新策略。 119 | 7. 🔄 **手动运行更新脚本**:立即执行自动更新脚本。 120 | 8. 🔄 **切换 GitHub 直连/代理**:在使用直连或代理之间切换。(仅限中国用户) 121 | 9. 📦 **查看本地版本**:检查当前安装的 Tailscale 版本。 122 | 10. 📦 **查看远程版本**:获取并显示可用的最新 Tailscale 版本。 123 | 11. 🔔 **管理推送通知**:配置 Server酱 / Bark / NTFY 通知。 124 | 12. 📊 **排序代理池**:测试代理的可用性并排序。(仅限中国用户) 125 | 13. 🛠️ **更新脚本包**:从 GitHub 获取最新的管理脚本并自动更新。 126 | 14. 📜 **查看更新日志**:查看与 Tailscale 启动或更新相关的日志。 127 | 15. ⛔ **退出**:退出管理工具。 128 | 129 | ## 🔔 通知系统 130 | 支持 Server酱、Bark 和 NTFY 通知方式: 131 | 132 | 配置选项包括: 133 | 134 | - 更新通知:版本升级成功/失败时提醒 135 | - 代理失败:检测代理都不可用时提醒 136 | 137 | ## ⚠️ 注意事项 138 | 1. 内存安装模式每次重启后需重新下载 Tailscale 139 | 2. 由于代理不稳定,可能出现下载失败,建议使用本地安装模式 140 | 3. 首次使用建议配置通知功能 141 | 4. 需要至少 curl 或 wget 之一才能正常使用 142 | 143 | ## 💬 联系方式 144 | 145 | - 如有问题或建议,请提交 issue 或 email 至 github@ch3ng.top 146 | 147 | ## 😍 鸣谢 148 | 1. [glinet-tailscale-updater](https://github.com/Admonstrator/glinet-tailscale-updater) 149 | 2. [golang](https://github.com/golang/go) 150 | 3. [UPX](https://github.com/upx/upx) 151 | 152 | ## Star History 153 | [![Star History Chart](https://api.star-history.com/svg?repos=CH3NGYZ/small-tailscale-openwrt&type=Date)](https://www.star-history.com/#CH3NGYZ/small-tailscale-openwrt&Date) 154 | 155 | ``` 156 | -------------------------------------------------------------------------------- /scripts/tailscale_up_generater.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ -f /etc/tailscale/tools.sh ] && . /etc/tailscale/tools.sh 4 | CONFIG_DIR="/etc/tailscale" 5 | CONF_FILE="$CONFIG_DIR/tailscale_up.conf" 6 | 7 | PARAMS_LIST="--accept-dns:flag:接受来自管理面板的 DNS 配置(默认 true) 8 | --accept-risk:value:接受风险类型并跳过确认(lose-ssh、mac-app-connector、linux-strict-rp-filter、all) 9 | --accept-routes:flag:接受其他 Tailscale 节点通告的路由(默认 false) 10 | --advertise-connector:flag:将此节点宣告为应用连接器(默认 false) 11 | --advertise-exit-node:flag:提供此节点作为出口节点以转发互联网流量(默认 false) 12 | --advertise-routes:value:向其他节点通告的路由(逗号分隔,例如 10.0.0.0/8,192.168.0.0/24),空字符串表示不通告 13 | --advertise-tags:value:请求的 ACL 标签(逗号分隔),每个必须以 tag: 开头 14 | --auth-key:value:节点授权密钥;如果以 file: 开头则为包含密钥的文件路径 15 | --client-id:value:用于通过工作负载身份联合生成授权密钥的 Client ID 16 | --client-secret:value:用于通过 OAuth 生成授权密钥的 Client Secret;以 file: 开头则为包含密钥的文件路径 17 | --exit-node:value:Tailscale 出口节点(IP、基本名称或 auto:any),空字符串表示不使用出口节点 18 | --exit-node-allow-lan-access:flag:通过出口节点路由时允许直接访问本地局域网(默认 false) 19 | --force-reauth:flag:强制重新认证(警告:会断开 Tailscale 连接,不应在 SSH 或 RDP 远程执行)(默认 false) 20 | --hostname:value:使用此主机名而不是操作系统提供的名称 21 | --id-token:value:从身份提供商获取的 ID token,用于与控制服务器交换以进行工作负载身份联合;以 file: 开头则为文件路径 22 | --json:flag:以 JSON 格式输出(警告:格式可能变更)(默认 false) 23 | --login-server:value:控制服务器的基础 URL(默认 https://controlplane.tailscale.com) 24 | --netfilter-mode:value:netfilter 模式(on、nodivert、off 之一)(默认 on) 25 | --operator:value:允许无需 sudo 操作 tailscaled 的 Unix 用户名 26 | --qr:flag:显示登录 URL 的二维码(默认 false) 27 | --qr-format:value:二维码格式(small 或 large,默认 small) 28 | --reset:flag:将未指定的设置重置为默认值(默认 false) 29 | --shields-up:flag:不允许传入连接(默认 false) 30 | --snat-subnet-routes:flag:对通过 --advertise-routes 通告的本地路由进行源 NAT(默认 true) 31 | --ssh:flag:运行 SSH 服务器,允许根据 tailnet 管理员声明的策略访问(默认 false) 32 | --stateful-filtering:flag:对转发的数据包应用有状态过滤(子网路由器、出口节点等)(默认 false) 33 | --timeout:value:等待 tailscaled 进入 Running 状态的最长时间;默认 0s 表示永远等待" 34 | 35 | # 获取参数类型 36 | get_param_type() { 37 | echo "$PARAMS_LIST" | grep "^$1:" | cut -d':' -f2 38 | } 39 | 40 | # 获取参数描述 41 | get_param_desc() { 42 | echo "$PARAMS_LIST" | grep "^$1:" | cut -d':' -f3- 43 | } 44 | 45 | # 加载配置文件 46 | load_conf() { 47 | [ -f "$CONF_FILE" ] || return 48 | while IFS='=' read -r key value; do 49 | [ -z "$key" ] && continue 50 | case "$key" in \#*) continue ;; esac 51 | key=$(echo "$key" | tr '-' '_' | tr '[:lower:]' '[:upper:]') 52 | value=$(echo "$value" | sed 's/^"\(.*\)"$/\1/') 53 | eval "$key=\"$value\"" 54 | log_info "加载配置: $key=$value" 55 | done < "$CONF_FILE" 56 | } 57 | 58 | # 保存配置到文件 59 | save_conf() { 60 | echo -n > "$CONF_FILE" 61 | echo "$PARAMS_LIST" | while IFS= read -r line; do 62 | key=$(echo "$line" | cut -d':' -f1) 63 | var_name=$(echo "$key" | tr '-' '_' | tr '[:lower:]' '[:upper:]') 64 | eval val=\$$var_name 65 | [ -n "$val" ] && echo "$key=\"$val\"" >> "$CONF_FILE" 66 | done 67 | } 68 | 69 | # 显示当前参数状态 70 | show_status() { 71 | clear 72 | log_info "当前 tailscale up 参数状态:" 73 | max_key_len=0 74 | max_val_len=0 75 | i=1 76 | OPTIONS="" 77 | echo "$PARAMS_LIST" > /tmp/params_list.txt 78 | while IFS= read -r line; do 79 | [ -z "$line" ] && continue 80 | key=$(echo "$line" | cut -d':' -f1) 81 | type=$(echo "$line" | cut -d':' -f2) 82 | desc=$(echo "$line" | cut -d':' -f3-) 83 | var_name=$(echo "$key" | tr '-' '_' | tr '[:lower:]' '[:upper:]') 84 | eval val=\$$var_name 85 | [ "${#key}" -gt "$max_key_len" ] && max_key_len=${#key} 86 | [ "${#val}" -gt "$max_val_len" ] && max_val_len=${#val} 87 | OPTIONS="${OPTIONS} 88 | $i|$key" 89 | emoji="❌" 90 | [ -n "$val" ] && emoji="✅" 91 | if [ -n "$val" ]; then 92 | printf "%2d) [%s] %-${max_key_len}s = %-${max_val_len}s # %s\n" \ 93 | "$i" "$emoji" "$key" "$val" "$desc" 94 | else 95 | printf "%2d) [%s] %-${max_key_len}s %*s# %s\n" \ 96 | "$i" "$emoji" "$key" $((max_val_len + 3)) "" "$desc" 97 | fi 98 | i=$((i + 1)) 99 | done < /tmp/params_list.txt 100 | log_info "⏳ 0) 退出 g) 生成带参数的 tailscale up 命令" 101 | log_info "⏳ 输入编号后回车即可修改: " 1 102 | } 103 | 104 | # 编辑指定参数 105 | edit_param() { 106 | idx=$1 107 | key=$(echo "$OPTIONS" | grep "^$idx|" | cut -d'|' -f2) 108 | [ -z "$key" ] && return 109 | type=$(get_param_type "$key") 110 | desc=$(get_param_desc "$key") 111 | var_name=$(echo "$key" | tr '-' '_' | tr '[:lower:]' '[:upper:]') 112 | eval val=\$$var_name 113 | 114 | if [ "$type" = "flag" ]; then 115 | if [ -z "$val" ]; then 116 | eval "$var_name=1" 117 | log_info "✅ 启用了 $key" 118 | else 119 | unset $var_name 120 | log_info "❌ 禁用了 $key" 121 | fi 122 | else 123 | if [ -z "$val" ]; then 124 | log_info "🔑 请输入 $key 的值($desc):" 1 125 | read val 126 | [ -n "$val" ] && eval "$var_name=\"$val\"" && log_info "✅ 保存了 $key 的值:$val" 127 | else 128 | log_info "🔄 当前 $key 的值为 $val,直接回车则清除,输入其他值则更新:" 1 129 | read newval 130 | if [ -n "$newval" ]; then 131 | eval "$var_name=\"$newval\"" 132 | log_info "✅ 更新了 $key 的值:$newval" 133 | else 134 | unset $var_name 135 | log_info "❌ 删除了 $key 的值" 136 | fi 137 | fi 138 | fi 139 | save_conf 140 | sleep 1 141 | } 142 | 143 | # 生成带参数的 tailscale up 命令 144 | generate_cmd() { 145 | cmd="tailscale up" 146 | temp_file=$(mktemp) 147 | echo "$PARAMS_LIST" > "$temp_file" 148 | 149 | while IFS= read -r line; do 150 | key=$(echo "$line" | cut -d':' -f1) 151 | type=$(echo "$line" | cut -d':' -f2) 152 | var_name=$(echo "$key" | tr '-' '_' | tr '[:lower:]' '[:upper:]') 153 | eval val=\$$var_name 154 | [ -z "$val" ] && continue 155 | 156 | if [ "$type" = "flag" ]; then 157 | cmd="$cmd $key" 158 | log_info "正在拼接命令: $key" 159 | else 160 | cmd="$cmd $key=$val" 161 | log_info "正在拼接命令: $key=$val" 162 | fi 163 | done < "$temp_file" 164 | 165 | rm -f "$temp_file" 166 | 167 | log_info "⏳ 生成命令:" 168 | log_info "$cmd" 169 | log_info "🟢 是否立即执行该命令?[y/N]: " 1 170 | read runnow 171 | if [ -z "$runnow" ] || [ "$runnow" = "y" ] || [ "$runnow" = "Y" ]; then 172 | log_info "🚀 正在执行 tailscale up ..." 173 | eval "$cmd" 174 | log_info "⏳ 请按回车继续..." 1 175 | read _ 176 | exit 0 177 | fi 178 | } 179 | 180 | # 主函数 181 | main() { 182 | while true; do 183 | load_conf 184 | show_status 185 | read input 186 | if [ "$input" = "0" ]; then 187 | exit 0 188 | elif [ "$input" = "g" ]; then 189 | generate_cmd 190 | elif echo "$OPTIONS" | grep -q "^$input|"; then 191 | edit_param "$input" 192 | fi 193 | done 194 | } 195 | 196 | main 197 | -------------------------------------------------------------------------------- /scripts/fetch_and_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | [ -f /etc/tailscale/tools.sh ] && . /etc/tailscale/tools.sh && safe_source "$INST_CONF" 6 | apply_github_mode 7 | 8 | GITHUB_API_LATEST_RELEASE_URL_SUFFIX="repos/CH3NGYZ/small-tailscale-openwrt/releases/latest" 9 | 10 | # 获取最新版本 11 | get_latest_version() { 12 | local api_url="${CUSTOM_API_PROXY}/${GITHUB_API_LATEST_RELEASE_URL_SUFFIX}" 13 | local tmp_json_file="/tmp/github_latest_release.json" 14 | local json="" 15 | local version="" 16 | 17 | # 使用 webget 下载 JSON 18 | if ! webget "$tmp_json_file" "$api_url" "echooff"; then 19 | log_error "❌ 错误:获取版本信息失败。" 20 | return 1 21 | fi 22 | 23 | # 读取 JSON 内容 24 | json=$(cat "$tmp_json_file") 25 | rm -f "$tmp_json_file" 26 | 27 | # 使用 jq 或 grep/sed 提取 tag_name 28 | if command -v jq >/dev/null 2>&1; then 29 | version=$(echo "$json" | jq -r '.tag_name // empty') 30 | else 31 | version=$(echo "$json" \ 32 | | grep -o '"tag_name"[ ]*:[ ]*"[^"]*"' \ 33 | | sed 's/.*"tag_name"[ ]*:[ ]*"\([^"]*\)".*/\1/' \ 34 | | head -n1) 35 | fi 36 | 37 | if [ -z "$version" ]; then 38 | log_error "❌ 错误:版本号为空" 39 | return 1 40 | fi 41 | 42 | echo "$version" 43 | } 44 | 45 | get_checksum() { 46 | local sums_file=$1 47 | local target_name=$2 48 | grep "$target_name" "$sums_file" | grep -v "${target_name}.build" | awk '{print $1}' 49 | } 50 | 51 | download_file() { 52 | local url=$1 53 | local output=$2 54 | local mirror_list=${3:-} 55 | local checksum=${4:-} 56 | 57 | if [ "$GITHUB_DIRECT" = "true" ] ; then 58 | log_info "📄 使用 GitHub 直连: https://github.com/$url" 59 | if webget "$output" "https://github.com/$url" "echooff"; then 60 | [ -n "$checksum" ] && verify_checksum "$output" "$checksum" 61 | return 0 62 | else 63 | return 1 64 | fi 65 | fi 66 | 67 | if [ -f "$mirror_list" ]; then 68 | while read -r mirror; do 69 | mirror=$(echo "$mirror" | sed 's|/*$|/|') 70 | log_info "🔗 使用代理镜像下载: ${mirror}${url}" 71 | if webget "$output" "${mirror}${url}" "echooff"; then 72 | if [ -n "$checksum" ]; then 73 | if verify_checksum "$output" "$checksum"; then 74 | return 0 75 | else 76 | log_warn "⚠️ 校验失败,尝试下一个镜像..." 77 | fi 78 | else 79 | return 0 80 | fi 81 | fi 82 | done < "$mirror_list" 83 | fi 84 | 85 | log_info "🔗 镜像全部失败,尝试 GitHub 直连: https://github.com/$url" 86 | if webget "$output" "https://github.com/$url" "echooff"; then 87 | [ -n "$checksum" ] && verify_checksum "$output" "$checksum" 88 | return 0 89 | else 90 | return 1 91 | fi 92 | } 93 | 94 | 95 | verify_checksum() { 96 | local file=$1 97 | local expected=$2 98 | 99 | local actual="" 100 | 101 | if [ ${#expected} -eq 64 ]; then 102 | actual=$(sha256sum "$file" | awk '{print $1}') 103 | log_info "🔗 预期 SHA256: $expected" 104 | log_info "🔗 计算 SHA256: $actual" 105 | elif [ ${#expected} -eq 32 ]; then 106 | actual=$(md5sum "$file" | awk '{print $1}') 107 | log_info "🔗 预期 MD5: $expected" 108 | log_info "🔗 计算 MD5: $actual" 109 | else 110 | log_info "🔗 预期: $expected" 111 | log_warn "⚠️ 未知校验长度,跳过校验" 112 | return 0 113 | fi 114 | 115 | if [ "$expected" = "$actual" ]; then 116 | log_info "✅ 校验通过" 117 | return 0 118 | else 119 | log_error "❌ 校验失败" 120 | return 1 121 | fi 122 | } 123 | 124 | 125 | # 主安装流程 126 | install_tailscale() { 127 | local version=$1 128 | local mode=$2 129 | local mirror_list=$3 130 | 131 | local arch="$ARCH" 132 | local tailscale_temp_path="/tmp/tailscaled.$$" 133 | local release_arch_filename="tailscaled-linux-$arch" 134 | local release_version_suffix="CH3NGYZ/small-tailscale-openwrt/releases/download/$version" 135 | 136 | log_info "🔗 准备校验文件..." 137 | sha_file="/tmp/SHA256SUMS.$$" 138 | md5_file="/tmp/MD5SUMS.$$" 139 | 140 | # 下载校验文件 141 | download_file "${release_version_suffix}/SHA256SUMS.txt" "$sha_file" "$mirror_list" || log_warn "⚠️ 无法获取 SHA256 校验文件" 142 | download_file "${release_version_suffix}/MD5SUMS.txt" "$md5_file" "$mirror_list" || log_warn "⚠️ 无法获取 MD5 校验文件" 143 | 144 | sha256="" 145 | md5="" 146 | [ -s "$sha_file" ] && sha256=$(get_checksum "$sha_file" "$release_arch_filename") 147 | [ -s "$md5_file" ] && md5=$(get_checksum "$md5_file" "$release_arch_filename") 148 | 149 | # 下载主程序并校验 150 | log_info "🔗 正在下载 Tailscale $version ($arch)..." 151 | if ! download_file "$release_version_suffix/$release_arch_filename" "$tailscale_temp_path" "$mirror_list" "$sha256"; then 152 | log_warn "⚠️ SHA256 校验失败,尝试使用 MD5..." 153 | if ! download_file "$release_version_suffix/$release_arch_filename" "$tailscale_temp_path" "$mirror_list" "$md5"; then 154 | log_error "❌ 校验失败,安装中止" 155 | rm -f "$tailscale_temp_path" 156 | exit 1 157 | fi 158 | fi 159 | 160 | # 安装 161 | chmod +x "$tailscale_temp_path" 162 | if [ "$mode" = "local" ]; then 163 | mkdir -p /usr/local/bin 164 | mv "$tailscale_temp_path" /usr/local/bin/tailscaled 165 | ln -sf /usr/local/bin/tailscaled /usr/bin/tailscaled 166 | ln -sf /usr/local/bin/tailscaled /usr/bin/tailscale 167 | log_info "✅ 安装到 /usr/local/bin/" 168 | else 169 | mv "$tailscale_temp_path" /tmp/tailscaled 170 | ln -sf /tmp/tailscaled /usr/bin/tailscaled 171 | ln -sf /tmp/tailscaled /usr/bin/tailscale 172 | log_info "✅ 安装到 /tmp (内存模式)" 173 | fi 174 | 175 | echo "$version" > "$VERSION_FILE" 176 | } 177 | 178 | # 参数解析 179 | MODE="local" 180 | VERSION="latest" 181 | MIRROR_LIST="" 182 | DRY_RUN=false 183 | 184 | while [ $# -gt 0 ]; do 185 | case "$1" in 186 | --mode=*) MODE="${1#*=}"; shift ;; 187 | --version=*) VERSION="${1#*=}"; shift ;; 188 | --mirror-list=*) MIRROR_LIST="${1#*=}"; shift ;; 189 | --dry-run) DRY_RUN=true; shift ;; 190 | *) log_error "未知参数: $1"; exit 1 ;; 191 | esac 192 | done 193 | 194 | if [ "$VERSION" = "latest" ]; then 195 | set +e 196 | while true; do 197 | VERSION=$(get_latest_version) 198 | if [ $? -eq 0 ] && [ -n "$VERSION" ]; then 199 | break 200 | fi 201 | sleep 1 202 | done 203 | set -e 204 | fi 205 | 206 | # 干跑模式(只输出版本号) 207 | if [ "$DRY_RUN" = "true" ]; then 208 | echo "$VERSION" 209 | exit 0 210 | fi 211 | 212 | # 执行安装 213 | install_tailscale "$VERSION" "$MODE" "$MIRROR_LIST" 214 | -------------------------------------------------------------------------------- /scripts/tools.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # /etc/tailscale/tools.sh 3 | CONFIG_DIR="/etc/tailscale" 4 | mkdir -p "$CONFIG_DIR" 5 | LOG_FILE="/var/log/tailscale_install.log" 6 | VERSION_FILE="$CONFIG_DIR/current_version" 7 | NTF_CONF="$CONFIG_DIR/notify.conf" 8 | INST_CONF="$CONFIG_DIR/install.conf" 9 | MIRROR_LIST="$CONFIG_DIR/proxies.txt" 10 | VALID_MIRRORS="$CONFIG_DIR/valid_proxies.txt" 11 | TMP_VALID_MIRRORS="/tmp/valid_mirrors.tmp" 12 | REMOTE_SCRIPTS_VERSION_FILE="$CONFIG_DIR/remote_ts_scripts_version" 13 | TIME_OUT=30 14 | 15 | # GitHub 代理模式配置 16 | set_direct_mode() { 17 | CUSTOM_RELEASE_PROXY="https://github.com" 18 | CUSTOM_RAW_PROXY="https://github.com" 19 | CUSTOM_API_PROXY="https://api.github.com" 20 | } 21 | 22 | set_proxy_mode() { 23 | CUSTOM_RELEASE_PROXY="https://gh.ch3ng.top" 24 | CUSTOM_RAW_PROXY="https://gh.ch3ng.top" 25 | CUSTOM_API_PROXY="https://ghapi.ch3ng.top" 26 | } 27 | 28 | # 根据配置自动设置模式 29 | apply_github_mode() { 30 | [ "$GITHUB_DIRECT" = "true" ] && set_direct_mode || set_proxy_mode 31 | } 32 | 33 | # 初始化日志系统 34 | log_info() { 35 | echo -n "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $1" 36 | [ $# -eq 2 ] || echo 37 | } 38 | 39 | log_warn() { 40 | echo -n "[$(date '+%Y-%m-%d %H:%M:%S')] [WARNING] $1" 41 | [ $# -eq 2 ] || echo 42 | } 43 | 44 | log_error() { 45 | echo -n "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $1" 46 | [ $# -eq 2 ] || echo 47 | } 48 | 49 | 50 | 51 | # 安全加载配置文件 52 | safe_source() { 53 | local file="$1" 54 | if [ -f "$file" ] && [ -s "$file" ]; then 55 | . "$file" 56 | else 57 | log_warn "⚠️ 配置文件 $file 不存在或为空" 58 | fi 59 | } 60 | 61 | webget() { 62 | # $1 输出文件 63 | # $2 URL 64 | # $3 是否静默: echooff 65 | # $4 禁止重定向: rediroff 66 | local outfile="$1" 67 | local url="$2" 68 | 69 | local ua="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0" 70 | 71 | # 是否静默 72 | local quiet="" 73 | [ "$3" = "echooff" ] && quiet="-s" 74 | 75 | # 是否禁用重定向 76 | local redirect="-L" 77 | [ "$4" = "rediroff" ] && redirect="" 78 | 79 | # ---- 优先使用 curl ---- 80 | if command -v curl >/dev/null 2>&1; then 81 | http_code=$(timeout "$TIME_OUT" curl $quiet $redirect \ 82 | -H "User-Agent: $ua" \ 83 | -w "%{http_code}" \ 84 | -o "$outfile" \ 85 | "$url" 2>/dev/null) 86 | 87 | case "$http_code" in 2*) return 0 ;; *) return 1 ;; esac 88 | fi 89 | 90 | # ---- 回退到 wget ---- 91 | if command -v wget >/dev/null 2>&1; then 92 | local q="--show-progress" 93 | [ "$3" = "echooff" ] && q="-q" 94 | 95 | local r="" 96 | [ "$4" = "rediroff" ] && r="--max-redirect=0" 97 | 98 | # wget 不直接返回 HTTP 状态码,需要解析 headers 99 | headers=$(mktemp) 100 | timeout "$TIME_OUT" wget $q $r \ 101 | --server-response --no-check-certificate \ 102 | --header="User-Agent: $ua" \ 103 | -O "$outfile" "$url" 2>"$headers" 104 | 105 | # 提取最后的 HTTP 状态码 106 | http_code=$(grep -oE 'HTTP/[0-9\.]+ [0-9]+' "$headers" | tail -n1 | awk '{print $2}') 107 | rm -f "$headers" 108 | 109 | case "$http_code" in 2*) return 0 ;; *) return 1 ;; esac 110 | fi 111 | 112 | log_error "❌ curl 和 wget 都不存在" 113 | return 1 114 | } 115 | 116 | # URL 编码函数 (POSIX 兼容) 117 | urlencode() { 118 | local str="$1" 119 | local encoded="" 120 | local i=0 121 | local length=${#str} 122 | while [ $i -lt $length ]; do 123 | local c=$(printf '%s' "$str" | cut -c$((i + 1))) 124 | case "$c" in 125 | [a-zA-Z0-9._~-]) 126 | encoded="${encoded}${c}" 127 | ;; 128 | *) 129 | encoded="${encoded}$(printf '%%%02X' "'$c")" 130 | ;; 131 | esac 132 | i=$((i + 1)) 133 | done 134 | printf '%s' "$encoded" 135 | } 136 | 137 | 138 | send_notify() { 139 | local host_name="$(uci get system.@system[0].hostname 2>/dev/null || echo OpenWrt)" 140 | local title="$host_name Tailscale通知" 141 | local user_title="$1" 142 | shift 143 | local body_content="$(printf "%s\n" "$@")" 144 | local content="$(printf "%s\n%s" "$user_title" "$body_content")" 145 | 146 | safe_source "$NTF_CONF" # 引入配置文件 147 | 148 | # 通用发送函数(curl 优先,wget 兼容) 149 | send_via_curl_or_wget() { 150 | local url="$1" 151 | local data="$2" 152 | local method="$3" 153 | local headers="$4" 154 | 155 | if command -v curl > /dev/null; then 156 | if [ "$method" = "POST" ]; then 157 | curl -sS -X POST "$url" -d "$data" -H "$headers" 158 | else 159 | curl -sS "$url" -d "$data" -H "$headers" 160 | fi 161 | elif command -v wget > /dev/null; then 162 | if [ "$method" = "POST" ]; then 163 | echo "$data" | wget --quiet --method=POST --body-file=- --header="$headers" "$url" 164 | else 165 | wget --quiet --post-data="$data" --header="$headers" "$url" 166 | fi 167 | else 168 | log_error "❌ curl 和 wget 都不可用,无法发送通知" 169 | return 1 170 | fi 171 | } 172 | 173 | # Server酱 174 | if [ "$NOTIFY_SERVERCHAN" = "1" ] && [ -n "$SERVERCHAN_KEY" ]; then 175 | data="text=$title&desp=$content" 176 | send_via_curl_or_wget "https://sctapi.ftqq.com/$SERVERCHAN_KEY.send" "$data" "POST" && log_info "✅ Server酱 通知已发送" 177 | fi 178 | 179 | # Bark 180 | if [ "$NOTIFY_BARK" = "1" ] && [ -n "$BARK_KEY" ]; then 181 | title_enc=$(urlencode "$title") 182 | content_enc=$(urlencode "$content") 183 | 184 | url="${BARK_KEY}/${title_enc}/${content_enc}" 185 | 186 | if command -v curl > /dev/null; then 187 | response=$(curl -sS "$url") 188 | if [ $? -eq 0 ]; then 189 | log_info "✅ Bark 通知已发送" 190 | else 191 | log_error "❌ 发送 Bark 通知失败,HTTP 状态码: $response" 192 | fi 193 | elif command -v wget > /dev/null; then 194 | if wget --quiet --output-document=/dev/null "$url"; then 195 | log_info "✅ Bark 通知已发送" 196 | else 197 | log_error "❌ 发送 Bark 通知失败,wget 返回错误" 198 | fi 199 | else 200 | log_error "❌ curl 和 wget 都不可用,无法发送 Bark 通知" 201 | fi 202 | fi 203 | 204 | # ntfy 205 | if [ "$NOTIFY_NTFY" = "1" ] && [ -n "$NTFY_KEY" ]; then 206 | headers="Title: $title" 207 | send_via_curl_or_wget "https://ntfy.sh/$NTFY_KEY" "$content" "POST" "$headers" && log_info "✅ NTFY 通知已发送" 208 | fi 209 | 210 | # 无任何通知方式启用 211 | if [ "$NOTIFY_SERVERCHAN" != "1" ] && [ "$NOTIFY_BARK" != "1" ] && [ "$NOTIFY_NTFY" != "1" ]; then 212 | log_error "❌ 未启用任何通知方式" 213 | fi 214 | } -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | clear 5 | . /etc/tailscale/tools.sh || { log_error "❌ 加载 tools.sh 失败"; exit 1; } 6 | log_info "加载公共函数..." 7 | 8 | log_info "加载配置文件..." 9 | safe_source "$INST_CONF" || log_warn "⚠️ INST_CONF 未找到或无效,使用默认配置" 10 | apply_github_mode 11 | 12 | GITHUB_API_RELEASE_LIST_URL_SUFFIX="repos/ch3ngyz/small-tailscale-openwrt/releases" 13 | 14 | get_arch() { 15 | arch_raw=$(uname -m) 16 | case "$arch_raw" in 17 | i386|i686) arch="386" ;; # 32位 x86 18 | x86_64) arch="amd64" ;; # 64位 x86 19 | 20 | armv7l|armv7|armhf|armv6l) arch="arm" ;; # 32位 ARM 21 | aarch64|arm64|armv8l) arch="arm64" ;; # 64位 ARM 22 | 23 | mips) arch="mips" ;; # 32位 MIPS big-endian 24 | mipsel|mipsel_24kc) arch="mipsle" ;; # 32位 MIPS little-endian 25 | mips64) arch="mips64" ;; # 64位 MIPS big-endian 26 | mips64el) arch="mips64le" ;; # 64位 MIPS little-endian 27 | 28 | *) 29 | echo "❌ 不支持的架构: $arch_raw, 请提交issue!" 30 | echo "https://github.com/CH3NGYZ/small-tailscale-openwrt/issues" 31 | exit 1 32 | ;; 33 | esac 34 | echo "$arch" 35 | } 36 | 37 | # 默认值 38 | MODE="" 39 | AUTO_UPDATE="" 40 | VERSION="latest" 41 | ARCH=$(get_arch) 42 | HOST_NAME=$(uci show system.@system[0].hostname | awk -F"'" '{print $2}') 43 | GITHUB_DIRECT=$GITHUB_DIRECT 44 | 45 | has_args=false # 🔧 新增:标记是否传入了参数 46 | if [ "$GITHUB_DIRECT" = "true" ] ; then 47 | GITHUB_DIRECT=true 48 | else 49 | GITHUB_DIRECT=false 50 | fi 51 | 52 | # 若有参数, 接受 --tmp为使用内存模式, --auto-update为自动更新 53 | while [ $# -gt 0 ]; do 54 | has_args=true # 🔧 有参数,关闭交互模式 55 | case "$1" in 56 | --tmp) MODE="tmp"; shift ;; 57 | --auto-update) AUTO_UPDATE=true; shift ;; 58 | --version=*) VERSION="${1#*=}"; shift ;; 59 | *) log_error "未知参数: $1"; exit 1 ;; 60 | esac 61 | done 62 | 63 | # 若无参数,进入交互模式 64 | if [ "$has_args" = false ]; then 65 | log_info 66 | log_info "📮 请选择安装 Tailscale 模式:" 67 | log_info " 1/y/Y/直接回车). 本地安装 🏠" 68 | log_info " 2/n/N ). 内存安装 💻" 69 | log_info " 0/e/E/其他字符). 退出安装 ⛔" 70 | log_info "⏳ 请输入选项: " 1 71 | read mode_input 72 | 73 | case "$mode_input" in 74 | 1|"y"|"Y"|"") MODE="local" ;; 75 | 2|"n"|"N") MODE="tmp" ;; 76 | *) log_error "❌ 已取消安装"; exit 1 ;; 77 | esac 78 | 79 | log_info 80 | log_info "🔄 是否启用 Tailscale 自动更新?" 81 | log_info " 1/y/Y/直接回车). 启用更新 ✅" 82 | log_info " 2/n/N ). 禁用更新 ❌" 83 | log_info " 0/e/E/其他字符). 退出安装 ⛔" 84 | log_info "⏳ 请输入选项: " 1 85 | read update_input 86 | 87 | case "$update_input" in 88 | 1|"y"|"Y"|"") AUTO_UPDATE=true ;; 89 | 2|"n"|"N") AUTO_UPDATE=false ;; 90 | *) log_error "⛔ 已取消安装"; exit 1 ;; 91 | esac 92 | log_info 93 | 94 | PAGE=1 95 | PER_PAGE=10 96 | 97 | while true; do 98 | clear 99 | log_info "🧩 正在拉取版本列表(第 $PAGE 页,每页 $PER_PAGE 条)..." 100 | 101 | API_URL="${CUSTOM_API_PROXY}/${GITHUB_API_RELEASE_LIST_URL_SUFFIX}?per_page=${PER_PAGE}&page=${PAGE}" 102 | retry=0 103 | while [ $retry -lt 3 ]; do 104 | if webget "/tmp/response.json" "$API_URL"; then 105 | break 106 | fi 107 | retry=$((retry + 1)) 108 | log_error "❌ 拉取失败($retry/3),重试中..." 109 | sleep 1 110 | done 111 | 112 | if [ $retry -ge 3 ]; then 113 | log_error "❌ 连续 3 次失败,取消操作" 114 | exit 1 115 | fi 116 | 117 | # 从返回解析 tags 118 | TAGS_TMP="/tmp/.tags.$$" 119 | if command -v jq >/dev/null 2>&1; then 120 | jq -r '.[].tag_name // empty' /tmp/response.json > "$TAGS_TMP" 121 | else 122 | grep -o '"tag_name"[ ]*:[ ]*"[^"]*"' /tmp/response.json \ 123 | | sed 's/.*"tag_name"[ ]*:[ ]*"\([^"]*\)".*/\1/' \ 124 | > "$TAGS_TMP" 125 | fi 126 | rm -f /tmp/response.json 127 | 128 | # 判断是否有 tags 129 | if [ ! -s "$TAGS_TMP" ]; then 130 | log_info "⚠️ 本页没有更多版本了" 131 | log_info "➡️ 输入 p 返回上一页,或 q 退出" 132 | read op 133 | case "$op" in 134 | p|P) [ "$PAGE" -gt 1 ] && PAGE=$((PAGE - 1)) ;; 135 | q|Q) exit 1 ;; 136 | esac 137 | continue 138 | fi 139 | 140 | # 展示本页 tags 141 | i=1 142 | log_info 143 | log_info "🔧 可用版本列表(第 $PAGE 页):" 144 | while read -r tag; do 145 | log_info " [$i] $tag" 146 | eval "TAG_$i=\"$tag\"" 147 | i=$((i + 1)) 148 | done < "$TAGS_TMP" 149 | total=$((i - 1)) 150 | 151 | log_info "" 152 | log_info "⏳ 输入序号选择版本(回车=最新,n=下一页,p=上一页,q=退出):" 1 153 | read input 154 | input=$(echo "$input" | xargs) 155 | 156 | case "$input" in 157 | "") # 直接回车 = 使用 latest 158 | VERSION="latest" 159 | break 160 | ;; 161 | q|Q) 162 | log_error "⛔ 已取消安装" 163 | exit 1 164 | ;; 165 | n|N) 166 | PAGE=$((PAGE + 1)) 167 | continue 168 | ;; 169 | p|P) 170 | [ "$PAGE" -gt 1 ] && PAGE=$((PAGE - 1)) 171 | continue 172 | ;; 173 | *) 174 | # 选择一个 tag 175 | if echo "$input" | grep -qE '^[0-9]+$' \ 176 | && [ "$input" -ge 1 ] \ 177 | && [ "$input" -le "$total" ]; then 178 | 179 | eval "VERSION=\$TAG_$input" 180 | log_info "✅ 使用指定版本: $VERSION" 181 | break 182 | else 183 | log_error "❌ 无效的选择" 184 | sleep 1 185 | fi 186 | ;; 187 | esac 188 | done 189 | rm -f "$TAGS_TMP" 190 | clear 191 | fi 192 | 193 | 194 | # 兜底 195 | MODE=${MODE:-local} 196 | AUTO_UPDATE=${AUTO_UPDATE:-false} 197 | VERSION=${VERSION:-latest} 198 | 199 | cat > "$INST_CONF" </dev/null || log_warn "⚠️ 停止 tailscaled 服务失败,继续清理残留文件" 226 | else 227 | log_warn "⚠️ 未找到 tailscale 服务文件,跳过停止服务步骤" 228 | fi 229 | 230 | # 清理残留文件 231 | log_info "🧹 清理残留文件..." 232 | if [ "$MODE" = "local" ]; then 233 | log_info "🗑️ 删除本地安装的残留文件..." 234 | rm -f /usr/local/bin/tailscale 235 | rm -f /usr/local/bin/tailscaled 236 | fi 237 | 238 | if [ "$MODE" = "tmp" ]; then 239 | log_info "🗑️ 删除/tmp中的残留文件..." 240 | rm -f /tmp/tailscale 241 | rm -f /tmp/tailscaled 242 | fi 243 | 244 | # 安装开始 245 | log_info "🚀 开始安装 Tailscale..." 246 | "$CONFIG_DIR/fetch_and_install.sh" \ 247 | --mode="$MODE" \ 248 | --version="$VERSION" \ 249 | --mirror-list="$VALID_MIRRORS" 250 | 251 | # 初始化服务 252 | log_info "🛠️ 初始化服务..." 253 | "$CONFIG_DIR/setup_service.sh" --mode="$MODE" 254 | 255 | # 设置定时任务 256 | log_info "⏰ 设置定时任务..." 257 | "$CONFIG_DIR/setup_cron.sh" --auto-update="$AUTO_UPDATE" 258 | -------------------------------------------------------------------------------- /pretest_mirrors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | [ -f /etc/tailscale/tools.sh ] && . /etc/tailscale/tools.sh && safe_source "$INST_CONF" 7 | 8 | TIME_OUT=20 9 | CONFIG_DIR="/etc/tailscale" 10 | INST_CONF="$CONFIG_DIR/install.conf" 11 | 12 | BIN_NAME="tailscaled-linux-amd64" 13 | BIN_PATH="/tmp/$BIN_NAME" 14 | SUM_NAME="SHA256SUMS.txt" 15 | SUM_PATH="/tmp/$SUM_NAME" 16 | 17 | BIN_URL_SUFFIX="CH3NGYZ/small-tailscale-openwrt/releases/latest/download/$BIN_NAME" 18 | SHA256SUMS_URL_SUFFIX="CH3NGYZ/small-tailscale-openwrt/releases/latest/download/$SUM_NAME" 19 | 20 | PROXIES_LIST_NAME="proxies.txt" 21 | PROXIES_LIST_PATH="$CONFIG_DIR/$PROXIES_LIST_NAME" 22 | PROXIES_LIST_URL_SUFFIX="CH3NGYZ/test-github-proxies/raw/refs/heads/main/$PROXIES_LIST_NAME" 23 | 24 | VALID_MIRRORS_PATH="$CONFIG_DIR/valid_proxies.txt" 25 | TMP_VALID_MIRRORS_PATH="/tmp/valid_mirrors.tmp" 26 | rm -f "$TMP_VALID_MIRRORS_PATH" 27 | touch "$TMP_VALID_MIRRORS_PATH" 28 | 29 | # ========= URL 配置 ========= 30 | set_direct_mode() { 31 | CUSTOM_RELEASE_PROXY="https://github.com" 32 | CUSTOM_RAW_PROXY="https://github.com" 33 | } 34 | 35 | set_proxy_mode() { 36 | CUSTOM_RELEASE_PROXY="https://gh.ch3ng.top" 37 | CUSTOM_RAW_PROXY="https://gh.ch3ng.top" 38 | } 39 | 40 | [ "$GITHUB_DIRECT" = "true" ] && set_direct_mode || set_proxy_mode 41 | 42 | 43 | # ========= 日志 ========= 44 | log_info() { 45 | echo -n "[$(date '+%Y-%m-%d %H:%M:%S')] [PRETEST] [INFO] $1" 46 | [ $# -eq 2 ] || echo 47 | } 48 | 49 | log_warn() { 50 | echo -n "[$(date '+%Y-%m-%d %H:%M:%S')] [PRETEST] [WARN] $1" 51 | [ $# -eq 2 ] || echo 52 | } 53 | 54 | log_error() { 55 | echo -n "[$(date '+%Y-%m-%d %H:%M:%S')] [PRETEST] [ERROR] $1" 56 | [ $# -eq 2 ] || echo 57 | } 58 | 59 | webget() { 60 | # $1 输出文件 61 | # $2 URL 62 | # $3 是否静默: echooff/echoon 63 | # $4 是否禁止重定向: rediroff 64 | 65 | local outfile="$1" 66 | local url="$2" 67 | 68 | # 控制输出 69 | local quiet="" 70 | [ "$3" = "echooff" ] && quiet="-s" || quiet="" 71 | 72 | # 控制重定向 73 | local redirect="-L" 74 | [ "$4" = "rediroff" ] && redirect="" 75 | 76 | if command -v curl >/dev/null 2>&1; then 77 | timeout "$TIME_OUT" curl $quiet $redirect -o "$outfile" -H "User-Agent: Mozilla" "$url" 78 | return $? 79 | fi 80 | 81 | if command -v wget >/dev/null 2>&1; then 82 | local q="--show-progress" 83 | [ "$3" = "echooff" ] && q="-q" 84 | 85 | local r="" 86 | [ "$4" = "rediroff" ] && r="--max-redirect=0" 87 | 88 | timeout "$TIME_OUT" wget $q $r --no-check-certificate -O "$outfile" "$url" 89 | return $? 90 | fi 91 | 92 | log_error "❌ curl 和 wget 都不存在" 93 | return 1 94 | } 95 | 96 | # ========= 下载尝试逻辑(可重建 URL) ========= 97 | rebuild_url() { 98 | case "$1" in 99 | sha) echo "${CUSTOM_RELEASE_PROXY}/${SHA256SUMS_URL_SUFFIX}" ;; 100 | proxies) echo "${CUSTOM_RAW_PROXY}/${PROXIES_LIST_URL_SUFFIX}" ;; 101 | esac 102 | } 103 | 104 | download_with_retry() { 105 | local dest="$1" type="$2" prefix="$3" 106 | local max_retry=3 107 | local attempt=1 108 | 109 | # 初次构建 URL(默认镜像) 110 | local url 111 | url="$(rebuild_url "$type")" 112 | 113 | while true; do 114 | attempt=1 115 | while [ $attempt -le $max_retry ]; do 116 | log_info "⏳ 下载 $type 文件 [$attempt/$max_retry]:$url → $dest" 117 | 118 | if webget "$dest" "$url" "echooff"; then 119 | return 0 120 | fi 121 | 122 | log_warn "❌ 下载 $type 失败 [$attempt/$max_retry]" 123 | attempt=$((attempt + 1)) 124 | done 125 | 126 | # ⛔ 走到这里说明同一个镜像 3 次都失败 127 | log_warn "⚠ 当前镜像连续 $max_retry 次下载失败,需重新配置镜像" 128 | 129 | local suffix 130 | case "$type" in 131 | sha) suffix="${SHA256SUMS_URL_SUFFIX}" ;; 132 | proxies) suffix="${PROXIES_LIST_URL_SUFFIX}" ;; 133 | esac 134 | 135 | if ! manual_fallback_with_reconfig "$prefix" "$suffix"; then 136 | log_error "❌ 镜像配置异常" 137 | exit 1 138 | fi 139 | 140 | # 重新配置镜像后重新生成URL 141 | url="$(rebuild_url "$type")" 142 | 143 | # 如果用户选择“强制直连”,manual_fallback_with_reconfig 会处理 144 | if [ "$GLOBAL_DIRECT_MODE" = "1" ]; then 145 | log_info "🔁 已进入直连模式,重试下载" 146 | fi 147 | done 148 | } 149 | 150 | force_direct_mode() { 151 | set_direct_mode 152 | sed -i -e '/^GITHUB_DIRECT=/d' -e '$aGITHUB_DIRECT=true' "$INST_CONF" 2>/dev/null || true 153 | : > "$VALID_MIRRORS_PATH" 154 | log_info "✅ 已切换到 GitHub 直连模式" 155 | } 156 | 157 | # ========= 手动选镜像 ========= 158 | manual_fallback_with_reconfig() { 159 | local prefix="$1" suffix="$2" 160 | log_info "镜像不可用,请选择:" 161 | log_info " 1) 手动输入镜像" 162 | log_info " 2) 强制直连" 163 | log_info " 3) 退出安装" 164 | 165 | while :; do 166 | log_info "请选择 1~3: " 1 167 | read -r choice || choice=2 168 | 169 | case "$choice" in 170 | 1) 171 | log_info "> 请输入您提供的镜像地址," 172 | log_info "> 镜像地址需要与 $suffix 拼凑后能下载此文件," 173 | log_info "> 且镜像地址以 https:// 开头, 以 / 结尾," 174 | log_info "> 例如: $prefix: " 1 175 | read -r input 176 | [ -z "$input" ] && continue 177 | case "$input" in 178 | http*://*) 179 | mirror="${input%/}" 180 | CUSTOM_RELEASE_PROXY="${mirror}" 181 | CUSTOM_RAW_PROXY="${mirror}" 182 | echo "$mirror" > "$VALID_MIRRORS_PATH" 183 | log_info "✅ 已切换至镜像:$mirror" 184 | return 0 ;; 185 | esac 186 | log_warn "❌ 无效地址" 187 | ;; 188 | 2) force_direct_mode; return 0 ;; 189 | 3) exit 10 ;; 190 | *) log_warn "⏳ 请输入 1~3: " 1;; 191 | esac 192 | done 193 | } 194 | 195 | # ========= STEP 1:下载校验文件 ========= $3 为 提示语中的例镜像 196 | download_with_retry "$SUM_PATH" sha "https://gh.ch3ng.top/" 197 | 198 | sha_expected="$(grep -E " ${BIN_NAME}$" "$SUM_PATH" | awk '{print $1}')" 199 | if [ -z "$sha_expected" ]; then 200 | log_error "❌ 校验文件格式异常" 201 | force_direct_mode 202 | exit 1 203 | fi 204 | 205 | # ========= STEP 2:下载代理列表 ========= $3 为 提示语中的例镜像 206 | download_with_retry "$PROXIES_LIST_PATH" proxies "https://gh.ch3ng.top/" 207 | log_info "✅ 代理列表下载成功" 208 | 209 | # ========= STEP 3:测速挑最快镜像 ========= 210 | log_info "⏳ 开始代理测速..." 211 | 212 | total=$(grep -cve '^\s*$' "$PROXIES_LIST_PATH") 213 | index=0 214 | 215 | test_mirror() { 216 | mirror="${1%/}/" 217 | progress="$2" 218 | 219 | local url="${mirror}${BIN_URL_SUFFIX}" 220 | log_info "⏳ 测试[$progress] $url" 221 | 222 | local start=$(date +%s.%N) 223 | if ! webget "$BIN_PATH" "$url" "echooff"; then 224 | log_warn "❌ 下载失败" 225 | return 226 | fi 227 | 228 | local sha_actual 229 | sha_actual=$(sha256sum "$BIN_PATH" | awk '{print $1}') 230 | 231 | if [ "$sha_expected" != "$sha_actual" ]; then 232 | log_warn "❌ SHA256 错误:$sha_actual" 233 | return 234 | fi 235 | 236 | local end=$(date +%s.%N) 237 | local cost=$(awk "BEGIN {printf \"%.2f\", $end - $start}") 238 | log_info "✅ 通过,用时 ${cost}s" 239 | 240 | echo "$cost $mirror" >> "$TMP_VALID_MIRRORS_PATH" 241 | } 242 | 243 | while read -r mirror; do 244 | case "$mirror" in http*) ;; *) continue ;; esac 245 | index=$((index+1)) 246 | test_mirror "$mirror" "$index/$total" 247 | 248 | valid_count=$(wc -l < "$TMP_VALID_MIRRORS_PATH") 249 | [ "$valid_count" -ge 3 ] && log_info "✅ 已找到 3 个有效代理,提前结束测速" && break 250 | done < "$PROXIES_LIST_PATH" 251 | 252 | rm -f "$BIN_PATH" 253 | 254 | # ========= STEP 4:保存最佳镜像 ========= 255 | if [ -s "$TMP_VALID_MIRRORS_PATH" ]; then 256 | sort -n "$TMP_VALID_MIRRORS_PATH" | awk '{print $2}' > "$VALID_MIRRORS_PATH" 257 | log_info "🏆 最佳镜像:$(head -n1 "$VALID_MIRRORS_PATH")" 258 | else 259 | log_info "❌ 未找到可用代理, 安装失败, 请考虑使用直连模式" 260 | exit 1 261 | fi 262 | 263 | rm -f "$TMP_VALID_MIRRORS_PATH" 264 | exit 0 -------------------------------------------------------------------------------- /scripts/helper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o pipefail 4 | 5 | SCRIPT_VERSION="v1.1.2" 6 | 7 | # 检查并引入 /etc/tailscale/tools.sh 文件 8 | [ -f /etc/tailscale/tools.sh ] && . /etc/tailscale/tools.sh 9 | safe_source "$INST_CONF" 10 | apply_github_mode 11 | 12 | HELPER_SCRIPT_URL_SUFFIX="CH3NGYZ/small-tailscale-openwrt/raw/refs/heads/main/scripts/helper.sh" 13 | INSTALL_SCRIPT_URL_SUFFIX="CH3NGYZ/small-tailscale-openwrt/raw/refs/heads/main/install.sh" 14 | 15 | get_download_tool() { 16 | if command -v curl > /dev/null 2>&1; then 17 | echo "curl" 18 | elif command -v wget > /dev/null 2>&1; then 19 | echo "wget" 20 | else 21 | log_info "❌ 没有找到 curl 或 wget, 无法下载或执行操作。" 22 | exit 1 23 | fi 24 | } 25 | 26 | # 获取可用的下载工具 27 | download_tool=$(get_download_tool) 28 | 29 | get_remote_version() { 30 | remote_ver_url="${CUSTOM_RAW_PROXY}/${HELPER_SCRIPT_URL_SUFFIX}" 31 | log_info "获取远程文件: ${remote_ver_url}" 32 | if [ "$download_tool" = "curl" ]; then 33 | # 设置 5 秒超时 34 | timeout 5 curl -sSL "$remote_ver_url" | grep -E '^SCRIPT_VERSION=' | cut -d'"' -f2 > "$REMOTE_SCRIPTS_VERSION_FILE" 35 | else 36 | # 设置 5 秒超时 37 | timeout 5 wget -qO- "$remote_ver_url" | grep -E '^SCRIPT_VERSION=' | cut -d'"' -f2 > "$REMOTE_SCRIPTS_VERSION_FILE" 38 | fi 39 | } 40 | 41 | # 显示菜单 42 | show_menu() { 43 | log_info "🎉 欢迎使用 Tailscale on OpenWRT 管理脚本 $SCRIPT_VERSION" 44 | if [ ! -s "$REMOTE_SCRIPTS_VERSION_FILE" ]; then 45 | log_info "⚠️ 无法获取远程脚本版本" 46 | else 47 | remote_version=$(cat "$REMOTE_SCRIPTS_VERSION_FILE") 48 | log_info "📦 远程脚本版本: $remote_version $( 49 | [ "$remote_version" != "$SCRIPT_VERSION" ] && echo '🚨脚本有更新, 请使用 13) 更新脚本包' || echo '✅已是最新' 50 | )" 51 | fi 52 | log_info "------------------------------------------" 53 | log_info " 1). 💾 安装 / 重装 Tailscale" 54 | log_info "------------------------------------------" 55 | log_info " 2). 📥 登录 Tailscale" 56 | log_info " 3). 📝 生成 Tailscale 启动命令" # 新增选项 57 | log_info " 4). 📤 登出 Tailscale" 58 | log_info " 5). ❌ 卸载 Tailscale" 59 | log_info "------------------------------------------" 60 | log_info " 6). 🔄 管理 Tailscale 自动更新" 61 | log_info " 7). 🔄 手动运行更新脚本" 62 | log_info " 8). 🔄 切换代理/直连状态" 63 | log_info " 9). 📦 查看本地 Tailscale 存在版本" 64 | log_info " 10). 📦 查看远程 Tailscale 最新版本" 65 | log_info " 11). 🔔 管理推送通知" 66 | log_info " 12). 📊 排序代理池" 67 | log_info " 13). 🛠️ 更新脚本包" 68 | log_info " 14). 📜 显示 Tailscale 更新日志" 69 | log_info "------------------------------------------" 70 | log_info " 0). ⛔ 退出" 71 | log_info "------------------------------------------" 72 | } 73 | 74 | # 处理用户选择 75 | handle_choice() { 76 | case $1 in 77 | 1) 78 | $CONFIG_DIR/setup.sh 79 | log_info "✅ 请按回车继续..." 1 80 | read _ 81 | ;; 82 | 2) 83 | if ! command -v tailscale >/dev/null 2>&1; then 84 | log_error "❌ tailscale 未安装或命令未找到" 85 | log_error "📦 请先安装 tailscale 后再运行本脚本" 86 | else 87 | local tmp_log="/tmp/tailscale_up.log" 88 | local pipe="/tmp/tailscale_up.pipe" 89 | 90 | : > "$tmp_log" 91 | [ -p "$pipe" ] && rm -f "$pipe" 92 | mkfifo "$pipe" 93 | 94 | log_info "🚀 执行 tailscale up, 正在生成登录链接..." 95 | 96 | # 后台运行 tailscale up 97 | ( 98 | tailscale up >"$tmp_log" 2>&1 99 | echo "__TS_UP_DONE__" >>"$tmp_log" 100 | ) & 101 | ts_up_pid=$! 102 | 103 | # 用 tail -F 写入命名管道 104 | tail -F "$tmp_log" >"$pipe" & 105 | tail_pid=$! 106 | 107 | auth_detected=false 108 | fail_detected=false 109 | 110 | while read -r line <"$pipe"; do 111 | echo "$line" | grep -qE "https://[^ ]*tailscale.com" && { 112 | auth_url=$(echo "$line" | grep -oE "https://[^ ]*tailscale.com[^ ]*") 113 | log_info "🔗 tailscale 等待认证, 请访问以下网址登录:$auth_url" 114 | auth_detected=true 115 | } 116 | 117 | echo "$line" | grep -qi "failed" && { 118 | log_error "❌ tailscale up 执行失败:$line" 119 | fail_detected=true 120 | break 121 | } 122 | 123 | echo "$line" | grep -q "__TS_UP_DONE__" && { 124 | if [ "$auth_detected" != "true" ] && [ "$fail_detected" != "true" ]; then 125 | if [ -s "$tmp_log" ]; then 126 | log_info "✅ tailscale up 执行完成:$(cat "$tmp_log")" 127 | else 128 | log_info "✅ tailscale up 执行完成, 无输出" 129 | fi 130 | fi 131 | break 132 | } 133 | done 134 | 135 | # 清理后台进程 136 | kill "$ts_up_pid" 2>/dev/null 137 | kill "$tail_pid" 2>/dev/null 138 | 139 | # 删除临时文件 140 | rm -f "$tmp_log" "$pipe" 141 | 142 | # 检查登录状态 143 | tailscale status >/dev/null 2>&1 144 | if [ $? -ne 0 ]; then 145 | log_error "⚠️ tailscale 未登录或状态异常" 146 | else 147 | log_info "🎉 tailscale 登录成功,状态正常" 148 | fi 149 | 150 | fi 151 | log_info "✅ 请按回车继续..." 1 152 | read _ 153 | ;; 154 | 3) 155 | $CONFIG_DIR/tailscale_up_generater.sh 156 | ;; 157 | 4) 158 | if ! command -v tailscale >/dev/null 2>&1; then 159 | log_error "❌ tailscale 未安装或命令未找到" 160 | log_error "📦 请先安装 tailscale 后再运行本脚本" 161 | else 162 | log_info "🔓 正在执行 tailscale logout..." 163 | 164 | if tailscale logout; then 165 | sleep 3 166 | if tailscale status 2>&1 | grep -q "Logged out."; then 167 | log_info "✅ 成功登出 tailscale" 168 | else 169 | log_error "⚠️ 登出后状态未知,请检查 tailscale status 状态" 170 | fi 171 | else 172 | log_error "❌ tailscale logout 命令执行失败" 173 | fi 174 | fi 175 | log_info "✅ 请按回车继续..." 1 176 | read _ 177 | ;; 178 | 5) 179 | $CONFIG_DIR/uninstall.sh 180 | log_info "✅ 请按回车退出..." 1 181 | read _ 182 | exit 0 183 | ;; 184 | 6) 185 | $CONFIG_DIR/update_ctl.sh 186 | log_info "✅ 请按回车继续..." 1 187 | read _ 188 | ;; 189 | 7) 190 | $CONFIG_DIR/autoupdate.sh 191 | log_info "✅ 请按回车继续..." 1 192 | read _ 193 | ;; 194 | 8) 195 | $CONFIG_DIR/github_direct_ctl.sh 196 | log_info "✅ 请按回车继续..." 1 197 | read _ 198 | ;; 199 | 9) 200 | if [ -f "$VERSION_FILE" ]; then 201 | log_info "📦 当前本地版本: $(cat "$VERSION_FILE")" 202 | else 203 | log_info "⚠️ 本地未记录版本信息, 可能未安装 Tailscale" 204 | fi 205 | log_info "✅ 请按回车继续..." 1 206 | read _ 207 | ;; 208 | 10) 209 | log_info "$($CONFIG_DIR/fetch_and_install.sh --dry-run)" 210 | log_info "✅ 请按回车继续..." 1 211 | read _ 212 | ;; 213 | 11) 214 | $CONFIG_DIR/notify_ctl.sh 215 | ;; 216 | 12) 217 | $CONFIG_DIR/test_mirrors.sh 218 | log_info "✅ 请按回车继续..." 1 219 | read _ 220 | ;; 221 | 13) 222 | URL="${CUSTOM_RAW_PROXY}/${INSTALL_SCRIPT_URL_SUFFIX}" 223 | tmpfile=$(mktemp) 224 | if [ "$download_tool" = "curl" ]; then 225 | curl -sSL "$URL" -o "$tmpfile" 226 | else 227 | wget -qO "$tmpfile" "$URL" 228 | fi 229 | if [ $? -ne 0 ]; then 230 | log_error "❌ 脚本下载失败, 脚本内置作者的代理失效" 231 | rm -f "$tmpfile" 232 | log_info "✅ 请按回车继续..." 1 233 | read _ 234 | else 235 | # exec 会替换当前进程,不会返回 236 | exec sh "$tmpfile" < /dev/tty 237 | fi 238 | ;; 239 | 14) 240 | # 检查日志文件是否存在 241 | if [ -f /tmp/tailscale_update.log ]; then 242 | # 如果文件存在,则显示日志内容 243 | log_info " 日志内容如下:" 244 | log_info " ---------------------------" 245 | cat /tmp/tailscale_update.log 246 | log_info " ---------------------------" 247 | else 248 | # 如果文件不存在,则提示用户日志文件未找到 249 | log_error "❌ 没有找到日志文件,更新脚本可能未执行!" 250 | fi 251 | log_info "✅ 请按回车继续..." 1 252 | read _ 253 | ;; 254 | 0) 255 | log_info "👋 退出脚本" 256 | sleep 1 257 | clear 258 | exit 0 259 | ;; 260 | *) 261 | log_info "❌ 无效选择, 请重新输入, 按回车继续..." 1 262 | read _ 263 | ;; 264 | esac 265 | } 266 | 267 | # 主循环前执行一次远程版本检测 268 | clear 269 | log_info "🔄 正在检测脚本更新, 最多需要 5 秒..." 270 | get_remote_version 271 | clear 272 | 273 | # 主循环 274 | while true; do 275 | show_menu 276 | log_info "✅ 请输入你的选择: " 1 277 | read choice 278 | log_info "" 279 | handle_choice "$choice" 280 | clear 281 | done 282 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | clear 5 | 6 | TIME_OUT=20 7 | CONFIG_DIR="/etc/tailscale" 8 | mkdir -p "$CONFIG_DIR" 9 | INST_CONF="$CONFIG_DIR/install.conf" 10 | 11 | if [ -f /tmp/tailscale-use-direct ]; then 12 | rm -f /tmp/tailscale-use-direct 13 | echo "GITHUB_DIRECT=true" > "$INST_CONF" 14 | GITHUB_DIRECT=true 15 | CUSTOM_RAW_PROXY="https://github.com" 16 | else 17 | echo "GITHUB_DIRECT=false" > "$INST_CONF" 18 | GITHUB_DIRECT=false 19 | CUSTOM_RAW_PROXY="https://gh.ch3ng.top" 20 | fi 21 | 22 | SCRIPTS_TGZ_PATH="/tmp/tailscale-openwrt-scripts.tar.gz" 23 | SCRIPTS_TGZ_URL_SUFFIX="CH3NGYZ/small-tailscale-openwrt/raw/refs/heads/main/tailscale-openwrt-scripts.tar.gz" 24 | PRETEST_MIRRORS_SH_URL_SUFFIX="CH3NGYZ/small-tailscale-openwrt/raw/refs/heads/main/pretest_mirrors.sh" 25 | 26 | # 预先计算的校验和 27 | EXPECTED_CHECKSUM_SHA256="37ae1127e425beb9350508373931757effaa51717eebb9a900169ce289a3ff86" 28 | EXPECTED_CHECKSUM_MD5="7ad83e165744523668ce051b0833293c" 29 | 30 | log_info() { 31 | echo -n "[$(date '+%Y-%m-%d %H:%M:%S')] [INSTALL] [INFO] $1" 32 | [ $# -eq 2 ] || echo 33 | } 34 | 35 | log_warn() { 36 | echo -n "[$(date '+%Y-%m-%d %H:%M:%S')] [INSTALL] [WARN] $1" 37 | [ $# -eq 2 ] || echo 38 | } 39 | 40 | log_error() { 41 | echo -n "[$(date '+%Y-%m-%d %H:%M:%S')] [INSTALL] [ERROR] $1" 42 | [ $# -eq 2 ] || echo 43 | } 44 | 45 | if ! command -v opkg >/dev/null 2>&1; then 46 | log_error "❌ 未检测到 opkg 命令,当前系统可能不是 OpenWRT 或缺少包管理器" 47 | log_error "❌ 无法继续执行安装脚本" 48 | exit 1 49 | fi 50 | 51 | sync_time() { 52 | log_info "正在同步系统时间..." 53 | # 尝试多个常见 NTP 服务器,直到成功 54 | for server in ntp.aliyun.com time1.cloud.tencent.com pool.ntp.org; do 55 | if ntpdate -u "$server" >/dev/null 2>&1 || ntpd -q -n -p "$server" >/dev/null 2>&1; then 56 | log_info "时间同步成功($server)" 57 | return 0 58 | fi 59 | done 60 | log_warn "所有 NTP 服务器都失败,尝试使用 HTTP 头时间" 61 | http_time=$(curl -I -s --connect-timeout 5 https://www.baidu.com | grep -i '^date:' | awk '{print $3,$4,$5,$6,$7}') 62 | [ -n "$http_time" ] && date -D "%d %b %Y %H:%M:%S %Z" -s "$http_time" && log_info "已用 HTTP 头设置时间" 63 | } 64 | sync_time 65 | 66 | # 检查是否已经安装所有必要软件包 67 | required_packages="libustream-openssl ca-bundle kmod-tun coreutils-timeout coreutils-nohup curl jq" 68 | need_install=0 69 | 70 | # 一次性获取已安装包列表(性能优化) 71 | installed_packages=$(opkg list-installed) 72 | 73 | # 如果已安装 libustream-mbedtls,则跳过 libustream-openssl 74 | skip_openssl=0 75 | if echo "$installed_packages" | grep -q "^libustream-mbedtls"; then 76 | skip_openssl=1 77 | fi 78 | 79 | for package in $required_packages; do 80 | # 跳过 openssl 版本,仅标记,不输出日志 81 | if [ "$skip_openssl" -eq 1 ] && [ "$package" = "libustream-openssl" ]; then 82 | continue 83 | fi 84 | 85 | if ! echo "$installed_packages" | grep -q "^$package"; then 86 | log_warn "⚠️ 包 $package 未安装" 87 | need_install=1 88 | fi 89 | done 90 | 91 | if [ "$need_install" -eq 0 ]; then 92 | log_info "✅ 已安装所有必要组件" 93 | else 94 | log_info "🔄 正在更新 opkg 源..." 95 | if ! opkg update 2>&1; then 96 | log_error "⚠️ opkg update 失败,请检查网络连接或源配置,继续执行..." 97 | else 98 | log_info "✅ opkg update 成功" 99 | fi 100 | 101 | for package in $required_packages; do 102 | # 在安装流程中才输出跳过提示 103 | if [ "$skip_openssl" -eq 1 ] && [ "$package" = "libustream-openssl" ]; then 104 | log_info "✅ 检测到 libustream-mbedtls,跳过 libustream-openssl" 105 | continue 106 | fi 107 | 108 | if ! echo "$installed_packages" | grep -q "^$package"; then 109 | log_warn "⚠️ 包 $package 未安装,开始安装..." 110 | if opkg install "$package" 2>&1; then 111 | log_info "✅ 包 $package 安装成功" 112 | else 113 | # ★ 针对 jq 的特殊跳过逻辑 ★ 114 | if [ "$package" = "jq" ]; then 115 | log_warn "⚠️ 安装 jq 失败,将使用回退解析方式,继续执行" 116 | continue 117 | fi 118 | 119 | # 针对 coreutils 的替代逻辑 120 | if [ "$package" = "coreutils-timeout" ] || [ "$package" = "coreutils-nohup" ]; then 121 | alt="coreutils" 122 | log_warn "⚠️ 安装 $package 失败,尝试安装 $alt 替代..." 123 | if opkg install $alt 2>&1; then 124 | log_info "✅ $alt 安装成功,可能已包含 $(echo $package | cut -d- -f2) 命令" 125 | continue 126 | fi 127 | fi 128 | 129 | log_error "❌ 安装 $package 失败,无法继续,请手动安装此包" 130 | exit 1 131 | fi 132 | fi 133 | done 134 | 135 | # 最终检查命令可用性 136 | for cmd in timeout nohup curl jq; do 137 | if ! command -v $cmd >/dev/null 2>&1; then 138 | log_error "❌ 未检测到 $cmd 命令,请手动安装后重新执行脚本" 139 | exit 1 140 | else 141 | log_info "✅ $cmd 命令已可用" 142 | fi 143 | done 144 | fi 145 | 146 | # 校验函数, 接收三个参数:文件路径、校验类型(sha256/md5)、预期值 147 | verify_checksum() { 148 | local file=$1 149 | local type=$2 150 | local expected=$3 151 | local actual="" 152 | 153 | case "$type" in 154 | sha256) 155 | if command -v sha256sum >/dev/null 2>&1; then 156 | actual=$(sha256sum "$file" | awk '{print $1}') 157 | elif command -v openssl >/dev/null 2>&1; then 158 | actual=$(openssl dgst -sha256 "$file" | awk '{print $2}') 159 | else 160 | log_error "❌ 系统缺少 sha256sum 或 openssl, 无法校验文件" 161 | return 1 162 | fi 163 | ;; 164 | md5) 165 | if command -v md5sum >/dev/null 2>&1; then 166 | actual=$(md5sum "$file" | awk '{print $1}') 167 | elif command -v openssl >/dev/null 2>&1; then 168 | actual=$(openssl dgst -md5 "$file" | awk '{print $2}') 169 | else 170 | log_error "❌ 系统缺少 md5sum 或 openssl, 无法校验文件" 171 | return 1 172 | fi 173 | ;; 174 | *) 175 | log_error "❌ 校验类型无效: $type" 176 | return 1 177 | ;; 178 | esac 179 | 180 | # 校验结果对比 181 | if [ "$actual" != "$expected" ]; then 182 | log_error "❌ 校验失败!预期: $expected, 实际: $actual" 183 | return 1 184 | fi 185 | 186 | return 0 187 | } 188 | 189 | webget() { 190 | # $1 输出文件 191 | # $2 URL 192 | # $3 是否静默: echooff/echoon 193 | # $4 是否禁止重定向: rediroff 194 | 195 | local outfile="$1" 196 | local url="$2" 197 | 198 | # 控制输出 199 | local quiet="" 200 | [ "$3" = "echooff" ] && quiet="-s" || quiet="" 201 | 202 | # 控制重定向 203 | local redirect="-L" 204 | [ "$4" = "rediroff" ] && redirect="" 205 | 206 | if command -v curl >/dev/null 2>&1; then 207 | timeout "$TIME_OUT" curl $quiet $redirect -o "$outfile" -H "User-Agent: Mozilla" "$url" 208 | return $? 209 | fi 210 | 211 | if command -v wget >/dev/null 2>&1; then 212 | local q="--show-progress" 213 | [ "$3" = "echooff" ] && q="-q" 214 | 215 | local r="" 216 | [ "$4" = "rediroff" ] && r="--max-redirect=0" 217 | 218 | timeout "$TIME_OUT" wget $q $r --no-check-certificate -O "$outfile" "$url" 219 | return $? 220 | fi 221 | 222 | log_error "❌ curl 和 wget 都不存在" 223 | return 1 224 | } 225 | 226 | scripts_tgz_url="${CUSTOM_RAW_PROXY}/${SCRIPTS_TGZ_URL_SUFFIX}" 227 | 228 | if webget "$SCRIPTS_TGZ_PATH" "$scripts_tgz_url" "echooff"; then 229 | log_info "📥 下载成功: $scripts_tgz_url" 230 | else 231 | log_error "❌ 下载失败" 232 | exit 1 233 | fi 234 | 235 | sha_ok=0 236 | md5_ok=0 237 | 238 | if verify_checksum "$SCRIPTS_TGZ_PATH" "sha256" "$EXPECTED_CHECKSUM_SHA256"; then 239 | log_info "🔐 SHA256 校验通过" 240 | sha_ok=1 241 | else 242 | log_warn "⚠️ SHA256 校验失败 (忽略, 尝试 MD5)" 243 | fi 244 | 245 | if verify_checksum "$SCRIPTS_TGZ_PATH" "md5" "$EXPECTED_CHECKSUM_MD5"; then 246 | log_info "🔐 MD5 校验通过" 247 | md5_ok=1 248 | else 249 | log_warn "⚠️ MD5 校验失败" 250 | fi 251 | 252 | if [ $sha_ok -eq 1 ] || [ $md5_ok -eq 1 ]; then 253 | log_info "✅ 下载脚本包 + 校验成功!" 254 | else 255 | log_error "❌ 校验失败,安装中止" 256 | exit 1 257 | fi 258 | 259 | # 解压脚本 260 | log_info "📦 解压脚本包..." 261 | tar -xzf "$SCRIPTS_TGZ_PATH" -C "$CONFIG_DIR" 262 | 263 | # 设置权限 264 | chmod +x "$CONFIG_DIR"/*.sh 265 | 266 | # 创建helper的软连接 267 | ln -sf "$CONFIG_DIR/helper.sh" /usr/bin/tailscale-helper 268 | 269 | # 检查软链接是否创建成功 270 | if [ -L /usr/bin/tailscale-helper ]; then 271 | log_info "✅ 软连接已成功创建:$CONFIG_DIR/helper.sh -> /usr/bin/tailscale-helper" 272 | else 273 | log_error "❌ 创建软连接失败" 274 | fi 275 | 276 | # 初始化通知配置 277 | [ -f "$CONFIG_DIR/notify.conf" ] || cat > "$CONFIG_DIR/notify.conf" <<'EOF' 278 | # 通知开关 (1=启用 0=禁用) 279 | NOTIFY_UPDATE=1 280 | NOTIFY_MIRROR_FAIL=1 281 | NOTIFY_EMERGENCY=1 282 | 283 | NOTIFY_SERVERCHAN=0 284 | SERVERCHAN_KEY="" 285 | NOTIFY_BARK=0 286 | BARK_KEY="" 287 | NOTIFY_NTFY=0 288 | NTFY_KEY="" 289 | EOF 290 | 291 | 292 | run_pretest_mirrors() { 293 | pretest_mirrors_sh_url="${CUSTOM_RAW_PROXY}/${PRETEST_MIRRORS_SH_URL_SUFFIX}" 294 | log_info "🔄 下载 $pretest_mirrors_sh_url 并执行测速..." 295 | if webget "/tmp/pretest_mirrors.sh" "$pretest_mirrors_sh_url" "echooff"; then 296 | sh /tmp/pretest_mirrors.sh 297 | else 298 | log_info "❌ 下载 pretest_mirrors.sh 失败, 请重试!" 299 | return 1 300 | fi 301 | } 302 | 303 | if [ "$GITHUB_DIRECT" = "true" ] ; then 304 | log_info "✅ 使用Github直连, 跳过测速!" 305 | else 306 | if [ ! -f /etc/tailscale/proxies.txt ]; then 307 | log_info "🔍 本地不存在 proxies.txt, 将下载镜像列表并测速, 请等待..." 308 | run_pretest_mirrors 309 | ret=$? 310 | if [ $ret -eq 0 ]; then 311 | log_info "✅ 下载镜像列表并测速完成!" 312 | elif [ $ret -eq 10 ]; then 313 | log_info "👋 用户取消安装" 314 | exit 0 315 | elif [ $ret -eq 1 ]; then 316 | log_info "❌ 下载或测速失败, 无法继续!" 317 | exit 1 318 | else 319 | log_error "❌ 下载或测速失败, 无法继续!" 320 | exit 1 321 | fi 322 | else 323 | log_info "✅ 本地存在 proxies.txt, 无需再次下载!" 324 | fi 325 | fi 326 | 327 | log_info "✅ 配置工具安装完毕!" 328 | log_info "✅ 运行 tailscale-helper 可以打开功能菜单" 329 | log_info "👋 回车直接执行, 输入其他字符退出: " 1 330 | read choice 331 | if [ -z "$choice" ]; then 332 | tailscale-helper 333 | else 334 | log_info "👋 退出脚本....." 335 | sleep 1 336 | clear 337 | exit 0 338 | fi 339 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Sync and Build Tailscale 2 | 3 | on: 4 | schedule: 5 | - cron: '10 23 * * *' 6 | workflow_dispatch: 7 | inputs: 8 | versions: 9 | description: 'Tailscale versions (e.g. 1.76.1-1.78.0,1.79.2 — leave empty for latest)' 10 | required: false 11 | watch: 12 | types: [started] 13 | 14 | permissions: 15 | contents: write 16 | 17 | env: 18 | REPO_OFFICIAL: 'tailscale/tailscale' 19 | BUILD_PLATFORMS: 'amd64 386 arm arm64 mips mipsle mips64 mips64le' 20 | GO_BUILD_FLAGS: "-tags ts_include_cli -ldflags='-s -w'" 21 | 22 | jobs: 23 | prepare_versions: 24 | runs-on: ubuntu-latest 25 | outputs: 26 | matrix: ${{ steps.set_matrix.outputs.matrix }} 27 | steps: 28 | - name: Fetch all releases from tailscale official repo 29 | id: fetch_releases 30 | run: | 31 | curl -s "https://api.github.com/repos/${{ env.REPO_OFFICIAL }}/releases?per_page=1000" | \ 32 | jq -r '.[].tag_name' | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' > all_releases.txt 33 | 34 | - name: Parse user input and build matrix 35 | id: set_matrix 36 | run: | 37 | echo "===> 输入参数: ${{ github.event.inputs.versions }}" 38 | input="${{ github.event.inputs.versions }}" 39 | 40 | # 获取本地仓库已有 release 列表 41 | existing_releases=$(curl -s "https://api.github.com/repos/${{ github.repository }}/releases" | jq -r '.[].tag_name' | sed 's/^v//') 42 | echo "===> 本地已有 release: " 43 | echo "$existing_releases" 44 | echo "====> 开始处理版本范围" 45 | 46 | declare -A versions 47 | 48 | if [ -z "$input" ]; then 49 | # 未提供输入,默认取最新 tag 50 | tag=$(head -n1 all_releases.txt) 51 | tag=${tag#v} # 去除前缀 v 52 | if echo "$existing_releases" | grep -qx "$tag"; then 53 | echo "release $tag 已存在" 54 | echo 'matrix=["skip"]' >> "$GITHUB_OUTPUT" 55 | exit 0 56 | fi 57 | echo "未提供输入,默认取第一个 release: $tag" 58 | echo "matrix=$(jq -cn --arg v "$tag" '[ $v ]')" >> "$GITHUB_OUTPUT" 59 | exit 0 60 | fi 61 | 62 | echo "===> 开始解析版本范围:$input" 63 | 64 | IFS=',' read -ra parts <<< "$input" 65 | for part in "${parts[@]}"; do 66 | if [[ "$part" == *"-"* ]]; then 67 | start=${part%-*} 68 | end=${part#*-} 69 | echo " 发现范围: $start 到 $end" 70 | matched=$(sed 's/^v//' all_releases.txt | sort -V | awk -v s="$start" -v e="$end" '$0 >= s && $0 <= e') 71 | echo " 匹配结果:$(echo "$matched" | xargs)" 72 | for v in $matched; do 73 | # 跳过已有 release 74 | if echo "$existing_releases" | grep -qx "$v"; then 75 | echo " 跳过已有 release: $v" 76 | continue 77 | fi 78 | versions["v$v"]=1 79 | done 80 | else 81 | echo " 发现单一版本: $part" 82 | if echo "$existing_releases" | grep -qx "$part"; then 83 | echo " 跳过已有 release: $part" 84 | continue 85 | fi 86 | versions["v$part"]=1 87 | fi 88 | done 89 | 90 | echo "===> 收集到版本(未排序):" 91 | for v in "${!versions[@]}"; do 92 | echo " - $v" 93 | done 94 | 95 | all_versions_sorted=$(for v in "${!versions[@]}"; do echo "$v" | sed 's/^v//'; done | sort -V) 96 | echo "===> 排序后版本:" 97 | echo "$all_versions_sorted" 98 | 99 | # 如果排序后的版本为空,写入占位 skip 100 | if [ -z "$all_versions_sorted" ]; then 101 | echo "===> 没有可用版本,生成占位 skip" 102 | jq_array=$(jq -cn --arg v skip '[ $v ]') 103 | else 104 | jq_array=$(printf '%s\n' $all_versions_sorted | jq -R . | jq -s .) 105 | fi 106 | 107 | echo "===> 生成 matrix:" 108 | echo "$jq_array" | jq -c 109 | echo "matrix=$(echo "$jq_array" | jq -c)" >> "$GITHUB_OUTPUT" 110 | echo "====> 结束处理版本范围" 111 | 112 | 113 | build_and_release: 114 | needs: prepare_versions 115 | runs-on: ubuntu-latest 116 | strategy: 117 | matrix: 118 | version: ${{ fromJson(needs.prepare_versions.outputs.matrix) }} 119 | fail-fast: true 120 | max-parallel: 16 121 | env: 122 | TAG: v${{ matrix.version }} 123 | steps: 124 | - name: Exit if placeholder 125 | run: | 126 | if [ "${TAG}" = "vskip" ]; then 127 | echo "No versions to build. Exiting." 128 | echo "skip=true" >> $GITHUB_ENV 129 | exit 0 130 | else 131 | echo "skip=false" >> $GITHUB_ENV 132 | fi 133 | 134 | - name: Check if release exists 135 | id: check_release 136 | if: env.skip != 'true' 137 | run: | 138 | REPO="${{ github.repository }}" 139 | TAG="v${{ matrix.version }}" 140 | TOKEN="${{ secrets.GITHUB_TOKEN }}" 141 | 142 | # 查询 Release 143 | RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \ 144 | -H "Authorization: token $TOKEN" \ 145 | -H "Accept: application/vnd.github+json" \ 146 | "https://api.github.com/repos/$REPO/releases/tags/$TAG") 147 | 148 | if [ "$RESPONSE" -eq 200 ]; then 149 | echo "Release $TAG already exists" 150 | echo "skip=true" >> $GITHUB_ENV 151 | else 152 | echo "Release $TAG does not exist" 153 | echo "skip=false" >> $GITHUB_ENV 154 | fi 155 | 156 | - name: Checkout Tailscale source 157 | uses: actions/checkout@v4 158 | if: env.skip != 'true' 159 | with: 160 | repository: ${{ env.REPO_OFFICIAL }} 161 | ref: ${{ env.TAG }} 162 | 163 | - name: Setup Go environment 164 | uses: actions/setup-go@v5 165 | if: env.skip != 'true' 166 | with: 167 | go-version: 'stable' 168 | 169 | - name: Install latest UPX 170 | if: env.skip != 'true' 171 | run: | 172 | set -e 173 | LATEST_UPX=$(curl -s https://api.github.com/repos/upx/upx/releases/latest | grep -oP '"tag_name": "v\K[0-9.]+' | head -n1) 174 | echo "Latest UPX version: $LATEST_UPX" 175 | curl -LO "https://github.com/upx/upx/releases/download/v${LATEST_UPX}/upx-${LATEST_UPX}-amd64_linux.tar.xz" 176 | tar -xf "upx-${LATEST_UPX}-amd64_linux.tar.xz" 177 | sudo cp "upx-${LATEST_UPX}-amd64_linux/upx" /usr/local/bin/ 178 | upx --version 179 | 180 | - name: Build optimized binaries 181 | if: env.skip != 'true' 182 | run: | 183 | echo "${{ env.TAG }}" > version.txt 184 | mkdir -p ./build 185 | for arch in ${{ env.BUILD_PLATFORMS }}; do 186 | echo "Building for $arch..." 187 | binary_name="./build/tailscaled-linux-${arch}.build" 188 | 189 | export CGO_ENABLED=0 190 | export GOOS=linux 191 | export GOARCH=$arch 192 | 193 | case "$arch" in 194 | mips|mipsle) 195 | export GOMIPS=softfloat 196 | ;; 197 | esac 198 | 199 | go build -o "$binary_name" ${{ env.GO_BUILD_FLAGS }} ./cmd/tailscaled 200 | 201 | if [ ! -f "$binary_name" ]; then 202 | echo "❌ 编译失败: $binary_name 不存在" 203 | exit 1 204 | fi 205 | done 206 | 207 | - name: Compress binaries 208 | if: env.skip != 'true' 209 | run: | 210 | mkdir -p ./upx_compress 211 | for arch in ${{ env.BUILD_PLATFORMS }}; do 212 | binary_name="./build/tailscaled-linux-${arch}.build" 213 | compressed_name="./upx_compress/tailscaled-linux-${arch}" 214 | 215 | case "$arch" in 216 | mips64|mips64le) 217 | echo "⚠️ 跳过 $arch 的 UPX 压缩" 218 | cp "$binary_name" "$compressed_name" 219 | ;; 220 | *) 221 | if [[ -f "$binary_name" ]]; then 222 | upx --lzma --best --no-progress "$binary_name" -o "$compressed_name" 223 | else 224 | echo "❌ 编译失败: $binary_name 不存在" 225 | fi 226 | ;; 227 | esac 228 | done 229 | 230 | - name: Generate checksums 231 | if: env.skip != 'true' 232 | run: | 233 | sha256sum ./build/tailscaled-* > SHA256SUMS.txt 234 | md5sum ./build/tailscaled-* > MD5SUMS.txt 235 | sha256sum ./upx_compress/tailscaled-* >> SHA256SUMS.txt 236 | md5sum ./upx_compress/tailscaled-* >> MD5SUMS.txt 237 | 238 | sed -i 's|./build/||g' SHA256SUMS.txt 239 | sed -i 's|./build/||g' MD5SUMS.txt 240 | sed -i 's|./upx_compress/||g' SHA256SUMS.txt 241 | sed -i 's|./upx_compress/||g' MD5SUMS.txt 242 | sed -i 's/ */ /g' SHA256SUMS.txt 243 | sed -i 's/ */ /g' MD5SUMS.txt 244 | 245 | cat SHA256SUMS.txt 246 | cat MD5SUMS.txt 247 | 248 | - name: Wait previous version release 249 | if: env.skip != 'true' 250 | run: | 251 | # echo "=== 当前 Matrix 版本列表 ===" 252 | # echo '${{ needs.prepare_versions.outputs.matrix }}' | jq -r '.[]' 253 | 254 | # 解析 matrix 为 bash 数组 255 | readarray -t matrix_versions < <(echo '${{ needs.prepare_versions.outputs.matrix }}' | jq -r '.[]') 256 | 257 | # 找出当前版本在 matrix 中的前一个版本 258 | prev_version="" 259 | for i in "${!matrix_versions[@]}"; do 260 | if [ "${matrix_versions[$i]}" == "${{ matrix.version }}" ]; then 261 | if [ $i -gt 0 ]; then 262 | prev_version="${matrix_versions[$i-1]}" 263 | fi 264 | break 265 | fi 266 | done 267 | 268 | if [ -z "$prev_version" ]; then 269 | echo "当前版本是最早版本,无需等待,直接发布 ${{ matrix.version }}" 270 | exit 0 271 | fi 272 | 273 | echo "当前版本之前的 Matrix 版本: $prev_version" 274 | 275 | # 循环等待,直到该版本 release 已存在 276 | until curl -s -f "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/tags/v$prev_version" >/dev/null 2>&1; do 277 | echo "⏳ 等待前一个版本 v$prev_version 的 release 创建..." 278 | sleep 5 279 | done 280 | 281 | echo "✅ 前一个版本 v$prev_version 的 release 已存在 — 可以发布当前版本 ${{ matrix.version }}" 282 | 283 | - name: Create Release 284 | uses: softprops/action-gh-release@v1 285 | if: env.skip != 'true' 286 | with: 287 | tag_name: ${{ env.TAG }} 288 | name: 'Tailscale ${{ env.TAG }}' 289 | body: | 290 | ## 🚀 Tailscale ${{ env.TAG }} UPX压缩版发布 291 | 292 | [![Downloads](https://img.shields.io/github/downloads/CH3NGYZ/small-tailscale-openwrt/${{ env.TAG }}/total)](https://github.com/CH3NGYZ/small-tailscale-openwrt/releases/${{ env.TAG }}) 293 | 294 | 🧩 **支持架构**: 295 | `${{ env.BUILD_PLATFORMS }}` 296 | 297 | ⚙️ **内容说明**: 298 | - 每个架构只生成一个联合二进制文件,通过改名即可分别作为`tailscale`(前台程序)和`tailscaled`(后台程序)运行。 299 | - *注意:由于 UPX[目前不支持, 也没有计划支持](https://github.com/upx/upx/issues/272#issuecomment-1250010942)`mips64`与`mips64le`架构, 此release中这两个架构的可执行文件为`未压缩`的状态* 300 | 301 | 📄 **校验方式**: 302 | 把 tailscale-linux-* 和校验文件放在同一个目录下,执行: 303 | ```bash 304 | sha256sum -c SHA256SUMS.txt 305 | md5sum -c MD5SUMS.txt 306 | ``` 307 | 308 | 🔧 **快速启动**: 309 | ```bash 310 | chmod +x /path/to/tailscaled-linux-amd64 311 | ln -s /path/to/tailscaled-linux-amd64 /usr/bin/tailscale 312 | ln -s /path/to/tailscaled-linux-amd64 /usr/bin/tailscaled 313 | tailscaled & # 服务进程需后台运行 314 | tailscale up # 等待服务进程运行几秒之后再运行(等待服务进程与tailscale服务器连接) 315 | ``` 316 | 317 | 📦 **附带文件**: 318 | | 文件名 | 说明 | 319 | |----------------------|-----------------------| 320 | | tailscaled-linux-ARCH | 不同架构已压缩的可执行文件 | 321 | | tailscaled-linux-ARCH.build | 不同架构未压缩的可执行文件 | 322 | | SHA256SUMS.txt | SHA256 校验文件 | 323 | | MD5SUMS.txt | MD5 校验文件 | 324 | | version.txt | 当前版本 | 325 | 326 | 📜 **更新日志**: 327 | [点此查看](https://tailscale.com/changelog) 328 | 329 | draft: false 330 | prerelease: false 331 | files: | 332 | ./build/tailscaled-* 333 | ./upx_compress/tailscaled-* 334 | version.txt 335 | SHA256SUMS.txt 336 | MD5SUMS.txt --------------------------------------------------------------------------------