2026-05-14 19:34:33 +08:00
|
|
|
|
#!/bin/sh
|
|
|
|
|
|
set -eu
|
2026-09-04 03:45:52 +08:00
|
|
|
|
umask 077
|
2026-05-14 19:34:33 +08:00
|
|
|
|
|
2026-05-20 01:02:01 +08:00
|
|
|
|
REPO="${AETHER_TUNNEL_RELEASE_REPO:-fawney19/Aether}"
|
|
|
|
|
|
TAG="${AETHER_TUNNEL_RELEASE_TAG:-}"
|
|
|
|
|
|
INSTALL_DIR="${AETHER_TUNNEL_INSTALL_DIR:-}"
|
|
|
|
|
|
CONFIG_PATH="${AETHER_TUNNEL_CONFIG:-}"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
TMP_DIR=""
|
2026-09-04 03:45:52 +08:00
|
|
|
|
CONFIG_TMP_PATH=""
|
2026-05-14 19:34:33 +08:00
|
|
|
|
|
2026-05-20 01:02:01 +08:00
|
|
|
|
say() { printf '%s\n' "[Aether Tunnel] $1"; }
|
|
|
|
|
|
fail() { printf '%s\n' "[Aether Tunnel] $1" >&2; exit 1; }
|
2026-05-14 19:34:33 +08:00
|
|
|
|
|
|
|
|
|
|
cleanup() {
|
2026-09-04 03:45:52 +08:00
|
|
|
|
if [ -n "$CONFIG_TMP_PATH" ] && [ -f "$CONFIG_TMP_PATH" ] && [ ! -L "$CONFIG_TMP_PATH" ]; then
|
|
|
|
|
|
rm -f "$CONFIG_TMP_PATH"
|
|
|
|
|
|
fi
|
2026-05-14 19:34:33 +08:00
|
|
|
|
if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then
|
|
|
|
|
|
rm -rf "$TMP_DIR"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
trap cleanup EXIT INT TERM
|
|
|
|
|
|
|
|
|
|
|
|
need_cmd() {
|
|
|
|
|
|
command -v "$1" >/dev/null 2>&1 || fail "缺少命令:$1"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-04 03:45:52 +08:00
|
|
|
|
validate_https_download_url() {
|
|
|
|
|
|
url="$1"
|
|
|
|
|
|
case "$url" in
|
|
|
|
|
|
https://*) ;;
|
|
|
|
|
|
*) fail "远程下载必须使用绝对 HTTPS URL" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
case "$url" in
|
|
|
|
|
|
*'#'*) fail "远程下载 URL 不得包含 fragment" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
authority=${url#https://}
|
|
|
|
|
|
authority=${authority%%[/?]*}
|
|
|
|
|
|
[ -n "$authority" ] || fail "远程下载 URL 的 host 不能为空"
|
|
|
|
|
|
case "$authority" in
|
|
|
|
|
|
*'@'*) fail "远程下载 URL 不得包含凭据" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
validate_trusted_github_download_url() {
|
|
|
|
|
|
url="$1"
|
|
|
|
|
|
validate_https_download_url "$url"
|
|
|
|
|
|
authority=${url#https://}
|
|
|
|
|
|
authority=${authority%%[/?]*}
|
|
|
|
|
|
case "$authority" in
|
|
|
|
|
|
*:*) fail "GitHub 下载 URL 不得使用非标准端口:$authority" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
host=$(printf '%s' "$authority" | tr 'A-Z' 'a-z')
|
|
|
|
|
|
case "$host" in
|
|
|
|
|
|
api.github.com|github.com|objects.githubusercontent.com|*.objects.githubusercontent.com|release-assets.githubusercontent.com|*.release-assets.githubusercontent.com) ;;
|
|
|
|
|
|
*) fail "GitHub 下载重定向到了不受信任的主机:$host" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 19:34:33 +08:00
|
|
|
|
download() {
|
|
|
|
|
|
url="$1"
|
|
|
|
|
|
out="$2"
|
2026-09-04 03:45:52 +08:00
|
|
|
|
validate_trusted_github_download_url "$url"
|
|
|
|
|
|
command -v curl >/dev/null 2>&1 || fail "需要 curl 安全下载 GitHub release 制品"
|
|
|
|
|
|
|
|
|
|
|
|
download_dir=$(dirname "$out")
|
|
|
|
|
|
[ -d "$download_dir" ] && [ ! -L "$download_dir" ] \
|
|
|
|
|
|
|| fail "下载目标目录不是安全目录:$download_dir"
|
|
|
|
|
|
current_url="$url"
|
|
|
|
|
|
redirect_count=0
|
|
|
|
|
|
while :; do
|
|
|
|
|
|
response_tmp=$(mktemp "$download_dir/.aether-download.body.XXXXXXXX") \
|
|
|
|
|
|
|| fail "无法创建安全下载临时文件"
|
|
|
|
|
|
headers_tmp=$(mktemp "$download_dir/.aether-download.headers.XXXXXXXX") || {
|
|
|
|
|
|
rm -f "$response_tmp"
|
|
|
|
|
|
fail "无法创建安全响应头临时文件"
|
|
|
|
|
|
}
|
|
|
|
|
|
if ! status=$(curl -sS --retry 3 --connect-timeout 10 \
|
|
|
|
|
|
--proto '=https' --max-redirs 0 --dump-header "$headers_tmp" \
|
|
|
|
|
|
--output "$response_tmp" --write-out '%{http_code}' "$current_url"); then
|
|
|
|
|
|
rm -f "$response_tmp" "$headers_tmp"
|
|
|
|
|
|
fail "GitHub 下载失败:$current_url"
|
|
|
|
|
|
fi
|
|
|
|
|
|
case "$status" in
|
|
|
|
|
|
2??)
|
|
|
|
|
|
rm -f "$headers_tmp"
|
|
|
|
|
|
[ -f "$response_tmp" ] && [ ! -L "$response_tmp" ] \
|
|
|
|
|
|
|| fail "下载结果不是普通文件"
|
|
|
|
|
|
mv -f "$response_tmp" "$out" || {
|
|
|
|
|
|
rm -f "$response_tmp"
|
|
|
|
|
|
fail "无法原子保存下载结果:$out"
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
;;
|
|
|
|
|
|
301|302|303|307|308)
|
|
|
|
|
|
location=$(awk '
|
|
|
|
|
|
tolower(substr($0, 1, 9)) == "location:" {
|
|
|
|
|
|
value = substr($0, 10)
|
|
|
|
|
|
sub(/^[[:space:]]*/, "", value)
|
|
|
|
|
|
sub(/\r$/, "", value)
|
|
|
|
|
|
}
|
|
|
|
|
|
END { print value }
|
|
|
|
|
|
' "$headers_tmp")
|
|
|
|
|
|
rm -f "$response_tmp" "$headers_tmp"
|
|
|
|
|
|
[ -n "$location" ] || fail "GitHub 重定向缺少 Location 响应头"
|
|
|
|
|
|
validate_trusted_github_download_url "$location"
|
|
|
|
|
|
redirect_count=$((redirect_count + 1))
|
|
|
|
|
|
[ "$redirect_count" -le 10 ] || fail "GitHub 下载重定向次数过多"
|
|
|
|
|
|
current_url="$location"
|
|
|
|
|
|
;;
|
|
|
|
|
|
*)
|
|
|
|
|
|
rm -f "$response_tmp" "$headers_tmp"
|
|
|
|
|
|
fail "GitHub 下载返回 HTTP $status:$current_url"
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
done
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
validate_release_repo() {
|
|
|
|
|
|
value="$1"
|
|
|
|
|
|
[ -n "$value" ] && [ "${#value}" -le 200 ] \
|
|
|
|
|
|
|| fail "release 仓库必须是安全的 GitHub OWNER/REPO 标识符"
|
|
|
|
|
|
case "$value" in
|
|
|
|
|
|
*/*) ;;
|
|
|
|
|
|
*) fail "release 仓库必须是安全的 GitHub OWNER/REPO 标识符" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
owner=${value%%/*}
|
|
|
|
|
|
name=${value#*/}
|
|
|
|
|
|
case "$name" in
|
|
|
|
|
|
*/*) fail "release 仓库必须是安全的 GitHub OWNER/REPO 标识符" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
case "$owner" in
|
|
|
|
|
|
[A-Za-z0-9]*) ;;
|
|
|
|
|
|
*) fail "release 仓库必须是安全的 GitHub OWNER/REPO 标识符" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
case "$name" in
|
|
|
|
|
|
[A-Za-z0-9]*) ;;
|
|
|
|
|
|
*) fail "release 仓库必须是安全的 GitHub OWNER/REPO 标识符" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
case "$owner$name" in
|
|
|
|
|
|
*[!A-Za-z0-9._-]*) fail "release 仓库必须是安全的 GitHub OWNER/REPO 标识符" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
validate_tunnel_release_tag() {
|
|
|
|
|
|
value="$1"
|
|
|
|
|
|
[ -n "$value" ] && [ "${#value}" -le 128 ] \
|
|
|
|
|
|
|| fail "release tag 包含不安全的 URL 或路径字符"
|
|
|
|
|
|
case "$value" in
|
|
|
|
|
|
*[!A-Za-z0-9._+-]*) fail "release tag 包含不安全的 URL 或路径字符" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
version=${value#tunnel-v}
|
|
|
|
|
|
[ "$version" != "$value" ] || fail "release tag 必须使用 tunnel-v<version> 格式"
|
|
|
|
|
|
semver_identifier='(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)'
|
|
|
|
|
|
if ! printf '%s' "$version" | LC_ALL=C grep -Eq "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-(${semver_identifier})(\\.${semver_identifier})*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"; then
|
|
|
|
|
|
fail "release tag 必须包含有效的 SemVer 版本"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
validate_release_asset_name() {
|
|
|
|
|
|
value="$1"
|
|
|
|
|
|
[ -n "$value" ] && [ "${#value}" -le 200 ] \
|
|
|
|
|
|
|| fail "release 制品名称无效"
|
|
|
|
|
|
case "$value" in
|
|
|
|
|
|
aether-tunnel-*.tar.gz) ;;
|
|
|
|
|
|
*) fail "release 制品名称无效" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
case "$value" in
|
|
|
|
|
|
*[!A-Za-z0-9._-]*) fail "release 制品名称包含不安全的路径字符" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
validate_node_name() {
|
|
|
|
|
|
value="$1"
|
|
|
|
|
|
[ -n "$value" ] && [ "${#value}" -le 255 ] \
|
|
|
|
|
|
|| fail "node name 必须是 1 到 255 个字符"
|
|
|
|
|
|
newline='
|
|
|
|
|
|
'
|
|
|
|
|
|
carriage_return=$(printf '\r')
|
|
|
|
|
|
case "$value" in
|
|
|
|
|
|
*"$newline"*|*"$carriage_return"*) fail "node name 不得包含控制字符" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
case "$value" in
|
|
|
|
|
|
[[:space:]]*|*[[:space:]]) fail "node name 不得包含首尾空白" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
if printf '%s' "$value" | LC_ALL=C grep -q '[[:cntrl:]]'; then
|
|
|
|
|
|
fail "node name 不得包含控制字符"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
prompt_if_empty() {
|
|
|
|
|
|
name="$1"
|
|
|
|
|
|
value="$2"
|
|
|
|
|
|
prompt="$3"
|
|
|
|
|
|
if [ -n "$value" ]; then
|
|
|
|
|
|
printf '%s' "$value"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
printf '%s' "$prompt" >&2
|
|
|
|
|
|
if [ -r /dev/tty ]; then
|
|
|
|
|
|
IFS= read -r value < /dev/tty
|
|
|
|
|
|
else
|
|
|
|
|
|
fail "$name 未通过环境变量提供,且当前环境无法交互输入"
|
|
|
|
|
|
fi
|
|
|
|
|
|
[ -n "$value" ] || fail "$name 不能为空"
|
|
|
|
|
|
printf '%s' "$value"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
toml_quote() {
|
|
|
|
|
|
value="$1"
|
|
|
|
|
|
if command -v python3 >/dev/null 2>&1; then
|
2026-09-04 03:45:52 +08:00
|
|
|
|
quoted=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1], ensure_ascii=False))' "$value" 2>/dev/null || true)
|
|
|
|
|
|
if [ -n "$quoted" ]; then
|
|
|
|
|
|
printf '%s\n' "$quoted"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
2026-05-14 19:34:33 +08:00
|
|
|
|
fi
|
2026-09-04 03:45:52 +08:00
|
|
|
|
printf '%s' "$value" | awk '
|
|
|
|
|
|
BEGIN { printf "\"" }
|
|
|
|
|
|
{
|
|
|
|
|
|
if (NR > 1) printf "\\n"
|
|
|
|
|
|
gsub(/\\/, "\\\\")
|
|
|
|
|
|
gsub(/\"/, "\\\"")
|
|
|
|
|
|
gsub(/\t/, "\\t")
|
|
|
|
|
|
gsub(/\r/, "\\r")
|
|
|
|
|
|
printf "%s", $0
|
|
|
|
|
|
}
|
|
|
|
|
|
END { print "\"" }
|
|
|
|
|
|
'
|
2026-05-14 19:34:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 01:02:01 +08:00
|
|
|
|
resolve_latest_tunnel_tag() {
|
2026-09-04 03:45:52 +08:00
|
|
|
|
validate_release_repo "$REPO"
|
|
|
|
|
|
if [ -n "$TAG" ]; then
|
|
|
|
|
|
validate_tunnel_release_tag "$TAG"
|
|
|
|
|
|
printf '%s\n' "$TAG"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
2026-05-14 19:34:33 +08:00
|
|
|
|
api_url="https://api.github.com/repos/${REPO}/releases?per_page=100"
|
|
|
|
|
|
releases="$TMP_DIR/releases.json"
|
|
|
|
|
|
download "$api_url" "$releases" >/dev/null 2>&1 || fail "无法读取 GitHub Releases:$api_url"
|
|
|
|
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
|
|
|
|
python3 - "$releases" <<'PY'
|
2026-09-04 03:45:52 +08:00
|
|
|
|
import json, re, sys
|
2026-05-14 19:34:33 +08:00
|
|
|
|
releases = json.load(open(sys.argv[1], encoding='utf-8'))
|
2026-09-04 03:45:52 +08:00
|
|
|
|
tag_pattern = re.compile(r'^tunnel-v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$')
|
|
|
|
|
|
tunnel = [r for r in releases if not r.get('draft') and not r.get('prerelease') and tag_pattern.fullmatch(str(r.get('tag_name', '')))]
|
2026-05-20 01:02:01 +08:00
|
|
|
|
tunnel.sort(key=lambda r: r.get('published_at') or r.get('created_at') or '', reverse=True)
|
|
|
|
|
|
if tunnel:
|
|
|
|
|
|
print(tunnel[0]['tag_name'])
|
2026-05-14 19:34:33 +08:00
|
|
|
|
PY
|
|
|
|
|
|
else
|
2026-09-04 03:45:52 +08:00
|
|
|
|
# Without a JSON parser, only accept an exact stable SemVer tag. This
|
|
|
|
|
|
# keeps the fallback from accidentally selecting a prerelease entry.
|
|
|
|
|
|
grep -Eo '"tag_name"[[:space:]]*:[[:space:]]*"tunnel-v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)"' "$releases" |
|
|
|
|
|
|
head -n 1 |
|
|
|
|
|
|
sed 's/.*"\(tunnel-v[^"]*\)".*/\1/'
|
2026-05-14 19:34:33 +08:00
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
detect_asset() {
|
|
|
|
|
|
os=$(uname -s 2>/dev/null || printf unknown)
|
|
|
|
|
|
arch=$(uname -m 2>/dev/null || printf unknown)
|
|
|
|
|
|
|
|
|
|
|
|
case "$os" in
|
|
|
|
|
|
Linux) platform=linux ;;
|
|
|
|
|
|
Darwin) platform=macos ;;
|
|
|
|
|
|
MINGW*|MSYS*|CYGWIN*) fail "检测到 Windows shell,请使用 PowerShell:irm <install.ps1-url> | iex" ;;
|
|
|
|
|
|
*) fail "不支持的系统:$os" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
|
|
case "$arch" in
|
|
|
|
|
|
x86_64|amd64) cpu=amd64 ;;
|
|
|
|
|
|
aarch64|arm64) cpu=arm64 ;;
|
|
|
|
|
|
*) fail "不支持的 CPU 架构:$arch" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
|
|
if [ "$platform" = "linux" ] && command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then
|
2026-05-20 01:02:01 +08:00
|
|
|
|
printf 'aether-tunnel-linux-musl-%s.tar.gz\n' "$cpu"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
else
|
2026-05-20 01:02:01 +08:00
|
|
|
|
printf 'aether-tunnel-%s-%s.tar.gz\n' "$platform" "$cpu"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
choose_paths() {
|
|
|
|
|
|
if [ -z "$INSTALL_DIR" ]; then
|
|
|
|
|
|
if [ "$(id -u 2>/dev/null || printf 1)" = "0" ]; then
|
|
|
|
|
|
INSTALL_DIR="/usr/local/bin"
|
|
|
|
|
|
else
|
|
|
|
|
|
INSTALL_DIR="$HOME/.local/bin"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [ -z "$CONFIG_PATH" ]; then
|
|
|
|
|
|
if [ "$(id -u 2>/dev/null || printf 1)" = "0" ]; then
|
2026-05-20 01:02:01 +08:00
|
|
|
|
CONFIG_PATH="/etc/aether-tunnel/aether-tunnel.toml"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
else
|
2026-05-20 01:02:01 +08:00
|
|
|
|
CONFIG_PATH="$HOME/.aether-tunnel/aether-tunnel.toml"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-04 03:45:52 +08:00
|
|
|
|
stat_owner_id() {
|
|
|
|
|
|
stat -c '%u' "$1" 2>/dev/null || stat -f '%u' "$1" 2>/dev/null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stat_mode() {
|
|
|
|
|
|
stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1" 2>/dev/null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stat_link_count() {
|
|
|
|
|
|
stat -c '%h' "$1" 2>/dev/null || stat -f '%l' "$1" 2>/dev/null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
require_single_link_regular_file() {
|
|
|
|
|
|
path="$1"
|
|
|
|
|
|
description="$2"
|
|
|
|
|
|
[ -f "$path" ] && [ ! -L "$path" ] || fail "$description 不是普通文件:$path"
|
|
|
|
|
|
link_count=$(stat_link_count "$path") || fail "无法读取 $description 的硬链接计数:$path"
|
|
|
|
|
|
[ "$link_count" = "1" ] || fail "$description 不得是多重硬链接文件:$path"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
validate_trusted_directory_ancestors() {
|
|
|
|
|
|
path="$1"
|
|
|
|
|
|
description="$2"
|
|
|
|
|
|
current="$path"
|
|
|
|
|
|
while :; do
|
|
|
|
|
|
[ ! -L "$current" ] || fail "$description 的祖先目录不得是符号链接:$current"
|
|
|
|
|
|
if [ -e "$current" ]; then
|
|
|
|
|
|
[ -d "$current" ] || fail "$description 的祖先路径不是目录:$current"
|
|
|
|
|
|
ancestor_owner=$(stat_owner_id "$current") \
|
|
|
|
|
|
|| fail "无法读取 $description 的祖先目录所有者:$current"
|
|
|
|
|
|
current_uid=$(id -u)
|
|
|
|
|
|
[ "$ancestor_owner" = "0" ] || [ "$ancestor_owner" = "$current_uid" ] \
|
|
|
|
|
|
|| fail "$description 的祖先目录必须属于 root 或当前用户:$current"
|
|
|
|
|
|
ancestor_mode=$(stat_mode "$current") \
|
|
|
|
|
|
|| fail "无法读取 $description 的祖先目录权限:$current"
|
|
|
|
|
|
case "$ancestor_mode" in
|
|
|
|
|
|
*[!0-7]*) fail "$description 的祖先目录权限格式无效:$ancestor_mode" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
ancestor_permissions=$((0$ancestor_mode))
|
|
|
|
|
|
[ $((ancestor_permissions & 0022)) -eq 0 ] \
|
|
|
|
|
|
|| fail "$description 的祖先目录不得允许组或其他用户写入:$current"
|
|
|
|
|
|
fi
|
|
|
|
|
|
parent=$(dirname "$current")
|
|
|
|
|
|
[ "$parent" != "$current" ] || break
|
|
|
|
|
|
current="$parent"
|
|
|
|
|
|
done
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
prepare_secure_config_path() {
|
|
|
|
|
|
config_dir=$(dirname "$CONFIG_PATH")
|
|
|
|
|
|
config_name=$(basename "$CONFIG_PATH")
|
|
|
|
|
|
validate_trusted_directory_ancestors "$config_dir" "配置目录"
|
|
|
|
|
|
[ ! -L "$config_dir" ] || fail "配置目录不得是符号链接:$config_dir"
|
|
|
|
|
|
if [ -e "$config_dir" ] && [ ! -d "$config_dir" ]; then
|
|
|
|
|
|
fail "配置目录路径不是目录:$config_dir"
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [ ! -d "$config_dir" ]; then
|
|
|
|
|
|
mkdir -p "$config_dir"
|
|
|
|
|
|
chmod 700 "$config_dir"
|
|
|
|
|
|
fi
|
|
|
|
|
|
[ ! -L "$config_dir" ] || fail "配置目录不得是符号链接:$config_dir"
|
|
|
|
|
|
config_dir=$(cd "$config_dir" && pwd -P) || fail "无法解析配置目录:$config_dir"
|
|
|
|
|
|
CONFIG_PATH="$config_dir/$config_name"
|
|
|
|
|
|
|
|
|
|
|
|
config_dir_owner=$(stat_owner_id "$config_dir") || fail "无法读取配置目录所有者:$config_dir"
|
|
|
|
|
|
[ "$config_dir_owner" = "$(id -u)" ] || fail "配置目录必须属于当前用户:$config_dir"
|
|
|
|
|
|
config_dir_mode=$(stat_mode "$config_dir") || fail "无法读取配置目录权限:$config_dir"
|
|
|
|
|
|
case "$config_dir_mode" in
|
|
|
|
|
|
*[!0-7]*) fail "配置目录权限格式无效:$config_dir_mode" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
config_dir_permissions=$((0$config_dir_mode))
|
|
|
|
|
|
[ $((config_dir_permissions & 0022)) -eq 0 ] \
|
|
|
|
|
|
|| fail "配置目录不得允许组或其他用户写入:$config_dir"
|
|
|
|
|
|
|
|
|
|
|
|
[ ! -L "$CONFIG_PATH" ] || fail "配置文件不得是符号链接:$CONFIG_PATH"
|
|
|
|
|
|
if [ -e "$CONFIG_PATH" ]; then
|
|
|
|
|
|
require_single_link_regular_file "$CONFIG_PATH" "配置文件"
|
|
|
|
|
|
config_owner=$(stat_owner_id "$CONFIG_PATH") || fail "无法读取配置文件所有者:$CONFIG_PATH"
|
|
|
|
|
|
[ "$config_owner" = "$(id -u)" ] || fail "配置文件必须属于当前用户:$CONFIG_PATH"
|
|
|
|
|
|
chmod 600 "$CONFIG_PATH" || fail "无法保护配置文件权限:$CONFIG_PATH"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 19:34:33 +08:00
|
|
|
|
verify_checksum() {
|
|
|
|
|
|
archive="$1"
|
|
|
|
|
|
sums="$2"
|
|
|
|
|
|
asset="$3"
|
2026-09-04 03:45:52 +08:00
|
|
|
|
require_single_link_regular_file "$archive" "release 制品"
|
|
|
|
|
|
require_single_link_regular_file "$sums" "SHA256 校验文件"
|
|
|
|
|
|
matches=$(awk -v asset="$asset" '
|
|
|
|
|
|
{
|
|
|
|
|
|
targets_asset = 0
|
|
|
|
|
|
for (field = 2; field <= NF; field += 1) {
|
|
|
|
|
|
if ($field == asset || $field == "*" asset) targets_asset = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
if (targets_asset) {
|
|
|
|
|
|
if (NF == 2 && ($2 == asset || $2 == "*" asset)) print $1
|
|
|
|
|
|
else print "INVALID"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
' "$sums")
|
|
|
|
|
|
match_count=$(printf '%s\n' "$matches" | awk 'NF { count += 1 } END { print count + 0 }')
|
|
|
|
|
|
[ "$match_count" -eq 1 ] \
|
|
|
|
|
|
|| fail "SHA256SUMS.txt 必须且只能包含一个制品条目:$asset"
|
|
|
|
|
|
expected=$(printf '%s\n' "$matches" | awk 'NF { print; exit }')
|
|
|
|
|
|
[ "${#expected}" -eq 64 ] || fail "SHA256SUMS.txt 中的哈希格式无效:$asset"
|
|
|
|
|
|
case "$expected" in
|
|
|
|
|
|
*[!0-9A-Fa-f]*)
|
|
|
|
|
|
fail "SHA256SUMS.txt 中的哈希格式无效:$asset"
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
2026-05-14 19:34:33 +08:00
|
|
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
|
|
|
|
actual=$(sha256sum "$archive" | awk '{print $1}')
|
|
|
|
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
|
|
|
|
actual=$(shasum -a 256 "$archive" | awk '{print $1}')
|
|
|
|
|
|
else
|
2026-09-04 03:45:52 +08:00
|
|
|
|
fail "缺少 sha256sum 或 shasum,无法验证 release 制品"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
fi
|
2026-09-04 03:45:52 +08:00
|
|
|
|
actual=$(printf '%s' "$actual" | tr 'A-F' 'a-f')
|
|
|
|
|
|
expected=$(printf '%s' "$expected" | tr 'A-F' 'a-f')
|
2026-05-14 19:34:33 +08:00
|
|
|
|
[ "$actual" = "$expected" ] || fail "SHA256 校验失败:$asset"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-04 03:45:52 +08:00
|
|
|
|
extract_tunnel_binary() {
|
|
|
|
|
|
archive="$1"
|
|
|
|
|
|
destination="$2"
|
|
|
|
|
|
require_single_link_regular_file "$archive" "release 制品"
|
|
|
|
|
|
members=$(tar -tzf "$archive" 2>/dev/null) || fail "无法读取 release 制品"
|
|
|
|
|
|
member_count=$(printf '%s\n' "$members" | awk 'NF { count += 1 } END { print count + 0 }')
|
|
|
|
|
|
[ "$member_count" -eq 1 ] || fail "制品必须且只能包含一个 aether-tunnel 文件"
|
|
|
|
|
|
[ "$members" = "aether-tunnel" ] || fail "制品必须在根目录包含 aether-tunnel"
|
|
|
|
|
|
|
|
|
|
|
|
listing=$(tar -tvzf "$archive" 2>/dev/null) || fail "无法读取 release 制品元数据"
|
|
|
|
|
|
member_type=$(printf '%s\n' "$listing" | awk 'NF { print substr($1, 1, 1); exit }')
|
|
|
|
|
|
[ "$member_type" = "-" ] || fail "制品中的 aether-tunnel 不是普通文件"
|
|
|
|
|
|
|
|
|
|
|
|
destination_dir=$(dirname "$destination")
|
|
|
|
|
|
extract_tmp=$(mktemp "$destination_dir/.aether-tunnel.extract.XXXXXXXX") \
|
|
|
|
|
|
|| fail "无法创建安全解压临时文件"
|
|
|
|
|
|
if ! tar -xOzf "$archive" aether-tunnel > "$extract_tmp"; then
|
|
|
|
|
|
rm -f "$extract_tmp"
|
|
|
|
|
|
fail "无法安全提取 aether-tunnel"
|
|
|
|
|
|
fi
|
|
|
|
|
|
[ -s "$extract_tmp" ] || {
|
|
|
|
|
|
rm -f "$extract_tmp"
|
|
|
|
|
|
fail "制品中的 aether-tunnel 为空"
|
|
|
|
|
|
}
|
|
|
|
|
|
mv -f "$extract_tmp" "$destination" || {
|
|
|
|
|
|
rm -f "$extract_tmp"
|
|
|
|
|
|
fail "无法原子保存解压后的 aether-tunnel"
|
|
|
|
|
|
}
|
|
|
|
|
|
require_single_link_regular_file "$destination" "解压后的 aether-tunnel"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
prepare_secure_install_dir() {
|
|
|
|
|
|
validate_trusted_directory_ancestors "$INSTALL_DIR" "安装目录"
|
|
|
|
|
|
[ ! -L "$INSTALL_DIR" ] || fail "安装目录不得是符号链接:$INSTALL_DIR"
|
|
|
|
|
|
if [ -e "$INSTALL_DIR" ] && [ ! -d "$INSTALL_DIR" ]; then
|
|
|
|
|
|
fail "安装目录路径不是目录:$INSTALL_DIR"
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [ ! -d "$INSTALL_DIR" ]; then
|
|
|
|
|
|
mkdir -p "$INSTALL_DIR"
|
|
|
|
|
|
fi
|
|
|
|
|
|
[ ! -L "$INSTALL_DIR" ] || fail "安装目录不得是符号链接:$INSTALL_DIR"
|
|
|
|
|
|
INSTALL_DIR=$(cd "$INSTALL_DIR" && pwd -P) || fail "无法解析安装目录:$INSTALL_DIR"
|
|
|
|
|
|
|
|
|
|
|
|
install_dir_owner=$(stat_owner_id "$INSTALL_DIR") || fail "无法读取安装目录所有者:$INSTALL_DIR"
|
|
|
|
|
|
[ "$install_dir_owner" = "$(id -u)" ] || fail "安装目录必须属于当前用户:$INSTALL_DIR"
|
|
|
|
|
|
install_dir_mode=$(stat_mode "$INSTALL_DIR") || fail "无法读取安装目录权限:$INSTALL_DIR"
|
|
|
|
|
|
case "$install_dir_mode" in
|
|
|
|
|
|
*[!0-7]*) fail "安装目录权限格式无效:$install_dir_mode" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
install_dir_permissions=$((0$install_dir_mode))
|
|
|
|
|
|
[ $((install_dir_permissions & 0022)) -eq 0 ] \
|
|
|
|
|
|
|| fail "安装目录不得允许组或其他用户写入:$INSTALL_DIR"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
install_tunnel_binary_file() {
|
|
|
|
|
|
source_binary="$1"
|
|
|
|
|
|
require_single_link_regular_file "$source_binary" "待安装二进制"
|
|
|
|
|
|
prepare_secure_install_dir
|
|
|
|
|
|
target_binary="$INSTALL_DIR/aether-tunnel"
|
|
|
|
|
|
[ ! -L "$target_binary" ] || fail "安装目标不得是符号链接:$target_binary"
|
|
|
|
|
|
if [ -e "$target_binary" ]; then
|
|
|
|
|
|
require_single_link_regular_file "$target_binary" "安装目标"
|
|
|
|
|
|
target_owner=$(stat_owner_id "$target_binary") || fail "无法读取安装目标所有者:$target_binary"
|
|
|
|
|
|
[ "$target_owner" = "$(id -u)" ] || fail "安装目标必须属于当前用户:$target_binary"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
install_tmp=$(mktemp "$INSTALL_DIR/.aether-tunnel.tmp.XXXXXXXX") \
|
|
|
|
|
|
|| fail "无法在安装目录中创建安全临时文件"
|
|
|
|
|
|
if ! cat "$source_binary" > "$install_tmp"; then
|
|
|
|
|
|
rm -f "$install_tmp"
|
|
|
|
|
|
fail "无法写入临时安装文件"
|
|
|
|
|
|
fi
|
|
|
|
|
|
chmod 0755 "$install_tmp" || {
|
|
|
|
|
|
rm -f "$install_tmp"
|
|
|
|
|
|
fail "无法设置临时安装文件权限"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[ ! -L "$target_binary" ] || {
|
|
|
|
|
|
rm -f "$install_tmp"
|
|
|
|
|
|
fail "安装目标在写入期间变成了符号链接:$target_binary"
|
|
|
|
|
|
}
|
|
|
|
|
|
if [ -e "$target_binary" ]; then
|
|
|
|
|
|
if ! require_single_link_regular_file "$target_binary" "安装目标"; then
|
|
|
|
|
|
rm -f "$install_tmp"
|
|
|
|
|
|
fail "安装目标在写入期间变得不安全:$target_binary"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
mv -f "$install_tmp" "$target_binary" || {
|
|
|
|
|
|
rm -f "$install_tmp"
|
|
|
|
|
|
fail "无法原子替换安装目标:$target_binary"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 19:34:33 +08:00
|
|
|
|
install_binary() {
|
|
|
|
|
|
tag="$1"
|
|
|
|
|
|
asset="$2"
|
2026-09-04 03:45:52 +08:00
|
|
|
|
validate_release_repo "$REPO"
|
|
|
|
|
|
validate_tunnel_release_tag "$tag"
|
|
|
|
|
|
validate_release_asset_name "$asset"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
base="https://github.com/${REPO}/releases/download/${tag}"
|
|
|
|
|
|
archive="$TMP_DIR/$asset"
|
|
|
|
|
|
say "下载 $tag / $asset"
|
|
|
|
|
|
download "$base/$asset" "$archive"
|
2026-09-04 03:45:52 +08:00
|
|
|
|
download "$base/SHA256SUMS.txt" "$TMP_DIR/SHA256SUMS.txt" >/dev/null 2>&1 || fail "无法下载 SHA256SUMS.txt"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
verify_checksum "$archive" "$TMP_DIR/SHA256SUMS.txt" "$asset"
|
|
|
|
|
|
|
2026-09-04 03:45:52 +08:00
|
|
|
|
extract_tunnel_binary "$archive" "$TMP_DIR/aether-tunnel"
|
|
|
|
|
|
install_tunnel_binary_file "$TMP_DIR/aether-tunnel"
|
2026-05-20 01:02:01 +08:00
|
|
|
|
say "已安装二进制:$INSTALL_DIR/aether-tunnel"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
has_legacy_single_server_keys() {
|
|
|
|
|
|
[ -f "$CONFIG_PATH" ] || return 1
|
|
|
|
|
|
awk '
|
|
|
|
|
|
/^[[:space:]]*\[/ { exit }
|
|
|
|
|
|
/^[[:space:]]*(aether_url|management_token)[[:space:]]*=/ { found=1; exit }
|
|
|
|
|
|
END { exit found ? 0 : 1 }
|
|
|
|
|
|
' "$CONFIG_PATH"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
server_exists() {
|
|
|
|
|
|
[ -f "$CONFIG_PATH" ] || return 1
|
|
|
|
|
|
quoted_url="$1"
|
|
|
|
|
|
quoted_name="$2"
|
|
|
|
|
|
awk -v url="aether_url = $quoted_url" -v name="node_name = $quoted_name" '
|
|
|
|
|
|
BEGIN { found_url=0; found_name=0 }
|
|
|
|
|
|
/^\[\[servers\]\]/ {
|
|
|
|
|
|
if (found_url && found_name) { found=1 }
|
|
|
|
|
|
found_url=0; found_name=0
|
|
|
|
|
|
}
|
|
|
|
|
|
$0 == url { found_url=1 }
|
|
|
|
|
|
$0 == name { found_name=1 }
|
|
|
|
|
|
END { if (found_url && found_name) { found=1 }; exit found ? 0 : 1 }
|
|
|
|
|
|
' "$CONFIG_PATH"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
append_server_config() {
|
|
|
|
|
|
aether_url="$1"
|
|
|
|
|
|
management_token="$2"
|
|
|
|
|
|
node_name="$3"
|
2026-05-21 16:08:14 +08:00
|
|
|
|
tunnel_security="$4"
|
|
|
|
|
|
tunnel_encryption_key="$5"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
|
2026-09-04 03:45:52 +08:00
|
|
|
|
validate_node_name "$node_name"
|
|
|
|
|
|
prepare_secure_config_path
|
2026-05-14 19:34:33 +08:00
|
|
|
|
quoted_url=$(toml_quote "$aether_url")
|
|
|
|
|
|
quoted_token=$(toml_quote "$management_token")
|
|
|
|
|
|
quoted_name=$(toml_quote "$node_name")
|
2026-05-21 16:08:14 +08:00
|
|
|
|
quoted_encryption_key=$(toml_quote "$tunnel_encryption_key")
|
2026-05-22 15:20:36 +08:00
|
|
|
|
if [ -n "$tunnel_security" ]; then
|
|
|
|
|
|
quoted_security=$(toml_quote "$tunnel_security")
|
|
|
|
|
|
fi
|
2026-05-14 19:34:33 +08:00
|
|
|
|
|
|
|
|
|
|
if has_legacy_single_server_keys; then
|
2026-05-20 01:02:01 +08:00
|
|
|
|
fail "现有配置仍使用旧的顶层 aether_url/management_token,请先运行 aether-tunnel setup 迁移为 [[servers]] 后重试:$CONFIG_PATH"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if server_exists "$quoted_url" "$quoted_name"; then
|
|
|
|
|
|
say "配置中已存在相同 aether_url + node_name,跳过追加:$CONFIG_PATH"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
2026-09-04 03:45:52 +08:00
|
|
|
|
config_existed=false
|
2026-05-14 19:34:33 +08:00
|
|
|
|
if [ -f "$CONFIG_PATH" ]; then
|
2026-09-04 03:45:52 +08:00
|
|
|
|
config_existed=true
|
|
|
|
|
|
backup_path=$(mktemp "$CONFIG_PATH.bak.$(date +%Y%m%d%H%M%S).XXXXXXXX") \
|
|
|
|
|
|
|| fail "无法创建安全配置备份"
|
|
|
|
|
|
chmod 600 "$backup_path" || fail "无法保护配置备份权限:$backup_path"
|
|
|
|
|
|
cat "$CONFIG_PATH" > "$backup_path" || fail "无法备份配置文件:$CONFIG_PATH"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
fi
|
|
|
|
|
|
|
2026-09-04 03:45:52 +08:00
|
|
|
|
CONFIG_TMP_PATH=$(mktemp "$CONFIG_PATH.tmp.XXXXXXXX") || fail "无法创建安全配置临时文件"
|
|
|
|
|
|
chmod 600 "$CONFIG_TMP_PATH" || fail "无法保护配置临时文件权限"
|
|
|
|
|
|
if [ "$config_existed" = true ]; then
|
|
|
|
|
|
cat "$CONFIG_PATH" > "$CONFIG_TMP_PATH" || fail "无法读取现有配置:$CONFIG_PATH"
|
|
|
|
|
|
fi
|
2026-05-14 19:34:33 +08:00
|
|
|
|
{
|
2026-09-04 03:45:52 +08:00
|
|
|
|
if [ -s "$CONFIG_TMP_PATH" ]; then
|
2026-05-14 19:34:33 +08:00
|
|
|
|
printf '\n'
|
|
|
|
|
|
fi
|
2026-05-20 01:02:01 +08:00
|
|
|
|
printf '# Added by Aether Tunnel one-click installer. Existing config is preserved.\n'
|
2026-05-14 19:34:33 +08:00
|
|
|
|
printf '[[servers]]\n'
|
|
|
|
|
|
printf 'aether_url = %s\n' "$quoted_url"
|
|
|
|
|
|
printf 'management_token = %s\n' "$quoted_token"
|
|
|
|
|
|
printf 'node_name = %s\n' "$quoted_name"
|
2026-05-22 15:20:36 +08:00
|
|
|
|
if [ -n "$tunnel_security" ]; then
|
|
|
|
|
|
printf 'tunnel_security = %s\n' "$quoted_security"
|
|
|
|
|
|
fi
|
2026-05-21 16:08:14 +08:00
|
|
|
|
if [ -n "$tunnel_encryption_key" ]; then
|
|
|
|
|
|
printf 'tunnel_encryption_key = %s\n' "$quoted_encryption_key"
|
|
|
|
|
|
fi
|
2026-09-04 03:45:52 +08:00
|
|
|
|
} >> "$CONFIG_TMP_PATH"
|
|
|
|
|
|
chmod 600 "$CONFIG_TMP_PATH" || fail "无法保护配置临时文件权限"
|
|
|
|
|
|
[ ! -L "$CONFIG_PATH" ] || fail "配置文件在写入期间变成了符号链接:$CONFIG_PATH"
|
|
|
|
|
|
if [ -e "$CONFIG_PATH" ]; then
|
|
|
|
|
|
require_single_link_regular_file "$CONFIG_PATH" "配置文件"
|
|
|
|
|
|
fi
|
|
|
|
|
|
mv -f "$CONFIG_TMP_PATH" "$CONFIG_PATH" || fail "无法原子替换配置文件:$CONFIG_PATH"
|
|
|
|
|
|
CONFIG_TMP_PATH=""
|
|
|
|
|
|
chmod 600 "$CONFIG_PATH" || fail "无法保护配置文件权限:$CONFIG_PATH"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
say "已追加 [[servers]] 到:$CONFIG_PATH"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main() {
|
2026-05-20 01:02:01 +08:00
|
|
|
|
TMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t aether-tunnel)
|
2026-05-14 19:34:33 +08:00
|
|
|
|
need_cmd tar
|
2026-09-04 03:45:52 +08:00
|
|
|
|
validate_release_repo "$REPO"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
choose_paths
|
|
|
|
|
|
|
2026-05-20 01:02:01 +08:00
|
|
|
|
aether_url=$(prompt_if_empty AETHER_TUNNEL_AETHER_URL "${AETHER_TUNNEL_AETHER_URL:-}" "Aether URL: ")
|
|
|
|
|
|
management_token=$(prompt_if_empty AETHER_TUNNEL_MANAGEMENT_TOKEN "${AETHER_TUNNEL_MANAGEMENT_TOKEN:-}" "Management token (ae_xxx): ")
|
|
|
|
|
|
node_name=$(prompt_if_empty AETHER_TUNNEL_NODE_NAME "${AETHER_TUNNEL_NODE_NAME:-}" "Node name: ")
|
2026-09-04 03:45:52 +08:00
|
|
|
|
validate_node_name "$node_name"
|
2026-05-22 15:20:36 +08:00
|
|
|
|
tunnel_security="${AETHER_TUNNEL_SECURITY:-}"
|
2026-05-21 16:08:14 +08:00
|
|
|
|
tunnel_encryption_key="${AETHER_TUNNEL_ENCRYPTION_KEY:-}"
|
|
|
|
|
|
case "$tunnel_security" in
|
2026-05-22 15:20:36 +08:00
|
|
|
|
""|off|non_tls_required) ;;
|
2026-05-21 16:08:14 +08:00
|
|
|
|
*) fail "AETHER_TUNNEL_SECURITY 必须是 off 或 non_tls_required" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
if [ "$tunnel_security" = "non_tls_required" ] && [ -z "$tunnel_encryption_key" ]; then
|
|
|
|
|
|
fail "AETHER_TUNNEL_SECURITY=non_tls_required 时必须设置 AETHER_TUNNEL_ENCRYPTION_KEY"
|
|
|
|
|
|
fi
|
2026-05-14 19:34:33 +08:00
|
|
|
|
|
2026-05-20 01:02:01 +08:00
|
|
|
|
tag=$(resolve_latest_tunnel_tag)
|
|
|
|
|
|
[ -n "$tag" ] || fail "没有找到可用的 tunnel-v* release"
|
2026-09-04 03:45:52 +08:00
|
|
|
|
validate_tunnel_release_tag "$tag"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
asset=$(detect_asset)
|
|
|
|
|
|
install_binary "$tag" "$asset"
|
2026-05-21 16:08:14 +08:00
|
|
|
|
append_server_config "$aether_url" "$management_token" "$node_name" "$tunnel_security" "$tunnel_encryption_key"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
|
|
|
|
|
|
say "完成。运行以下命令启动/配置服务:"
|
2026-05-20 01:02:01 +08:00
|
|
|
|
say " $INSTALL_DIR/aether-tunnel setup $CONFIG_PATH"
|
2026-05-14 19:34:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main "$@"
|