mirror of
https://github.com/okxlin/appstore.git
synced 2026-09-23 00:00:59 +00:00
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.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
CONTAINER_NAME="headscale"
|
||||
PANEL_APP_PORT_HTTP="40183"
|
||||
SUBNET="172.18.0.241"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p ./data/config ./data/data
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
docker compose down --remove-orphans
|
||||
Executable
+372
@@ -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" <<EOF
|
||||
|
||||
prefixes:
|
||||
v4: ${prefix_v4}
|
||||
v6: ${prefix_v6}
|
||||
allocation: sequential
|
||||
EOF
|
||||
}
|
||||
|
||||
append_database() {
|
||||
grep -qE '^database:' "$CONFIG_FILE" && return 0
|
||||
local normalized_type="${db_type,,}"
|
||||
if [[ "$normalized_type" == "postgres" || "$normalized_type" == "postgresql" ]]; then
|
||||
cat >> "$CONFIG_FILE" <<EOF
|
||||
|
||||
database:
|
||||
type: postgres
|
||||
postgres:
|
||||
host: $(yaml_quote "$db_host")
|
||||
port: ${db_port}
|
||||
name: $(yaml_quote "$db_name")
|
||||
user: $(yaml_quote "$db_user")
|
||||
pass: $(yaml_quote "$db_pass")
|
||||
ssl: ${db_ssl}
|
||||
EOF
|
||||
else
|
||||
cat >> "$CONFIG_FILE" <<EOF
|
||||
|
||||
database:
|
||||
type: sqlite
|
||||
sqlite:
|
||||
path: $(yaml_quote "$db_path")
|
||||
write_ahead_log: true
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
append_policy() {
|
||||
grep -qE '^policy:' "$CONFIG_FILE" && return 0
|
||||
cat >> "$CONFIG_FILE" <<EOF
|
||||
|
||||
policy:
|
||||
mode: file
|
||||
path: "${policy_path}"
|
||||
EOF
|
||||
}
|
||||
|
||||
append_dns() {
|
||||
grep -qE '^dns:' "$CONFIG_FILE" && return 0
|
||||
cat >> "$CONFIG_FILE" <<EOF
|
||||
|
||||
dns:
|
||||
override_local_dns: ${dns_override}
|
||||
nameservers:
|
||||
global:
|
||||
${dns_nameservers}
|
||||
${dns_split}
|
||||
${dns_search_domains}
|
||||
${dns_extra_records}
|
||||
magic_dns: ${dns_magic}
|
||||
base_domain: ${dns_base}
|
||||
EOF
|
||||
}
|
||||
|
||||
append_prefixes
|
||||
append_database
|
||||
append_policy
|
||||
append_dns
|
||||
|
||||
echo "Migrated Headscale config for 0.27+ layout; backup: ${backup}"
|
||||
@@ -0,0 +1,3 @@
|
||||
CONTAINER_NAME="headscale"
|
||||
PANEL_APP_PORT_HTTP="40183"
|
||||
SUBNET="172.18.0.241"
|
||||
@@ -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
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
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
|
||||
|
||||
node:
|
||||
expiry: 0
|
||||
ephemeral:
|
||||
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: /etc/headscale/policy.hujson
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"randomizeClientPort": true,
|
||||
}
|
||||
@@ -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.29.2
|
||||
labels:
|
||||
createdBy: "Apps"
|
||||
|
||||
networks:
|
||||
1panel-network:
|
||||
external: true
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p ./data/config ./data/data
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
docker compose down --remove-orphans
|
||||
Executable
+200
@@ -0,0 +1,200 @@
|
||||
#!/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"
|
||||
}
|
||||
|
||||
top_level_scalar() {
|
||||
local key="$1"
|
||||
local value
|
||||
value="$(sed -n -E "s/^${key}:[[:space:]]*//p" "$CONFIG_FILE" | head -n 1 || true)"
|
||||
clean_value "$value"
|
||||
}
|
||||
|
||||
section_scalar() {
|
||||
local section="$1"
|
||||
local key="$2"
|
||||
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)"
|
||||
clean_value "$value"
|
||||
}
|
||||
|
||||
ensure_node_timeout() {
|
||||
local source_file="$1"
|
||||
local output_file="$2"
|
||||
local timeout="$3"
|
||||
|
||||
if grep -qE '^node:[[:space:]]*($|#)' "$source_file"; then
|
||||
if awk '
|
||||
/^[^[:space:]#][^:]*:/ {
|
||||
in_node = ($1 == "node:")
|
||||
next
|
||||
}
|
||||
in_node && /^[[:space:]]+inactivity_timeout:[[:space:]]*/ { found = 1 }
|
||||
END { exit(found ? 0 : 1) }
|
||||
' "$source_file"; then
|
||||
cp "$source_file" "$output_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if awk '
|
||||
/^[^[:space:]#][^:]*:/ {
|
||||
in_node = ($1 == "node:")
|
||||
next
|
||||
}
|
||||
in_node && /^ ephemeral:[[:space:]]*($|#)/ { found = 1 }
|
||||
END { exit(found ? 0 : 1) }
|
||||
' "$source_file"; then
|
||||
awk -v timeout="$timeout" '
|
||||
/^[^[:space:]#][^:]*:/ { in_node = ($1 == "node:") }
|
||||
{ print }
|
||||
in_node && /^ ephemeral:[[:space:]]*($|#)/ && !inserted {
|
||||
print " inactivity_timeout: " timeout
|
||||
inserted = 1
|
||||
}
|
||||
' "$source_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" <<EOF
|
||||
|
||||
node:
|
||||
expiry: 0
|
||||
ephemeral:
|
||||
inactivity_timeout: ${timeout}
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||
echo "$CONFIG_FILE not found; skipped Headscale 0.29 configuration migration"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
randomize_client_port="$(top_level_scalar randomize_client_port)"
|
||||
ephemeral_timeout="$(top_level_scalar ephemeral_node_inactivity_timeout)"
|
||||
|
||||
if [[ -z "$randomize_client_port" && -z "$ephemeral_timeout" ]]; then
|
||||
echo "Headscale configuration already uses the 0.29 layout; skipped migration"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
policy_mode="$(section_scalar policy mode)"
|
||||
policy_path="$(section_scalar policy path)"
|
||||
policy_mode="${policy_mode:-file}"
|
||||
config_dir="$(dirname "$CONFIG_FILE")"
|
||||
policy_file="${POLICY_FILE:-${config_dir}/policy.hujson}"
|
||||
set_default_policy=0
|
||||
|
||||
case "${randomize_client_port,,}" in
|
||||
true|yes|1)
|
||||
if [[ "${policy_mode,,}" != "file" ]]; then
|
||||
echo "Headscale 0.29 moves randomize_client_port into the policy. Add randomizeClientPort to the database policy before upgrading." >&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}"
|
||||
@@ -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 是控制面服务,不承担客户端数据面转发。
|
||||
|
||||
## 配置项
|
||||
| 变量 | 说明 | 默认值 | 必填 |
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
Reference in New Issue
Block a user