342 lines
10 KiB
Bash
342 lines
10 KiB
Bash
#!/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 排除,请勿提交或外传"
|