From 2f118eb2b3aa0b8094174fb783b2cf50b2c028c2 Mon Sep 17 00:00:00 2001 From: okxlin <61420215+okxlin@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:07:05 +0800 Subject: [PATCH] feat(headscale): add staged 0.29.2 upgrade Add 0.28.0 as the required intermediate release, migrate removed 0.29 configuration safely, preserve policy behavior, and harden the container to the upstream permission model. --- apps/headscale/0.28.0/.env.sample | 3 + apps/headscale/0.28.0/data.yml | 35 ++ apps/headscale/0.28.0/data/config/config.yaml | 76 ++++ apps/headscale/0.28.0/data/config/derp.yaml | 33 ++ apps/headscale/0.28.0/data/data/db.sqlite | 0 apps/headscale/0.28.0/docker-compose.yml | 28 ++ apps/headscale/0.28.0/scripts/init.sh | 4 + apps/headscale/0.28.0/scripts/uninstall.sh | 4 + apps/headscale/0.28.0/scripts/upgrade.sh | 372 ++++++++++++++++++ apps/headscale/0.29.2/.env.sample | 3 + apps/headscale/0.29.2/data.yml | 35 ++ apps/headscale/0.29.2/data/config/config.yaml | 78 ++++ apps/headscale/0.29.2/data/config/derp.yaml | 33 ++ .../0.29.2/data/config/policy.hujson | 3 + apps/headscale/0.29.2/data/data/db.sqlite | 0 apps/headscale/0.29.2/docker-compose.yml | 28 ++ apps/headscale/0.29.2/scripts/init.sh | 4 + apps/headscale/0.29.2/scripts/uninstall.sh | 4 + apps/headscale/0.29.2/scripts/upgrade.sh | 200 ++++++++++ apps/headscale/README.md | 35 +- 20 files changed, 976 insertions(+), 2 deletions(-) create mode 100644 apps/headscale/0.28.0/.env.sample create mode 100644 apps/headscale/0.28.0/data.yml create mode 100644 apps/headscale/0.28.0/data/config/config.yaml create mode 100644 apps/headscale/0.28.0/data/config/derp.yaml create mode 100644 apps/headscale/0.28.0/data/data/db.sqlite create mode 100644 apps/headscale/0.28.0/docker-compose.yml create mode 100755 apps/headscale/0.28.0/scripts/init.sh create mode 100755 apps/headscale/0.28.0/scripts/uninstall.sh create mode 100755 apps/headscale/0.28.0/scripts/upgrade.sh create mode 100644 apps/headscale/0.29.2/.env.sample create mode 100644 apps/headscale/0.29.2/data.yml create mode 100644 apps/headscale/0.29.2/data/config/config.yaml create mode 100644 apps/headscale/0.29.2/data/config/derp.yaml create mode 100644 apps/headscale/0.29.2/data/config/policy.hujson create mode 100644 apps/headscale/0.29.2/data/data/db.sqlite create mode 100644 apps/headscale/0.29.2/docker-compose.yml create mode 100755 apps/headscale/0.29.2/scripts/init.sh create mode 100755 apps/headscale/0.29.2/scripts/uninstall.sh create mode 100755 apps/headscale/0.29.2/scripts/upgrade.sh diff --git a/apps/headscale/0.28.0/.env.sample b/apps/headscale/0.28.0/.env.sample new file mode 100644 index 000000000..0ef8723cb --- /dev/null +++ b/apps/headscale/0.28.0/.env.sample @@ -0,0 +1,3 @@ +CONTAINER_NAME="headscale" +PANEL_APP_PORT_HTTP="40183" +SUBNET="172.18.0.241" diff --git a/apps/headscale/0.28.0/data.yml b/apps/headscale/0.28.0/data.yml new file mode 100644 index 000000000..a6c909b07 --- /dev/null +++ b/apps/headscale/0.28.0/data.yml @@ -0,0 +1,35 @@ +additionalProperties: + formFields: + - default: 40183 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: Port (Corresponding to internal 8080) + labelZh: 端口 (对应内部 8080) + label: + en: 'Port (Corresponding to internal 8080)' + zh: '端口 (对应内部 8080)' + zh-Hant: '埠(對應內部 8080)' + ja: 'ポート(内部 8080 に対応)' + ko: '포트(내부 8080에 대응)' + ru: 'Порт (соответствует внутреннему 8080)' + ms: 'Port (sepadan dengan dalaman 8080)' + pt-br: 'Porta (correspondente à interna 8080)' + required: true + rule: paramPort + type: number + - default: 172.18.0.241 + edit: true + envKey: SUBNET + labelEn: 1panel-network Subnet IP (View the docker network to obtain the CIDR block) + labelZh: 1panel-network 子网 IP (查看docker网络获取网段) + label: + en: '1panel-network Subnet IP (View the docker network to obtain the CIDR block)' + zh: '1panel-network 子网 IP (查看docker网络获取网段)' + zh-Hant: '1panel-network 子網 IP(查看 docker 網路取得網段)' + ja: '1panel-network サブネット IP(docker ネットワークで CIDR ブロックを確認)' + ko: '1panel-network 서브넷 IP(도커 네트워크에서 CIDR 블록 확인)' + ru: 'IP подсети 1panel-network (посмотрите сеть Docker, чтобы получить блок CIDR)' + ms: 'IP subnet 1panel-network (lihat rangkaian docker untuk mendapatkan blok CIDR)' + pt-br: 'IP de sub-rede 1panel-network (veja a rede docker para obter o bloco CIDR)' + required: true + type: text diff --git a/apps/headscale/0.28.0/data/config/config.yaml b/apps/headscale/0.28.0/data/config/config.yaml new file mode 100644 index 000000000..eb4aa6dc0 --- /dev/null +++ b/apps/headscale/0.28.0/data/config/config.yaml @@ -0,0 +1,76 @@ +--- +server_url: https://myheadscale.example.com:443 # Change to your public Headscale URL. +listen_addr: 0.0.0.0:8080 +metrics_listen_addr: 0.0.0.0:9090 +grpc_listen_addr: 0.0.0.0:50443 +grpc_allow_insecure: false + +noise: + private_key_path: /var/lib/headscale/noise_private.key + +prefixes: + v4: 100.64.0.0/10 + v6: fd7a:115c:a1e0::/48 + allocation: sequential + +derp: + server: + enabled: false + region_id: 999 + region_code: headscale + region_name: Headscale Embedded DERP + verify_clients: true + stun_listen_addr: 0.0.0.0:3478 + private_key_path: /var/lib/headscale/derp_server_private.key + automatically_add_embedded_derp_region: true + urls: + - https://controlplane.tailscale.com/derpmap/default + paths: [] + auto_update_enabled: true + update_frequency: 24h + +disable_check_updates: true +ephemeral_node_inactivity_timeout: 30m + +database: + type: sqlite + sqlite: + path: /var/lib/headscale/db.sqlite + write_ahead_log: true + +acme_url: https://acme-v02.api.letsencrypt.org/directory +acme_email: "" +tls_letsencrypt_hostname: "" +tls_letsencrypt_cache_dir: /var/lib/headscale/cache +tls_letsencrypt_challenge_type: HTTP-01 +tls_letsencrypt_listen: ":http" +tls_cert_path: "" +tls_key_path: "" + +log: + format: text + level: info + +policy: + mode: file + path: "" + +dns: + override_local_dns: false + nameservers: + global: + - 223.5.5.5 + - 1.1.1.1 + split: {} + search_domains: [] + extra_records: [] + magic_dns: true + base_domain: example.com + +unix_socket: /var/run/headscale/headscale.sock +unix_socket_permission: "0770" + +logtail: + enabled: false + +randomize_client_port: true diff --git a/apps/headscale/0.28.0/data/config/derp.yaml b/apps/headscale/0.28.0/data/config/derp.yaml new file mode 100644 index 000000000..269030a40 --- /dev/null +++ b/apps/headscale/0.28.0/data/config/derp.yaml @@ -0,0 +1,33 @@ +# /etc/headscale/derp.yaml +regions: + 900: + regionid: 900 + regioncode: thk + regionname: Tencent Hongkong + nodes: + - name: 900a + regionid: 900 + hostname: xxxx + ipv4: xxxx #非必须 + stunport: 3478 + stunonly: false + derpport: 40184 + - name: 900b + regionid: 900 + hostname: xxxx + ipv4: xxxx + stunport: 3478 + stunonly: false + derpport: 12345 + 901: + regionid: 901 + regioncode: tsh + regionname: Tencent Shanghai + nodes: + - name: 901a + regionid: 901 + hostname: xxxx + ipv4: xxxx + stunport: 3478 + stunonly: false + derpport: 40184 diff --git a/apps/headscale/0.28.0/data/data/db.sqlite b/apps/headscale/0.28.0/data/data/db.sqlite new file mode 100644 index 000000000..e69de29bb diff --git a/apps/headscale/0.28.0/docker-compose.yml b/apps/headscale/0.28.0/docker-compose.yml new file mode 100644 index 000000000..19d2c78cb --- /dev/null +++ b/apps/headscale/0.28.0/docker-compose.yml @@ -0,0 +1,28 @@ +services: + headscale: + container_name: ${CONTAINER_NAME} + restart: always + networks: + 1panel-network: + ipv4_address: ${SUBNET} + ports: + - "${PANEL_APP_PORT_HTTP}:8080" + read_only: true + tmpfs: + - /var/run/headscale + volumes: + - "./data/config:/etc/headscale:ro" + - "./data/data:/var/lib/headscale" + healthcheck: + test: ["CMD", "headscale", "health"] + interval: 10s + timeout: 5s + retries: 5 + command: ['serve'] + image: headscale/headscale:v0.28.0 + labels: + createdBy: "Apps" + +networks: + 1panel-network: + external: true diff --git a/apps/headscale/0.28.0/scripts/init.sh b/apps/headscale/0.28.0/scripts/init.sh new file mode 100755 index 000000000..3f6405b91 --- /dev/null +++ b/apps/headscale/0.28.0/scripts/init.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +mkdir -p ./data/config ./data/data diff --git a/apps/headscale/0.28.0/scripts/uninstall.sh b/apps/headscale/0.28.0/scripts/uninstall.sh new file mode 100755 index 000000000..8a45fbb68 --- /dev/null +++ b/apps/headscale/0.28.0/scripts/uninstall.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +docker compose down --remove-orphans diff --git a/apps/headscale/0.28.0/scripts/upgrade.sh b/apps/headscale/0.28.0/scripts/upgrade.sh new file mode 100755 index 000000000..8fbf67bc6 --- /dev/null +++ b/apps/headscale/0.28.0/scripts/upgrade.sh @@ -0,0 +1,372 @@ +#!/usr/bin/env bash +set -euo pipefail + +CONFIG_FILE="${CONFIG_FILE:-./data/config/config.yaml}" + +clean_value() { + local value="$1" + value="${value%% #*}" + value="${value%\"}" + value="${value#\"}" + value="${value%\'}" + value="${value#\'}" + printf '%s\n' "$value" +} + +yaml_quote() { + local value="$1" + value="${value//\\/\\\\}" + value="${value//\"/\\\"}" + printf '"%s"' "$value" +} + +scalar_any() { + local key="$1" + local default="$2" + local value + value="$(sed -n -E "s/^[[:space:]]*${key}:[[:space:]]*//p" "$CONFIG_FILE" | head -n 1 || true)" + if [[ -n "$value" ]]; then + clean_value "$value" + else + printf '%s\n' "$default" + fi +} + +section_scalar() { + local section="$1" + local key="$2" + local default="$3" + local value + value="$(awk -v section="$section" -v key="$key" ' + /^[[:space:]]*#/ { next } + /^[^[:space:]][^:]*:/ { + in_section = ($1 == section ":") + next + } + in_section && $0 ~ "^[[:space:]]+" key ":[[:space:]]*" { + sub("^[[:space:]]+" key ":[[:space:]]*", "") + print + exit + } + ' "$CONFIG_FILE" || true)" + if [[ -n "$value" ]]; then + clean_value "$value" + else + printf '%s\n' "$default" + fi +} + +first_prefix() { + local family="$1" + local default="$2" + local value + value="$(section_scalar prefixes "$family" "")" + if [[ -z "$value" && "$family" == "v4" ]]; then + value="$(awk ' + /^[[:space:]]*#/ { next } + /^[^[:space:]][^:]*:/ { + in_prefixes = ($1 == "ip_prefixes:") + next + } + in_prefixes && /^[[:space:]]*-[[:space:]]*[0-9]+\./ { + sub(/^[[:space:]]*-[[:space:]]*/, "") + print + exit + } + ' "$CONFIG_FILE" || true)" + elif [[ -z "$value" ]]; then + value="$(awk ' + /^[[:space:]]*#/ { next } + /^[^[:space:]][^:]*:/ { + in_prefixes = ($1 == "ip_prefixes:") + next + } + in_prefixes && /^[[:space:]]*-[[:space:]]*[0-9A-Fa-f:]+\/[0-9]+/ && $0 !~ /\./ { + sub(/^[[:space:]]*-[[:space:]]*/, "") + print + exit + } + ' "$CONFIG_FILE" || true)" + fi + if [[ -n "$value" ]]; then + clean_value "$value" + else + printf '%s\n' "$default" + fi +} + +dns_global_lines() { + local lines + lines="$(awk ' + /^[[:space:]]*#/ { next } + /^[^[:space:]][^:]*:/ { + if (in_dns && $1 != "dns_config:" && $1 != "dns:") exit + in_dns = ($1 == "dns_config:" || $1 == "dns:") + in_nameservers = 0 + in_global = 0 + next + } + in_dns && /^[[:space:]]+nameservers:[[:space:]]*(#.*)?$/ { + in_nameservers = 1 + next + } + in_nameservers && /^[[:space:]]+[a-zA-Z_]+:[[:space:]]*/ && $1 != "global:" { + exit + } + in_nameservers && $1 == "global:" { + in_global = 1 + next + } + in_nameservers && in_global && /^[[:space:]]+[a-zA-Z_]+:[[:space:]]*/ { + exit + } + in_nameservers && /^[[:space:]]*-[[:space:]]*/ { + sub(/^[[:space:]]*-[[:space:]]*/, "") + sub(/[[:space:]]+#.*$/, "") + gsub(/^["'\''"]|["'\''"]$/, "") + print " - " $0 + } + ' "$CONFIG_FILE" || true)" + if [[ -n "$lines" ]]; then + printf '%s\n' "$lines" + else + printf '%s\n' " - 223.5.5.5" " - 1.1.1.1" + fi +} + +dns_child_block() { + local old_key="$1" + local new_key="$2" + local extra_indent="$3" + local default="$4" + local block + block="$(awk -v old_key="$old_key" -v new_key="$new_key" -v extra_indent="$extra_indent" ' + /^[[:space:]]*#/ { + if (found) { + print extra_indent $0 + } + next + } + /^[^[:space:]][^:]*:/ { + if (found) { + exit + } + in_dns = ($1 == "dns_config:" || $1 == "dns:") + next + } + in_dns && !found && $0 ~ "^[[:space:]][[:space:]]" old_key ":[[:space:]]*" { + found = 1 + sub("^[[:space:]][[:space:]]" old_key ":", " " new_key ":") + print extra_indent $0 + next + } + found { + if ($0 ~ "^[[:space:]][[:space:]][A-Za-z_][A-Za-z0-9_]*:[[:space:]]*" && $0 !~ "^[[:space:]][[:space:]][[:space:]][[:space:]]") { + exit + } + print extra_indent $0 + } + ' "$CONFIG_FILE" || true)" + if [[ -n "$block" ]]; then + printf '%s\n' "$block" + else + printf '%s\n' "$default" + fi +} + +app_root_from_config() { + local config_dir + local data_dir + config_dir="$(dirname "$CONFIG_FILE")" + data_dir="$(dirname "$config_dir")" + + if [[ "$(basename "$config_dir")" == "config" && "$(basename "$data_dir")" == "data" ]]; then + dirname "$data_dir" + else + printf '.\n' + fi +} + +sqlite_host_path() { + local container_path="$1" + case "$container_path" in + /var/lib/headscale/*) + printf '%s/data/data/%s\n' "$APP_ROOT" "${container_path#/var/lib/headscale/}" + ;; + /*) + printf '%s\n' "$container_path" + ;; + *) + printf '%s/%s\n' "$APP_ROOT" "$container_path" + ;; + esac +} + +backup_sqlite_database() { + local db_type="$1" + local db_path="$2" + local normalized_type="${db_type,,}" + + if [[ "$normalized_type" == "postgres" || "$normalized_type" == "postgresql" ]]; then + return 0 + fi + + local host_path + host_path="$(sqlite_host_path "$db_path")" + if [[ ! -f "$host_path" ]]; then + echo "SQLite database ${host_path} not found; skipped database backup" + return 0 + fi + + local backup_dir + backup_dir="$(dirname "$host_path")/backup-$(date +%Y%m%d%H%M%S)" + mkdir -p "$backup_dir" + + local copied=0 + local candidate + for candidate in "$host_path" "$host_path-wal" "$host_path-shm"; do + if [[ -f "$candidate" ]]; then + cp "$candidate" "$backup_dir/" + copied=1 + fi + done + + if [[ "$copied" -eq 1 ]]; then + echo "Backed up Headscale SQLite database files to ${backup_dir}" + else + rmdir "$backup_dir" 2>/dev/null || true + fi +} + +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "$CONFIG_FILE not found; skipped Headscale config migration" + exit 0 +fi + +if ! grep -qE '^(acl_policy_path|dns_config:|ip_prefixes:|db_type:|db_path:|db_host:|db_port:|db_name:|db_user:|db_pass:|db_ssl:|private_key_path:|node_update_check_interval:)' "$CONFIG_FILE"; then + echo "Headscale config already uses the 0.27+ layout; skipped migration" + exit 0 +fi + +backup="${CONFIG_FILE}.bak-$(date +%Y%m%d%H%M%S)" +cp "$CONFIG_FILE" "$backup" +APP_ROOT="$(app_root_from_config)" + +prefix_v4="$(first_prefix v4 "100.64.0.0/10")" +prefix_v6="$(first_prefix v6 "fd7a:115c:a1e0::/48")" +db_type="$(scalar_any db_type "$(section_scalar database type "sqlite")")" +db_path="$(scalar_any db_path "$(section_scalar sqlite path "/var/lib/headscale/db.sqlite")")" +db_host="$(scalar_any db_host "localhost")" +db_port="$(scalar_any db_port "5432")" +db_name="$(scalar_any db_name "headscale")" +db_user="$(scalar_any db_user "foo")" +db_pass="$(scalar_any db_pass "bar")" +db_ssl="$(scalar_any db_ssl "false")" +policy_path="$(scalar_any acl_policy_path "$(section_scalar policy path "")")" +dns_override="$(scalar_any override_local_dns "false")" +dns_magic="$(scalar_any magic_dns "true")" +dns_base="$(scalar_any base_domain "example.com")" +dns_nameservers="$(dns_global_lines)" +dns_split="$(dns_child_block restricted_nameservers split " " " split: {}")" +dns_search_domains="$(dns_child_block domains search_domains "" " search_domains: []")" +dns_extra_records="$(dns_child_block extra_records extra_records "" " extra_records: []")" + +backup_sqlite_database "$db_type" "$db_path" + +tmp="${CONFIG_FILE}.tmp-$$" +awk ' + function is_top_level_key() { + return $0 ~ /^[^[:space:]#][^:]*:/ + } + + skip_old_block && is_top_level_key() { + skip_old_block = 0 + } + + !skip_old_block && $0 ~ /^(private_key_path|node_update_check_interval|db_type|db_path|db_host|db_port|db_name|db_user|db_pass|db_ssl|acl_policy_path):/ { + next + } + + !skip_old_block && $0 ~ /^(ip_prefixes|dns_config):/ { + skip_old_block = 1 + next + } + + !skip_old_block { + print + } +' "$CONFIG_FILE" > "$tmp" +mv "$tmp" "$CONFIG_FILE" + +append_prefixes() { + grep -qE '^prefixes:' "$CONFIG_FILE" && return 0 + cat >> "$CONFIG_FILE" <> "$CONFIG_FILE" <> "$CONFIG_FILE" <> "$CONFIG_FILE" <> "$CONFIG_FILE" < "$output_file" + return 0 + fi + + awk -v timeout="$timeout" ' + function is_top_level(line) { + return line ~ /^[^[:space:]#][^:]*:/ + } + in_node && is_top_level($0) && !inserted { + print " ephemeral:" + print " inactivity_timeout: " timeout + inserted = 1 + in_node = 0 + } + { print } + $0 ~ /^node:[[:space:]]*($|#)/ { in_node = 1 } + END { + if (in_node && !inserted) { + print " ephemeral:" + print " inactivity_timeout: " timeout + } + } + ' "$source_file" > "$output_file" + return 0 + fi + + cp "$source_file" "$output_file" + cat >> "$output_file" <&2 + exit 1 + fi + if [[ -n "$policy_path" ]]; then + echo "Headscale 0.29 moves randomize_client_port into the policy. Add randomizeClientPort to ${policy_path} before upgrading, then remove randomize_client_port from config.yaml." >&2 + exit 1 + fi + set_default_policy=1 + ;; +esac + +backup="${CONFIG_FILE}.bak-$(date +%Y%m%d%H%M%S)" +if [[ -e "$backup" ]]; then + backup="${backup}-$$" +fi +cp -p "$CONFIG_FILE" "$backup" + +base_tmp="${CONFIG_FILE}.base-$$" +final_tmp="${CONFIG_FILE}.tmp-$$" +policy_tmp="${policy_file}.tmp-$$" +cleanup() { + rm -f "$base_tmp" "$final_tmp" "$policy_tmp" +} +trap cleanup EXIT + +awk -v set_default_policy="$set_default_policy" ' + /^[^[:space:]#][^:]*:/ { in_policy = ($1 == "policy:") } + /^(randomize_client_port|ephemeral_node_inactivity_timeout):[[:space:]]*/ { next } + set_default_policy && in_policy && /^[[:space:]]+path:[[:space:]]*/ { + print " path: /etc/headscale/policy.hujson" + path_set = 1 + next + } + { print } + END { + if (set_default_policy && !path_set) { + exit 42 + } + } +' "$CONFIG_FILE" > "$base_tmp" || { + echo "Could not migrate policy.path in $CONFIG_FILE; restored original configuration" >&2 + exit 1 +} + +if [[ -n "$ephemeral_timeout" ]]; then + ensure_node_timeout "$base_tmp" "$final_tmp" "$ephemeral_timeout" +else + cp "$base_tmp" "$final_tmp" +fi + +if [[ "$set_default_policy" -eq 1 ]]; then + expected_policy=$'{\n "randomizeClientPort": true,\n}\n' + if [[ -e "$policy_file" ]]; then + if [[ "$(< "$policy_file")" != "${expected_policy%$'\n'}" ]]; then + echo "$policy_file already exists; add randomizeClientPort to that policy manually before upgrading." >&2 + exit 1 + fi + else + printf '%s' "$expected_policy" > "$policy_tmp" + chmod 600 "$policy_tmp" + mv "$policy_tmp" "$policy_file" + fi +fi + +chmod --reference="$CONFIG_FILE" "$final_tmp" +mv "$final_tmp" "$CONFIG_FILE" +echo "Migrated Headscale configuration for 0.29; backup: ${backup}" diff --git a/apps/headscale/README.md b/apps/headscale/README.md index 0af78d2a5..0b9db57a2 100644 --- a/apps/headscale/README.md +++ b/apps/headscale/README.md @@ -5,12 +5,31 @@ Tailscale 控制服务器的开源自托管实现。 英文说明:An open source, self-hosted implementation of the Tailscale control server. +## 产品介绍 +Headscale 是 Tailscale 控制服务器的开源自托管实现,用于集中管理自有 Tailnet 节点。 + +## 主要功能 +- 管理用户、节点、预授权密钥和 API 密钥。 +- 提供 ACL、MagicDNS、DERP 配置和节点注册能力。 +- 使用 SQLite 持久化控制面数据,配置和数据目录可备份迁移。 + +## 访问说明 +安装完成后,通过表单中的 `PANEL_APP_PORT_HTTP` 访问 Headscale HTTP 服务;CLI 管理命令可在 1Panel 容器终端中执行。 + +## Introduction +Headscale is an open-source, self-hosted implementation of the Tailscale control server for managing a private Tailnet. + +## Features +- Manage users, nodes, pre-authentication keys, and API keys. +- Provide ACL, MagicDNS, DERP, and node registration capabilities. +- Persist control-plane data in SQLite with backup-friendly configuration and data directories. + ## 部署说明 - 本应用使用 Docker Compose 在 1Panel 中部署。 - 应用分类:工具。 - 支持架构:amd64。 -- 可选版本:`0.23.0-alpha3`、`0.26.1`、`0.27.1`。 -- 安装后按应用表单中的端口访问 Web UI、SSH 或对应服务。 +- 可选版本:`0.23.0-alpha3`、`0.26.1`、`0.27.1`、`0.28.0`、`0.29.2`。 +- 安装后通过应用表单中的端口访问 Headscale HTTP 控制服务。 ## 端口 | 变量 | 说明 | 默认值 | 必填 | @@ -29,6 +48,18 @@ Tailscale 控制服务器的开源自托管实现。 - `0.27.1` 最低支持的 Tailscale 客户端版本为 `v1.64.0`,旧客户端需要先升级。 - 版本目录内的 `scripts/upgrade.sh` 会自动备份并迁移旧版 `config.yaml`;如手动改过 Postgres、DNS、ACL 等配置,升级后请核对 `database`、`dns`、`policy` 配置块。 +升级到 `0.29.2` 前请特别注意: + +- Headscale 强制按次版本顺序升级。商店内必须依次完成 `0.27.1 -> 0.28.0 -> 0.29.2`,不能从 `0.27.1` 直接跳到 `0.29.2`。 +- `0.28.0` 最低支持 Tailscale 客户端 `v1.74.0`,`0.29.2` 最低支持 `v1.80.0`;请先升级所有客户端。 +- 每一步升级前都应备份 `./data/config`,以及 `db.sqlite`、`db.sqlite-wal`、`db.sqlite-shm`(如存在)。`0.28.0` 会执行节点标签、预授权密钥和数据库结构迁移。 +- `0.29.x` 改变了 ACL 通配符和主机名处理规则。使用自定义 ACL、非标准地址段或 `proto:icmp` 的用户必须先核对官方变更说明。 +- `0.29.2` 的升级脚本会备份 `config.yaml`,并将旧的 `randomize_client_port: true` 迁移到 `policy.hujson`。如果已经使用自定义文件或数据库策略,脚本会停止而不会覆盖策略;请先在现有策略中加入 `randomizeClientPort`,再移除旧配置项后重试。 + +## 安全说明 +- `0.28.0` 和 `0.29.2` 按上游容器部署方式使用只读根文件系统、只读配置挂载和临时 `/var/run/headscale`。 +- 新版本不再申请 `NET_ADMIN`、`NET_RAW`、`SYS_MODULE`,也不设置容器网络转发 sysctl;Headscale 是控制面服务,不承担客户端数据面转发。 + ## 配置项 | 变量 | 说明 | 默认值 | 必填 | | --- | --- | --- | --- |