Files
2026-08-05 17:12:18 +08:00

342 lines
10 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
# ============================================
# Clash.Meta 配置生成器
#
# 从 deploy.sh 输出的节点信息文件(*info*.md,内含 vless:// 链接)
# 生成 Clash.Meta / mihomo 可用的 YAML 配置。
#
# 用法:
# bash gen-clash.sh # 交互式选择节点文件
# bash gen-clash.sh a.md b.md # 直接指定
# bash gen-clash.sh -o my.yaml a.md # 指定输出文件
# bash gen-clash.sh --all # 使用全部 *info*.md
#
# 本脚本仅在本地运行,不会上传到 VPS。
# ============================================
# 不切换工作目录:候选文件在脚本所在目录里找,
# 但命令行显式传入的路径仍相对调用者的当前目录解析
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; CYAN='\033[0;36m'; NC='\033[0m'
log() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
OUTPUT="${SCRIPT_DIR}/vpn-vps-xray-optimized.yaml"
USE_ALL=0
FILES=()
usage() {
sed -n '4,17p' "${BASH_SOURCE[0]}" | sed 's/^# \?//'
exit 0
}
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output) OUTPUT="${2:?-o 需要一个文件名}"; shift 2 ;;
--all) USE_ALL=1; shift ;;
-h|--help) usage ;;
-*) error "未知参数: $1"; usage ;;
*) FILES+=("$1"); shift ;;
esac
done
# ===== 收集候选文件 =====
mapfile -t CANDIDATES < <(ls -1 "${SCRIPT_DIR}"/*info*.md 2>/dev/null || true)
if [ "${#FILES[@]}" -eq 0 ]; then
if [ "${#CANDIDATES[@]}" -eq 0 ]; then
error "当前目录下找不到任何 *info*.md 节点信息文件"
error "请先在 VPS 上执行 bash deploy.sh --show,把输出保存为 xxx-info.md 放到这里"
exit 1
fi
if [ "$USE_ALL" -eq 1 ]; then
FILES=("${CANDIDATES[@]}")
else
echo ""
echo -e "${CYAN}可用的节点信息文件:${NC}"
for i in "${!CANDIDATES[@]}"; do
f="${CANDIDATES[$i]}"
# 顺带把链接里的备注名显示出来,方便辨认
tag=$(grep -m1 -o 'vless://[^[:space:]]*' "$f" 2>/dev/null | sed 's/.*#//' || true)
printf " %2d) %-45s %s\n" "$((i + 1))" "${f#"${SCRIPT_DIR}"/}" "${tag:+[${tag}]}"
done
echo ""
read -r -p "选择要写入配置的节点(空格分隔序号,回车=全选): " picks
if [ -z "${picks// /}" ]; then
FILES=("${CANDIDATES[@]}")
else
for n in $picks; do
if ! [[ "$n" =~ ^[0-9]+$ ]] || [ "$n" -lt 1 ] || [ "$n" -gt "${#CANDIDATES[@]}" ]; then
error "无效序号: $n"
exit 1
fi
FILES+=("${CANDIDATES[$((n - 1))]}")
done
fi
fi
fi
# ===== 工具函数 =====
urldecode() {
local s="${1//+/ }"
printf '%b' "${s//%/\\x}"
}
# 从 query string 中取出指定参数
qs_get() {
local query="$1" key="$2" kv
local IFS='&'
for kv in $query; do
if [ "${kv%%=*}" = "$key" ]; then
urldecode "${kv#*=}"
return
fi
done
}
# ===== 解析所有节点 =====
NAMES=(); SERVERS=(); PORTS=(); UUIDS=(); SNIS=(); PBKS=(); SIDS=(); FPS=(); FLOWS=(); SECS=()
declare -A name_seen=()
for f in "${FILES[@]}"; do
[ -f "$f" ] || { error "文件不存在: $f"; exit 1; }
link=$(grep -m1 -o 'vless://[^[:space:]]*' "$f" 2>/dev/null || true)
if [ -z "$link" ]; then
warn "跳过 ${f#"${SCRIPT_DIR}"/}:未找到 vless:// 链接"
continue
fi
rest="${link#vless://}"
# 备注名(# 之后)
if [[ "$rest" == *"#"* ]]; then
name=$(urldecode "${rest##*#}")
rest="${rest%%#*}"
else
name=""
fi
uuid="${rest%%@*}"
hpq="${rest#*@}"
hostport="${hpq%%\?*}"
query=""
[[ "$hpq" == *"?"* ]] && query="${hpq#*\?}"
# 兼容 IPv6 字面量 [::1]:443
if [[ "$hostport" == \[* ]]; then
server="${hostport%%]*}]"
port="${hostport##*]:}"
else
server="${hostport%%:*}"
port="${hostport##*:}"
fi
sec=$(qs_get "$query" security); sec="${sec:-none}"
sni=$(qs_get "$query" sni)
pbk=$(qs_get "$query" pbk)
sid=$(qs_get "$query" sid)
fp=$(qs_get "$query" fp); fp="${fp:-chrome}"
flow=$(qs_get "$query" flow)
net=$(qs_get "$query" type); net="${net:-tcp}"
if [ -z "$uuid" ] || [ -z "$server" ] || [ -z "$port" ]; then
error "${f#"${SCRIPT_DIR}"/} 的链接缺少 uuid/server/port,无法解析"
exit 1
fi
if [ "$sec" = "reality" ] && { [ -z "$pbk" ] || [ -z "$sni" ]; }; then
error "${f#"${SCRIPT_DIR}"/} 是 reality 节点但缺少 pbk 或 sni"
exit 1
fi
if [ "$net" != "tcp" ]; then
warn "${f#"${SCRIPT_DIR}"/}: transport=${net},本生成器目前只支持 tcp,已跳过"
continue
fi
[ -z "$name" ] && name="${server}"
# 名称去重,Clash 要求 proxy name 唯一
base="$name"; n=2
while [ -n "${name_seen[$name]:-}" ]; do
name="${base}-${n}"; n=$((n + 1))
done
name_seen["$name"]=1
NAMES+=("$name"); SERVERS+=("$server"); PORTS+=("$port"); UUIDS+=("$uuid")
SNIS+=("$sni"); PBKS+=("$pbk"); SIDS+=("$sid")
FPS+=("$fp"); FLOWS+=("$flow"); SECS+=("$sec")
log "解析成功: ${name} (${server}:${port}, ${sec})"
done
if [ "${#NAMES[@]}" -eq 0 ]; then
error "没有解析出任何可用节点"
exit 1
fi
# ===== 备份既有输出 =====
if [ -f "$OUTPUT" ]; then
cp "$OUTPUT" "${OUTPUT}.bak"
log "已备份原配置: ${OUTPUT}.bak"
fi
# ===== 生成 YAML =====
{
cat <<'HEADER'
# ============================================================
# 由 gen-clash.sh 自动生成,请勿手工编辑(重新生成会覆盖)
#
# ⚠ 需要 Clash.Meta / mihomo 内核(Clash Verge Rev、ClashX Meta、
# FlClash 等)。原版 Clash / Clash Premium 不支持 VLESS + Reality。
#
# ⚠ 本文件含真实 UUID 与服务器地址,等同凭据,切勿提交到版本库或外传。
# ============================================================
mixed-port: 7890
allow-lan: false
mode: rule
log-level: info
# fake-ip 下开启嗅探,才能从流量中还原域名,让域名类规则对直连 IP 也生效
sniffer:
enable: true
sniff:
HTTP:
ports: [80, 8080-8880]
override-destination: true
TLS:
ports: [443, 8443]
QUIC:
ports: [443, 8443]
skip-domain:
- "+.push.apple.com"
dns:
enable: true
ipv6: false
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
# 这些域名必须拿到真实 IP,否则局域网发现、时间同步、
# 网络连通性检测等会出问题
fake-ip-filter:
- "*.lan"
- "*.local"
- "*.localdomain"
- "*.home.arpa"
- "+.msftconnecttest.com"
- "+.msftncsi.com"
- "time.*.com"
- "ntp.*.com"
- "+.pool.ntp.org"
- "localhost.ptlogin2.qq.com"
# 用于解析下面 fallback 里 DoH 服务器自身的域名
default-nameserver:
- 223.5.5.5
- 119.29.29.29
nameserver:
- 223.5.5.5
- 119.29.29.29
# fallback 的意义是绕过投毒,必须用加密 DNS——
# 明文 UDP 的 8.8.8.8 在国内同样会被污染,等于没有 fallback
fallback:
- https://1.1.1.1/dns-query
- https://dns.google/dns-query
fallback-filter:
geoip: true
geoip-code: CN
ipcidr:
- 240.0.0.0/4
- 0.0.0.0/32
proxies:
HEADER
for i in "${!NAMES[@]}"; do
echo " - name: \"${NAMES[$i]}\""
echo " type: vless"
echo " server: ${SERVERS[$i]}"
echo " port: ${PORTS[$i]}"
echo " uuid: ${UUIDS[$i]}"
echo " network: tcp"
echo " udp: true"
if [ "${SECS[$i]}" = "reality" ]; then
echo " tls: true"
[ -n "${FLOWS[$i]}" ] && echo " flow: ${FLOWS[$i]}"
echo " servername: ${SNIS[$i]}"
echo " client-fingerprint: ${FPS[$i]}"
echo " reality-opts:"
echo " public-key: ${PBKS[$i]}"
[ -n "${SIDS[$i]}" ] && echo " short-id: ${SIDS[$i]}"
else
echo " tls: false"
fi
echo ""
done
cat <<'GROUPS_HEAD'
proxy-groups:
- name: "Proxy"
type: select
proxies:
- Auto-Select
GROUPS_HEAD
for n in "${NAMES[@]}"; do echo " - \"${n}\""; done
echo " - DIRECT"
echo ""
cat <<'AUTO_HEAD'
- name: "Auto-Select"
type: url-test
url: "http://www.gstatic.com/generate_204"
interval: 300
tolerance: 50
proxies:
AUTO_HEAD
for n in "${NAMES[@]}"; do echo " - \"${n}\""; done
echo ""
echo "rules:"
echo " # 代理服务器自身必须直连。否则 TUN / 系统代理会把「连接代理服务器」"
echo " # 这件事本身也丢进代理,形成回环,表现为 REALITY 握手拿到真证书。"
declare -A srv_seen=()
for s in "${SERVERS[@]}"; do
[ -n "${srv_seen[$s]:-}" ] && continue
srv_seen["$s"]=1
if [[ "$s" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo " - IP-CIDR,${s}/32,DIRECT,no-resolve"
elif [[ "$s" == \[*\] ]]; then
echo " - IP-CIDR6,${s//[\[\]]/}/128,DIRECT,no-resolve"
else
echo " - DOMAIN,${s},DIRECT"
fi
done
cat <<'RULES'
# 本机与局域网直连(fake-ip 下不加这条,路由器/NAS 会被丢进代理)
- IP-CIDR,127.0.0.0/8,DIRECT,no-resolve
- IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
- IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
- IP-CIDR,169.254.0.0/16,DIRECT,no-resolve
# 域名规则放在 IP 规则之前:GEOSITE 直接按域名命中,
# 不必先做一次真实 DNS 解析(fake-ip 下 GEOIP 前置会触发额外解析)
- GEOSITE,cn,DIRECT
- GEOIP,CN,DIRECT
# 兜底。MATCH 是终结规则,其后任何规则都不可达
- MATCH,Proxy
RULES
} > "$OUTPUT"
echo ""
log "已生成: ${OUTPUT}${#NAMES[@]} 个节点)"
for n in "${NAMES[@]}"; do echo " - ${n}"; done
echo ""
warn "该文件含真实凭据,已被根 .gitignore 排除,请勿提交或外传"