Skip to content

Instantly share code, notes, and snippets.

@NNdroid
Last active April 25, 2026 12:39
Show Gist options
  • Select an option

  • Save NNdroid/c71e4c1c26c475ee17566b41f06ac5e9 to your computer and use it in GitHub Desktop.

Select an option

Save NNdroid/c71e4c1c26c475ee17566b41f06ac5e9 to your computer and use it in GitHub Desktop.
yggdrasil installer
#!/bin/bash
# ================= 配置區域 =================
CONFIG_FILE="/etc/yggdrasil/yggdrasil.conf"
BACKUP_FILE="${CONFIG_FILE}.bak" # 備份文件保留在原目錄,不作臨時文件處理
PROXY=""
TARGET_URL="https://publicpeers.neilalexander.dev/"
# ⚠️ 再次提醒:請確保已更換為新的 Token
TG_BOT_TOKEN="YOUR_NEW_TG_BOT_TOKEN"
TG_CHAT_ID="YOUR_TG_CHAT_ID"
# ============================================
# 初始化日誌緩衝區
LOG_BUFFER=""
# 創建臨時工作目錄
WORK_DIR=$(mktemp -d -t yggdrasil_update_XXXXXX)
if [ ! -d "$WORK_DIR" ]; then
echo "無法創建臨時目錄,腳本退出。"
exit 1
fi
# 定義臨時文件路徑
CURRENT_JSON="${WORK_DIR}/current.json"
TMP_FILE="${WORK_DIR}/yggdrasil.conf.tmp"
JQ_ERR_LOG="${WORK_DIR}/jq_error.log"
# 日誌記錄函數
log() {
local level=$(echo "$1" | tr 'a-z' 'A-Z')
shift
local msg="$*"
local timestamp=$(date +'%Y-%m-%d %H:%M:%S')
local color_reset='\033[0m'
local color_red='\033[0;31m'
local color_green='\033[0;32m'
local color_yellow='\033[0;33m'
local color_cyan='\033[0;36m'
local color_code=""
case "$level" in
INFO) color_code="${color_cyan}" ;;
WARN) color_code="${color_yellow}" ;;
ERROR) color_code="${color_red}" ;;
SUCCESS) color_code="${color_green}" ;;
*) color_code="${color_reset}" ;;
esac
# 1. 屏幕輸出
echo -e "${color_code}[${timestamp}] [${level}] ${msg}${color_reset}"
# 2. 存入緩衝區 (用於 TG 推送)
LOG_BUFFER="${LOG_BUFFER}[${timestamp}] [${level}] ${msg}"$'\n'
}
# 發送 Telegram 通知
send_log_to_telegram() {
if [[ -z "$TG_BOT_TOKEN" || "$TG_BOT_TOKEN" == "YOUR_NEW_TG_BOT_TOKEN" || -z "$TG_CHAT_ID" ]]; then
echo "未配置有效的 Telegram Token,跳過發送。"
return 0
fi
[ -z "$LOG_BUFFER" ] && return 0
echo -e "\033[0;36m正在發送日誌到 Telegram...\033[0m"
curl --retry 3 --retry-delay 2 -sS -k "${PROXY}https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \
--data-urlencode "chat_id=${TG_CHAT_ID}" \
--data-urlencode "text=${LOG_BUFFER}" >/dev/null 2>&1 || true
}
# 腳本退出時的清理函數
cleanup() {
# 1. 發送 TG 通知
send_log_to_telegram
# 2. 清理臨時目錄
if [ -d "$WORK_DIR" ]; then
rm -rf "$WORK_DIR"
echo -e "\033[0;32m[系統] 臨時目錄 $WORK_DIR 已清理。\033[0m"
fi
}
# 綁定 EXIT 信號,無論腳本如何結束都會執行 cleanup
trap cleanup EXIT
# ================= 主程序開始 =================
log "INFO" "=== $(hostname) ==="
log "INFO" "=== 開始執行 Yggdrasil 節點更新任務 ==="
# 1. 前置檢查
if [ ! -f "$CONFIG_FILE" ]; then
log "ERROR" "找不到配置文件:$CONFIG_FILE"
exit 1
fi
if ! command -v jq &> /dev/null; then
log "ERROR" "系統未安裝 jq,請先執行 apt install jq 獲取依賴。"
exit 1
fi
if ! command -v yggdrasil &> /dev/null; then
log "ERROR" "系統未安裝 Yggdrasil 命令,無法執行格式轉換。"
exit 1
fi
# 2. 判斷配置格式並統一轉換為 JSON
log "INFO" "正在檢測配置文件格式..."
IS_HJSON=0
# 如果 jq 解析失敗,說明很可能是 HJSON (包含註解或非標準格式)
if jq -e . "$CONFIG_FILE" >/dev/null 2>&1; then
log "INFO" "檢測結果:標準 JSON 格式。"
cp "$CONFIG_FILE" "$CURRENT_JSON"
else
log "INFO" "檢測結果:HJSON 格式。正在轉換為標準 JSON..."
IS_HJSON=1
yggdrasil -normaliseconf -useconffile "$CONFIG_FILE" -json > "$CURRENT_JSON"
if ! jq -e . "$CURRENT_JSON" >/dev/null 2>&1; then
log "ERROR" "HJSON 轉換為 JSON 失敗,請手動檢查配置文件語法是否正確!"
exit 1
fi
fi
# 3. 抓取節點
log "INFO" "正在獲取在線節點列表..."
PEERS_JSON=$(curl -s "${PROXY}${TARGET_URL}" | perl -0777 -ne 'while (/<tr.*?>.*?<td.*?>(.*?)<\/td>.*?<td.*?>(.*?)<\/td>.*?<\/tr>/gs) { $addr = $1; $status = $2; $addr =~ s/<[^>]+>//g; $status =~ s/<[^>]+>//g; if ($status =~ /online/i && $addr =~ /(tcp|tls|quic|ws|wss):\/\//) { print "$addr\n"; } }' | jq -R . | jq -s .)
# 4. 檢查抓取結果
if [ -z "$PEERS_JSON" ] || [ "$PEERS_JSON" == "[]" ]; then
log "ERROR" "獲取節點失敗,或當前無在線節點(列表為空)!"
exit 1
fi
PEER_COUNT=$(echo "$PEERS_JSON" | jq '. | length')
log "INFO" "成功獲取到 $PEER_COUNT 個在線節點。"
# 5. 更新配置到臨時文件
log "INFO" "正在將新節點寫入臨時配置文件..."
jq --argjson p "$PEERS_JSON" '.Peers = $p' "$CURRENT_JSON" > "$TMP_FILE" 2> "$JQ_ERR_LOG"
# 6. 嚴格驗證臨時文件並替換
log "INFO" "正在校驗生成的配置文件合法性..."
if [ $? -eq 0 ] && [ -s "$TMP_FILE" ] && jq -e . "$TMP_FILE" >/dev/null 2>&1; then
log "INFO" "✅ 校驗通過!"
# 備份
cp -p "$CONFIG_FILE" "$BACKUP_FILE"
log "INFO" "原配置文件已備份至 $BACKUP_FILE"
# 根據原格式寫回
if [ "$IS_HJSON" -eq 1 ]; then
log "INFO" "正在將配置轉換回 HJSON 格式並寫入..."
yggdrasil -normaliseconf -useconffile "$TMP_FILE" > "$CONFIG_FILE"
else
log "INFO" "正在寫入 JSON 格式配置..."
cat "$TMP_FILE" > "$CONFIG_FILE"
fi
log "INFO" "新配置已成功寫入 $CONFIG_FILE"
else
# 驗證失敗
log "ERROR" "❌ 錯誤:生成的配置文件異常!"
log "ERROR" "詳細報錯信息:$(cat "$JQ_ERR_LOG")"
log "INFO" "已自動終止,原配置文件未受到任何影響。"
exit 1
fi
# 7. 重啟服務
log "INFO" "正在重啟 Yggdrasil 服務..."
systemctl restart yggdrasil
if [ $? -eq 0 ]; then
log "SUCCESS" "✅ Yggdrasil 服務重啟成功!更新任務圓滿結束。"
else
log "WARN" "⚠️ 服務重啟命令執行完成,但返回了非 0 狀態,請手動檢查 systemctl status yggdrasil。"
fi
#!/bin/bash
# ================= 配置區域 =================
CONFIG_FILE="/etc/yggdrasil/yggdrasil.conf"
BACKUP_FILE="${CONFIG_FILE}.bak"
PROXY=""
TARGET_URL="https://publicpeers.neilalexander.dev/"
# ⚠️ 請確保已更換為新的 Token
TG_BOT_TOKEN="YOUR_NEW_TG_BOT_TOKEN"
TG_CHAT_ID="YOUR_TG_CHAT_ID"
MAX_PEERS=5 # 最大保留節點數
TIMEOUT=2 # 每個節點的測速超時時間(秒)
CONCURRENCY=20 # 並發測速線程數
# ============================================
# 初始化日誌緩衝區
LOG_BUFFER=""
# 創建臨時工作目錄
WORK_DIR=$(mktemp -d -t yggdrasil_update_XXXXXX)
if [ ! -d "$WORK_DIR" ]; then
echo "無法創建臨時目錄,腳本退出。"
exit 1
fi
CURRENT_JSON="${WORK_DIR}/current.json"
TMP_FILE="${WORK_DIR}/yggdrasil.conf.tmp"
JQ_ERR_LOG="${WORK_DIR}/jq_error.log"
RAW_PEERS_TXT="${WORK_DIR}/raw_peers.txt"
TEST_SCRIPT="${WORK_DIR}/test_peer.sh"
RESULTS_TXT="${WORK_DIR}/results.txt"
# 日誌記錄函數
log() {
local level=$(echo "$1" | tr 'a-z' 'A-Z')
shift
local msg="$*"
local timestamp=$(date +'%Y-%m-%d %H:%M:%S')
local color_reset='\033[0m'
local color_red='\033[0;31m'
local color_green='\033[0;32m'
local color_yellow='\033[0;33m'
local color_cyan='\033[0;36m'
local color_code=""
case "$level" in
INFO) color_code="${color_cyan}" ;;
WARN) color_code="${color_yellow}" ;;
ERROR) color_code="${color_red}" ;;
SUCCESS) color_code="${color_green}" ;;
*) color_code="${color_reset}" ;;
esac
echo -e "${color_code}[${timestamp}] [${level}] ${msg}${color_reset}"
LOG_BUFFER="${LOG_BUFFER}[${timestamp}] [${level}] ${msg}"$'\n'
}
# 發送 Telegram 通知
send_log_to_telegram() {
if [[ -z "$TG_BOT_TOKEN" || "$TG_BOT_TOKEN" == "YOUR_NEW_TG_BOT_TOKEN" || -z "$TG_CHAT_ID" ]]; then
return 0
fi
[ -z "$LOG_BUFFER" ] && return 0
echo -e "\033[0;36m正在發送日誌到 Telegram...\033[0m"
curl --retry 3 --retry-delay 2 -sS -k "${PROXY}https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \
--data-urlencode "chat_id=${TG_CHAT_ID}" \
--data-urlencode "text=${LOG_BUFFER}" >/dev/null 2>&1 || true
}
# 腳本退出時的清理函數
cleanup() {
send_log_to_telegram
if [ -d "$WORK_DIR" ]; then
rm -rf "$WORK_DIR"
fi
}
trap cleanup EXIT
# ================= 主程序開始 =================
log "INFO" "=== $(hostname) ==="
log "INFO" "=== 開始執行 Yggdrasil 節點測速更新任務 ==="
# 1. 檢查依賴
for cmd in jq curl openssl awk perl xargs; do
if ! command -v $cmd &> /dev/null; then
log "ERROR" "系統未安裝 $cmd,請先安裝依賴。"
exit 1
fi
done
# 2. 判斷配置格式並統一轉換為 JSON
log "INFO" "正在檢測並備份配置文件格式..."
IS_HJSON=0
if jq -e . "$CONFIG_FILE" >/dev/null 2>&1; then
cp "$CONFIG_FILE" "$CURRENT_JSON"
else
IS_HJSON=1
yggdrasil -normaliseconf -useconffile "$CONFIG_FILE" -json > "$CURRENT_JSON"
if ! jq -e . "$CURRENT_JSON" >/dev/null 2>&1; then
log "ERROR" "HJSON 轉換為 JSON 失敗,請手動檢查配置!"
exit 1
fi
fi
# 3. 抓取節點列表
log "INFO" "正在從官網抓取在線節點列表..."
curl -s "${PROXY}${TARGET_URL}" | perl -0777 -ne 'while (/<tr.*?>.*?<td.*?>(.*?)<\/td>.*?<td.*?>(.*?)<\/td>.*?<\/tr>/gs) { $addr = $1; $status = $2; $addr =~ s/<[^>]+>//g; $status =~ s/<[^>]+>//g; if ($status =~ /online/i && $addr =~ /(tcp|tls|quic|ws|wss):\/\//) { print "$addr\n"; } }' > "$RAW_PEERS_TXT"
TOTAL_FETCHED=$(wc -l < "$RAW_PEERS_TXT")
if [ "$TOTAL_FETCHED" -eq 0 ]; then
log "ERROR" "獲取節點失敗,或當前無在線節點!"
exit 1
fi
log "INFO" "成功獲取到 $TOTAL_FETCHED 個在線節點,開始測速過濾..."
# 4. 生成並發測速腳本 (包含你之前提到的 QUIC 自適應邏輯)
cat << 'EOF' > "$TEST_SCRIPT"
#!/bin/bash
URI="$1"
TIMEOUT="$2"
PROTO=$(echo "$URI" | awk -F'://' '{print $1}')
HOST_PORT=${URI#*://}
# 提取主機和端口
if [[ "$HOST_PORT" == \[*\]:* ]]; then
HOST_NO_BRACKET=$(echo "$HOST_PORT" | grep -o '\[.*\]' | tr -d '[]')
PORT=$(echo "$HOST_PORT" | grep -o ':[0-9]*$' | tr -d ':')
else
HOST_NO_BRACKET=$(echo "$HOST_PORT" | cut -d: -f1)
PORT=$(echo "$HOST_PORT" | cut -d: -f2)
fi
START=$(date +%s%3N)
SUCCESS=0
case "$PROTO" in
tcp)
timeout $TIMEOUT bash -c "</dev/tcp/$HOST_NO_BRACKET/$PORT" 2>/dev/null && SUCCESS=1
;;
tls)
echo -n | timeout $TIMEOUT openssl s_client -connect "$HOST_PORT" >/dev/null 2>&1 && SUCCESS=1
;;
ws|wss)
SCHEME="http"
[[ "$PROTO" == "wss" ]] && SCHEME="https"
if timeout $TIMEOUT curl -s -k -i -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" "${SCHEME}://${HOST_PORT}/" 2>/dev/null | grep -iq "HTTP/.* 101"; then
SUCCESS=1
fi
;;
quic)
if openssl s_client -help 2>&1 | grep -q -- "-quic"; then
echo -n | timeout $TIMEOUT openssl s_client -quic -connect "$HOST_PORT" >/dev/null 2>&1 && SUCCESS=1
else
timeout $TIMEOUT bash -c "</dev/udp/$HOST_NO_BRACKET/$PORT" 2>/dev/null && SUCCESS=1
fi
;;
esac
END=$(date +%s%3N)
LATENCY=$((END - START))
if [ "$SUCCESS" -eq 1 ]; then
printf "%05d %s\n" "$LATENCY" "$URI"
fi
EOF
chmod +x "$TEST_SCRIPT"
# 5. 執行多線程測速
cat "$RAW_PEERS_TXT" | xargs -P "$CONCURRENCY" -I {} bash "$TEST_SCRIPT" "{}" "$TIMEOUT" | sort -n > "$RESULTS_TXT"
WORKING_COUNT=$(wc -l < "$RESULTS_TXT")
if [ "$WORKING_COUNT" -eq 0 ]; then
log "ERROR" "所有節點測速均失敗,網絡可能中斷,停止更新!"
exit 1
fi
log "INFO" "測速完成,共找到 $WORKING_COUNT 個可用節點。"
log "INFO" "--- TOP $MAX_PEERS 最快節點清單 ---"
# 6. 【修復 Bug 的地方】使用進程替換取代管道,確保 LOG_BUFFER 生效
while read -r latency uri; do
# 將延遲前面的 0 砍掉,讓顯示更美觀 (例如 00125 變成 125)
clean_latency=$(echo "$latency" | sed 's/^0*//')
[ -z "$clean_latency" ] && clean_latency="0"
log "INFO" "延遲 ${clean_latency}ms -> $uri"
done < <(head -n "$MAX_PEERS" "$RESULTS_TXT")
PEERS_JSON=$(head -n "$MAX_PEERS" "$RESULTS_TXT" | awk '{print $2}' | jq -R . | jq -s .)
# 7. 更新配置到臨時文件並驗證
jq --argjson p "$PEERS_JSON" '.Peers = $p' "$CURRENT_JSON" > "$TMP_FILE" 2> "$JQ_ERR_LOG"
if [ $? -eq 0 ] && [ -s "$TMP_FILE" ] && jq -e . "$TMP_FILE" >/dev/null 2>&1; then
cp -p "$CONFIG_FILE" "$BACKUP_FILE"
if [ "$IS_HJSON" -eq 1 ]; then
yggdrasil -normaliseconf -useconffile "$TMP_FILE" > "$CONFIG_FILE"
else
cat "$TMP_FILE" > "$CONFIG_FILE"
fi
log "SUCCESS" "✅ 已成功將 $MAX_PEERS 個最快節點寫入配置!"
else
log "ERROR" "❌ 錯誤:生成的配置文件異常!更新中斷。"
exit 1
fi
# 8. 重啟服務
log "INFO" "正在重啟 Yggdrasil 服務..."
systemctl restart yggdrasil
if [ $? -eq 0 ]; then
log "SUCCESS" "🎉 Yggdrasil 服務重啟成功!"
else
log "WARN" "⚠️ 服務重啟遇到問題,請手動檢查。"
fi
#!/bin/bash
# ================= 配置區域 =================
CONFIG_FILE="/etc/yggdrasil/yggdrasil.conf"
BACKUP_FILE="${CONFIG_FILE}.bak"
PROXY=""
TARGET_URL="https://publicpeers.neilalexander.dev/"
# ⚠️ 請確保已更換為新的 Token
TG_BOT_TOKEN="YOUR_NEW_TG_BOT_TOKEN"
TG_CHAT_ID="YOUR_TG_CHAT_ID"
MAX_PEERS=5 # 最大保留抓取節點數
TIMEOUT=2 # 每個節點的測速超時時間(秒)
CONCURRENCY=20 # 並發測速線程數
# 預設的固定節點(將會始終保留在列表最前面)
# 請依照格式填寫,若無則保持空陣列 DEFAULT_PEERS=()
DEFAULT_PEERS=(
#"tcp://1.2.3.4:12345"
#"tls://5.6.7.8:443?sni=www.visa.com"
)
# ============================================
# 初始化日誌緩衝區
LOG_BUFFER=""
# 創建臨時工作目錄
WORK_DIR=$(mktemp -d -t yggdrasil_update_XXXXXX)
if [ ! -d "$WORK_DIR" ]; then
echo "無法創建臨時目錄,腳本退出。"
exit 1
fi
# 新增:定義日誌文件路徑
LOG_FILE="${WORK_DIR}/update_run.log"
CURRENT_JSON="${WORK_DIR}/current.json"
TMP_FILE="${WORK_DIR}/yggdrasil.conf.tmp"
JQ_ERR_LOG="${WORK_DIR}/jq_error.log"
RAW_PEERS_TXT="${WORK_DIR}/raw_peers.txt"
TEST_SCRIPT="${WORK_DIR}/test_peer.sh"
RESULTS_TXT="${WORK_DIR}/results.txt"
# 日誌記錄函數
log() {
local level=$(echo "$1" | tr 'a-z' 'A-Z')
shift
local msg="$*"
local timestamp=$(date +'%Y-%m-%d %H:%M:%S')
local color_reset='\033[0m'
local color_red='\033[0;31m'
local color_green='\033[0;32m'
local color_yellow='\033[0;33m'
local color_cyan='\033[0;36m'
local color_code=""
case "$level" in
INFO) color_code="${color_cyan}" ;;
WARN) color_code="${color_yellow}" ;;
ERROR) color_code="${color_red}" ;;
SUCCESS) color_code="${color_green}" ;;
*) color_code="${color_reset}" ;;
esac
# 1. 打印到終端 (帶顏色)
echo -e "${color_code}[${timestamp}] [${level}] ${msg}${color_reset}"
# 2. 存入變量,準備發給 Telegram (不帶顏色)
LOG_BUFFER="${LOG_BUFFER}[${timestamp}] [${level}] ${msg}"$'\n'
# 3. 追加寫入到 WORK_DIR 的日誌文件中 (不帶顏色,保持純文本乾淨)
echo "[${timestamp}] [${level}] ${msg}" >> "$LOG_FILE"
}
# 發送 Telegram 通知
send_log_to_telegram() {
if [[ -z "$TG_BOT_TOKEN" || "$TG_BOT_TOKEN" == "YOUR_NEW_TG_BOT_TOKEN" || -z "$TG_CHAT_ID" ]]; then
return 0
fi
[ -z "$LOG_BUFFER" ] && return 0
echo -e "\033[0;36m正在發送日誌到 Telegram...\033[0m"
curl --retry 3 --retry-delay 2 -sS -k "${PROXY}https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \
--data-urlencode "chat_id=${TG_CHAT_ID}" \
--data-urlencode "text=${LOG_BUFFER}" >/dev/null 2>&1 || true
}
# 腳本退出時的清理函數
cleanup() {
send_log_to_telegram
# 提示:如果你希望腳本運行完後保留這份日誌,可以取消下面這行的註釋
# 將日誌複製到系統的 /var/log 目錄下 (需 root 權限) 或者 /tmp 目錄下
# cp "$LOG_FILE" "/var/log/yggdrasil_update_last_run.log" 2>/dev/null
if [ -d "$WORK_DIR" ]; then
rm -rf "$WORK_DIR"
fi
}
trap cleanup EXIT
# ================= 主程序開始 =================
log "INFO" "=== $(hostname) ==="
log "INFO" "=== 開始執行 Yggdrasil 節點測速更新任務 ==="
# 1. 檢查依賴
for cmd in jq curl openssl awk perl xargs dig; do
if ! command -v $cmd &> /dev/null; then
log "ERROR" "系統未安裝 $cmd,請先安裝依賴。"
exit 1
fi
done
# 2. 判斷配置格式並統一轉換為 JSON
log "INFO" "正在檢測並備份配置文件格式..."
IS_HJSON=0
if jq -e . "$CONFIG_FILE" >/dev/null 2>&1; then
cp "$CONFIG_FILE" "$CURRENT_JSON"
else
IS_HJSON=1
yggdrasil -normaliseconf -useconffile "$CONFIG_FILE" -json > "$CURRENT_JSON"
if ! jq -e . "$CURRENT_JSON" >/dev/null 2>&1; then
log "ERROR" "HJSON 轉換為 JSON 失敗,請手動檢查配置!"
exit 1
fi
fi
# 3. 抓取節點列表
log "INFO" "正在從官網抓取在線節點列表..."
curl -s "${PROXY}${TARGET_URL}" | perl -0777 -ne 'while (/<tr.*?>.*?<td.*?>(.*?)<\/td>.*?<td.*?>(.*?)<\/td>.*?<\/tr>/gs) { $addr = $1; $status = $2; $addr =~ s/<[^>]+>//g; $status =~ s/<[^>]+>//g; if ($status =~ /online/i && $addr =~ /(tcp|tls|quic|ws|wss):\/\//) { print "$addr\n"; } }' > "$RAW_PEERS_TXT"
TOTAL_FETCHED=$(wc -l < "$RAW_PEERS_TXT")
if [ "$TOTAL_FETCHED" -eq 0 ]; then
log "ERROR" "獲取節點失敗,或當前無在線節點!"
exit 1
fi
log "INFO" "成功獲取到 $TOTAL_FETCHED 個在線節點,開始測速過濾..."
# 4. 生成並發測速腳本 (包含 DNS IP替換 與 SNI 修改)
cat << 'EOF' > "$TEST_SCRIPT"
#!/bin/bash
ORIG_URI="$1"
TIMEOUT="$2"
# 1. 使用正規表示式安全解析 URI
if [[ "$ORIG_URI" =~ ^([a-zA-Z]+)://(\[[a-fA-F0-9:]+\]|[^:/]+):([0-9]+)(.*)$ ]]; then
PROTO="${BASH_REMATCH[1]}"
HOST="${BASH_REMATCH[2]}"
PORT="${BASH_REMATCH[3]}"
REST="${BASH_REMATCH[4]}"
else
exit 1
fi
# 2. DNS 查詢邏輯:替換 Host 為 IP (使用 127.0.0.1:53 TCP)
if [[ "$HOST" != \[*\] ]] && ! [[ "$HOST" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
RESOLVED=$(dig +tcp @127.0.0.1 -p 53 +short A "$HOST" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
if [ -z "$RESOLVED" ]; then
RESOLVED=$(dig +tcp @127.0.0.1 -p 53 +short AAAA "$HOST" | grep -E '^[0-9a-fA-F:]+$' | head -n 1)
if [ -n "$RESOLVED" ]; then
RESOLVED="[$RESOLVED]"
fi
fi
if [ -n "$RESOLVED" ]; then
HOST="$RESOLVED"
fi
fi
# 3. 處理 SNI 參數 (僅針對 tls 和 quic)
if [[ "$PROTO" == "tls" || "$PROTO" == "quic" ]]; then
if [ -z "$REST" ]; then
REST="?sni=www.visa.com"
elif [[ "$REST" == *"sni="* ]]; then
REST=$(echo "$REST" | sed -E 's/sni=[^&]*/sni=www.visa.com/g')
else
if [[ "$REST" == *\?* ]]; then
REST="${REST}&sni=www.visa.com"
else
REST="${REST}?sni=www.visa.com"
fi
fi
fi
# 重組即將寫入配置文件的最終 URI
NEW_URI="${PROTO}://${HOST}:${PORT}${REST}"
# 4. 執行連通性測試
HOST_NO_BRACKET=$(echo "$HOST" | tr -d '[]')
START=$(date +%s%3N)
SUCCESS=0
case "$PROTO" in
tcp)
timeout $TIMEOUT bash -c "</dev/tcp/$HOST_NO_BRACKET/$PORT" 2>/dev/null && SUCCESS=1
;;
tls)
echo -n | timeout $TIMEOUT openssl s_client -servername www.visa.com -connect "${HOST_NO_BRACKET}:${PORT}" >/dev/null 2>&1 && SUCCESS=1
;;
ws|wss)
SCHEME="http"
[[ "$PROTO" == "wss" ]] && SCHEME="https"
PATH_QUERY="/"
if [[ "$REST" == /* ]]; then
PATH_QUERY="$REST"
elif [[ "$REST" == \?* ]]; then
PATH_QUERY="/$REST"
fi
if timeout $TIMEOUT curl -s -k -i -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" "${SCHEME}://${HOST_NO_BRACKET}:${PORT}${PATH_QUERY}" 2>/dev/null | grep -iq "HTTP/.* 101"; then
SUCCESS=1
fi
;;
quic)
if openssl s_client -help 2>&1 | grep -q -- "-quic"; then
echo -n | timeout $TIMEOUT openssl s_client -quic -servername www.visa.com -connect "${HOST_NO_BRACKET}:${PORT}" >/dev/null 2>&1 && SUCCESS=1
else
timeout $TIMEOUT bash -c "</dev/udp/$HOST_NO_BRACKET/$PORT" 2>/dev/null && SUCCESS=1
fi
;;
esac
END=$(date +%s%3N)
LATENCY=$((END - START))
if [ "$SUCCESS" -eq 1 ]; then
printf "%05d %s\n" "$LATENCY" "$NEW_URI"
fi
EOF
chmod +x "$TEST_SCRIPT"
# 5. 執行多線程測速
cat "$RAW_PEERS_TXT" | xargs -P "$CONCURRENCY" -I {} bash "$TEST_SCRIPT" "{}" "$TIMEOUT" | sort -n > "$RESULTS_TXT"
WORKING_COUNT=$(wc -l < "$RESULTS_TXT")
if [ "$WORKING_COUNT" -eq 0 ]; then
log "ERROR" "所有節點測速均失敗,網絡可能中斷,停止更新!"
exit 1
fi
log "INFO" "測速完成,共找到 $WORKING_COUNT 個可用節點。"
log "INFO" "--- 預設節點 + TOP $MAX_PEERS 最快節點清單 ---"
# 6. 處理預設節點並轉換為 JSON 格式
if [ ${#DEFAULT_PEERS[@]} -eq 0 ]; then
DEFAULT_PEERS_JSON="[]"
else
for dp in "${DEFAULT_PEERS[@]}"; do
log "INFO" "預設節點 -> $dp"
done
DEFAULT_PEERS_JSON=$(printf '%s\n' "${DEFAULT_PEERS[@]}" | jq -R . | jq -s .)
fi
# 輸出日誌並生成抓取節點的 JSON 陣列
while read -r latency uri; do
clean_latency=$(echo "$latency" | sed 's/^0*//')
[ -z "$clean_latency" ] && clean_latency="0"
log "INFO" "延遲 ${clean_latency}ms -> $uri"
done < <(head -n "$MAX_PEERS" "$RESULTS_TXT")
PEERS_JSON=$(head -n "$MAX_PEERS" "$RESULTS_TXT" | awk '{print $2}' | jq -R . | jq -s .)
# 7. 更新配置到臨時文件並驗證
jq --argjson def "$DEFAULT_PEERS_JSON" --argjson p "$PEERS_JSON" '.Peers = $def + $p' "$CURRENT_JSON" > "$TMP_FILE" 2> "$JQ_ERR_LOG"
if [ $? -eq 0 ] && [ -s "$TMP_FILE" ] && jq -e . "$TMP_FILE" >/dev/null 2>&1; then
cp -p "$CONFIG_FILE" "$BACKUP_FILE"
if [ "$IS_HJSON" -eq 1 ]; then
yggdrasil -normaliseconf -useconffile "$TMP_FILE" > "$CONFIG_FILE"
else
cat "$TMP_FILE" > "$CONFIG_FILE"
fi
log "SUCCESS" "✅ 已成功將配置寫入文件!"
else
log "ERROR" "❌ 錯誤:生成的配置文件異常!更新中斷。"
exit 1
fi
# 8. 重啟服務
log "INFO" "正在重啟 Yggdrasil 服務..."
systemctl restart yggdrasil
if [ $? -eq 0 ]; then
log "SUCCESS" "🎉 Yggdrasil 服務重啟成功!"
else
log "WARN" "⚠️ 服務重啟遇到問題,請手動檢查。"
fi
#!/bin/bash
# ==========================================
# Yggdrasil 一鍵安裝與卸載腳本
# 適用系統: Debian / Ubuntu
# ==========================================
# 確保以 root 權限執行
if [ "$EUID" -ne 0 ]; then
echo -e "\033[0;31m[ERROR] 請使用 root 權限 (sudo) 執行此腳本。\033[0m"
exit 1
fi
WORK_DIR=""
ACTION=""
CUSTOM_PASSWORD=""
BACKUP_DIR="${HOME}/.backup/yggdrasil"
# 日誌記錄函數
log() {
local level=$(echo "$1" | tr 'a-z' 'A-Z')
shift
local msg="$*"
local timestamp=$(date +'%Y-%m-%d %H:%M:%S')
local color_reset='\033[0m'
local color_red='\033[0;31m'
local color_green='\033[0;32m'
local color_yellow='\033[0;33m'
local color_cyan='\033[0;36m'
local color_code=""
case "$level" in
INFO) color_code="${color_cyan}" ;;
WARN) color_code="${color_yellow}" ;;
ERROR) color_code="${color_red}" ;;
SUCCESS) color_code="${color_green}" ;;
*) color_code="${color_reset}" ;;
esac
echo -e "${color_code}[${timestamp}] [${level}] ${msg}${color_reset}"
}
# 退出時的清理函數
cleanup() {
if [ -n "$WORK_DIR" ] && [ -d "$WORK_DIR" ]; then
rm -rf "$WORK_DIR"
log "INFO" "系統臨時目錄 $WORK_DIR 已清理。"
fi
}
# 綁定 EXIT 信號
trap cleanup EXIT
# 獲取系統架構
get_arch() {
local arch=$(uname -m)
case "$arch" in
x86_64) echo "amd64" ;;
aarch64) echo "arm64" ;;
armv7l) echo "armhf" ;;
i386) echo "i386" ;;
*)
log "ERROR" "不支持的系統架構: $arch"
exit 1
;;
esac
}
# ================= 獨立函數:修改配置與還原密鑰 =================
modify_yggdrasil_config() {
local pwd="$1"
local config_file="/etc/yggdrasil.conf"
local temp_json="${WORK_DIR}/temp.json"
local modified_json="${WORK_DIR}/modified.json"
local is_hjson=0
log "INFO" "開始修改配置 (AdminListen, IfName, Listen, 密鑰還原)..."
if [ ! -f "$config_file" ]; then
log "ERROR" "找不到配置文件 $config_file"
return 1
fi
# 1. 判斷配置格式是否為 JSON,不是則視為 HJSON 並轉換為 JSON
if jq -e . "$config_file" >/dev/null 2>&1; then
cp "$config_file" "$temp_json"
else
is_hjson=1
yggdrasil -normaliseconf -useconffile "$config_file" -json > "$temp_json"
fi
# 2. 準備基礎的 jq 修改指令
local jq_cmd='.AdminListen = "unix:///var/run/yggdrasil/yggdrasil.sock" | .IfName = "ygg0" | .Listen = ["quic://[::]:8000?password=" + $pw]'
# 3. 尋找並加載最新備份的密鑰
local priv_key=""
local pub_key=""
local latest_backup=$(ls -t "${BACKUP_DIR}"/keys_*.json 2>/dev/null | head -n 1)
if [ -n "$latest_backup" ] && [ -f "$latest_backup" ]; then
log "INFO" "發現歷史備份密鑰,正在從 ${latest_backup} 讀取..."
priv_key=$(jq -r '.PrivateKey // empty' "$latest_backup")
pub_key=$(jq -r '.PublicKey // empty' "$latest_backup")
fi
# 4. 根據是否找到密鑰,執行對應的 jq 覆寫邏輯
if [ -n "$priv_key" ] && [ -n "$pub_key" ]; then
log "SUCCESS" "✅ 已加載歷史密鑰,即將替換掉全新生成的密鑰,保持舊 IP 節點不變!"
jq_cmd="${jq_cmd} | .PrivateKey = \$priv | .PublicKey = \$pub"
jq --arg pw "$pwd" --arg priv "$priv_key" --arg pub "$pub_key" "$jq_cmd" "$temp_json" > "$modified_json"
else
log "INFO" "未找到有效的備份密鑰,將使用全新生成的節點密鑰。"
jq --arg pw "$pwd" "$jq_cmd" "$temp_json" > "$modified_json"
fi
# 5. 根據原始格式寫回配置文件
if [ "$is_hjson" -eq 1 ]; then
log "INFO" "檢測到原始配置為 HJSON,轉換回 HJSON 格式並保存..."
yggdrasil -normaliseconf -useconffile "$modified_json" > "$config_file"
else
log "INFO" "檢測到原始配置為 JSON,直接保存..."
cat "$modified_json" > "$config_file"
fi
}
# ================= 核心功能:安裝 =================
install_yggdrasil() {
log "INFO" "=== 開始安裝最新版 Yggdrasil ==="
# 檢查必要依賴
if ! command -v curl &> /dev/null || ! command -v jq &> /dev/null; then
log "INFO" "正在安裝必要的依賴 (curl, jq)..."
apt-get update -y > /dev/null
apt-get install -y curl jq > /dev/null
fi
# 獲取最新版本號 (從 GitHub API)
log "INFO" "正在查詢 Yggdrasil 最新版本..."
local api_url="https://api.github.com/repos/yggdrasil-network/yggdrasil-go/releases/latest"
local latest_tag=$(curl -s "$api_url" | jq -r .tag_name)
if [ -z "$latest_tag" ] || [ "$latest_tag" == "null" ]; then
log "ERROR" "獲取最新版本失敗,請檢查網路連線。"
exit 1
fi
local version_num=${latest_tag#v}
local arch=$(get_arch)
log "SUCCESS" "發現最新版本: $latest_tag,系統架構: $arch"
# 檢查是否已安裝相同版本
if command -v yggdrasil &> /dev/null; then
local current_version=$(yggdrasil -version | awk '{print $3}')
if [ "$current_version" == "$latest_tag" ] || [ "$current_version" == "$version_num" ]; then
log "SUCCESS" "當前已安裝最新版本 ($current_version),無需更新二進位文件。"
else
log "WARN" "發現舊版本 ($current_version),即將進行升級..."
WORK_DIR=$(mktemp -d -t yggdrasil_install_XXXXXX)
local deb_file="yggdrasil-${version_num}-${arch}.deb"
local download_url="https://github.com/yggdrasil-network/yggdrasil-go/releases/download/${latest_tag}/${deb_file}"
log "INFO" "正在下載: $download_url"
curl -L -o "${WORK_DIR}/${deb_file}" "$download_url"
if [ ! -s "${WORK_DIR}/${deb_file}" ]; then
log "ERROR" "下載失敗或文件為空!"
exit 1
fi
log "INFO" "正在安裝 Yggdrasil..."
dpkg -i "${WORK_DIR}/${deb_file}"
apt-get install -f -y > /dev/null
fi
else
WORK_DIR=$(mktemp -d -t yggdrasil_install_XXXXXX)
local deb_file="yggdrasil-${version_num}-${arch}.deb"
local download_url="https://github.com/yggdrasil-network/yggdrasil-go/releases/download/${latest_tag}/${deb_file}"
log "INFO" "正在下載: $download_url"
curl -L -o "${WORK_DIR}/${deb_file}" "$download_url"
if [ ! -s "${WORK_DIR}/${deb_file}" ]; then
log "ERROR" "下載失敗或文件為空!"
exit 1
fi
log "INFO" "正在安裝 Yggdrasil..."
dpkg -i "${WORK_DIR}/${deb_file}"
apt-get install -f -y > /dev/null
fi
# 確保系統建立了臨時工作目錄
if [ -z "$WORK_DIR" ]; then
WORK_DIR=$(mktemp -d -t yggdrasil_install_XXXXXX)
fi
systemctl enable yggdrasil
systemctl restart yggdrasil
if systemctl is-active --quiet yggdrasil; then
log "SUCCESS" "✅ Yggdrasil 服務已啟動並設置為開機自啟!"
# 生成默認配置(如果不存在)
local config_file="/etc/yggdrasil.conf"
if [ ! -f "$config_file" ]; then
log "INFO" "生成初始配置文件到 $config_file..."
yggdrasil -genconf > "$config_file"
fi
# 處理密碼
local peer_password="$CUSTOM_PASSWORD"
if [ -z "$peer_password" ]; then
log "INFO" "未通過 --password 指定密碼,正在自動生成 32 位隨機密碼..."
peer_password=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
fi
log "SUCCESS" "QUIC 監聽密碼設定為: $peer_password"
# 調用獨立配置函數 (裡面會進行修改和金鑰還原)
modify_yggdrasil_config "$peer_password"
# 重啟服務應用修改
log "INFO" "應用配置變更,重啟 Yggdrasil 服務..."
systemctl restart yggdrasil
if systemctl is-active --quiet yggdrasil; then
log "SUCCESS" "✅ 配置修改成功且服務運行正常!"
else
log "ERROR" "❌ 服務重啟失敗,請檢查系統日誌。"
fi
else
log "ERROR" "❌ 安裝完成,但服務啟動失敗,請使用 systemctl status yggdrasil 檢查。"
fi
}
# ================= 核心功能:卸載 =================
uninstall_yggdrasil() {
log "INFO" "=== 開始卸載 Yggdrasil ==="
if ! command -v yggdrasil &> /dev/null; then
log "WARN" "系統中未檢測到 Yggdrasil,無需卸載。"
exit 0
fi
# =============== 備份密鑰邏輯 ===============
local config_file="/etc/yggdrasil.conf"
if [ -f "$config_file" ]; then
log "INFO" "正在備份現有的 Yggdrasil 金鑰對 (以保留 IPv6 位址)..."
mkdir -p "$BACKUP_DIR"
WORK_DIR=$(mktemp -d -t yggdrasil_uninstall_XXXXXX)
local temp_json="${WORK_DIR}/backup_temp.json"
if jq -e . "$config_file" >/dev/null 2>&1; then
cp "$config_file" "$temp_json"
else
yggdrasil -normaliseconf -useconffile "$config_file" -json > "$temp_json" 2>/dev/null
fi
if [ -f "$temp_json" ]; then
local timestamp=$(date +'%Y%m%d_%H%M%S')
local backup_file="${BACKUP_DIR}/keys_${timestamp}.json"
# 提取並備份 PublicKey 與 PrivateKey
jq '{PrivateKey: .PrivateKey, PublicKey: .PublicKey}' "$temp_json" > "$backup_file"
if [ -s "$backup_file" ] && [ "$(jq -r '.PrivateKey // empty' "$backup_file")" != "" ]; then
log "SUCCESS" "✅ 金鑰已成功備份至: $backup_file"
else
log "WARN" "⚠️ 金鑰備份失敗或配置中無金鑰,已忽略。"
rm -f "$backup_file"
fi
fi
fi
# ==========================================
# 停止並禁用服務
log "INFO" "正在停止並禁用 yggdrasil 服務..."
systemctl stop yggdrasil || true
systemctl disable yggdrasil || true
# 卸載軟體包
log "INFO" "正在移除軟體包..."
apt-get remove --purge -y yggdrasil
# 清理可能殘留的自定義目錄
log "INFO" "正在清理配置文件與殘留目錄..."
rm -f /etc/yggdrasil.conf
rm -f /etc/yggdrasil.conf.bak
rm -rf /usr/local/etc/yggdrasil/
# 重新加載 systemd
systemctl daemon-reload
log "SUCCESS" "✅ Yggdrasil 已從系統中完全卸載。"
}
# ================= 參數解析 =================
while [[ $# -gt 0 ]]; do
case "$1" in
install)
ACTION="install"
shift
;;
uninstall)
ACTION="uninstall"
shift
;;
--password)
CUSTOM_PASSWORD="$2"
shift 2
;;
*)
echo -e "\033[0;31m[ERROR] 未知參數: $1\033[0m"
echo -e "使用方法: $0 {install|uninstall} [--password <密碼>]"
echo -e " \033[0;32minstall\033[0m - 自動檢測最新版本並安裝 (若已安裝則升級配置)"
echo -e " \033[0;32m --password XXX\033[0m - (可選) 設置 QUIC 監聽密碼。未指定則隨機生成32位密碼"
echo -e " \033[0;31muninstall\033[0m - 完全卸載並備份金鑰,清理服務與配置"
exit 1
;;
esac
done
# 根據解析的動作執行
if [ "$ACTION" == "install" ]; then
install_yggdrasil
elif [ "$ACTION" == "uninstall" ]; then
uninstall_yggdrasil
else
echo -e "\033[0;31m[ERROR] 請指定動作 (install 或 uninstall)。\033[0m"
echo -e "示例: $0 install --password mysecret123"
exit 1
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment