feat: 多订阅、地区分组、回国模式、多入站、GEO 管理
回国模式生成 .mrs 规则集而不是用 GEOIP:国内站挂在境外 CDN 时解析出的 IP 不在本国段,只靠 GEOIP 会整片漏掉,所以域名维度打头、IP 维度兜底。 GEO 菜单列出数据文件的大小和更新时间,因为下载失败是静默的 —— 规则 一条都匹配不上,流量全落到 MATCH,看起来像「配置没生效」。 入站每条写成一行带标记的 YAML 流式映射,增删就是加一行删一行。
This commit is contained in:
+456
-81
@@ -17,6 +17,8 @@ SELF_VERSION="2.0.0"
|
||||
|
||||
REPO="MetaCubeX/mihomo"
|
||||
UI_ZIP="https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip"
|
||||
# 测延迟用的地址:返回 204 空响应,够轻,全球都能连
|
||||
HEALTH_URL="https://www.gstatic.com/generate_204"
|
||||
|
||||
BIN="${MIHOMO_BIN:-/usr/local/bin/mihomo}"
|
||||
CONF_DIR="${MIHOMO_CONF_DIR:-/etc/mihomo}"
|
||||
@@ -524,6 +526,44 @@ cfg_apply() { # cfg_apply <备份路径> — 校验并按
|
||||
return 1
|
||||
}
|
||||
|
||||
# 订阅里的节点名千奇百怪,用一串关键词把常见写法都罩住。(?i) 是忽略大小写。
|
||||
region_filter() {
|
||||
case "$1" in
|
||||
HK) printf '(?i)香港|hk|hong ?kong|🇭🇰' ;;
|
||||
TW) printf '(?i)台湾|台灣|tw|taiwan|🇹🇼' ;;
|
||||
JP) printf '(?i)日本|jp|japan|东京|大阪|🇯🇵' ;;
|
||||
SG) printf '(?i)新加坡|sg|singapore|狮城|獅城|🇸🇬' ;;
|
||||
US) printf '(?i)美国|美國|us|united ?states|usa|🇺🇸' ;;
|
||||
KR) printf '(?i)韩国|韓國|kr|korea|首尔|首爾|🇰🇷' ;;
|
||||
UK|GB) printf '(?i)英国|英國|uk|united ?kingdom|london|🇬🇧' ;;
|
||||
DE) printf '(?i)德国|德國|de|germany|🇩🇪' ;;
|
||||
FR) printf '(?i)法国|法國|fr|france|🇫🇷' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
REGIONS_SUPPORTED="HK TW JP SG US KR UK DE FR"
|
||||
|
||||
cc_normalize() { # 两位国家代码,转大写;不合格式就返回空并说明
|
||||
local cc; cc="$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]')"
|
||||
if [[ "$cc" =~ ^[A-Z]{2}$ ]]; then
|
||||
printf '%s' "$cc"
|
||||
else
|
||||
warn " «$1» 不是两位国家代码,这条地区规则就先不写了,之后可以手动加"
|
||||
printf ''
|
||||
fi
|
||||
}
|
||||
|
||||
# meta-rules-dat 的单个规则集。.mrs 是二进制格式,比整包 geosite.dat 小得多,
|
||||
# 只下你真正要用的那几个列表。走 gh_url 所以 --mirror 对它同样生效。
|
||||
mrs_url() { gh_url "MetaCubeX/meta-rules-dat/raw/meta/geo/$1/$2.mrs"; }
|
||||
|
||||
cfg_group_names() { # 配置里现有的策略组名(入站选出站时用)
|
||||
local f; f="$(conf_file)"
|
||||
[[ -f "$f" ]] || return 0
|
||||
# 组名后面常跟着行内注释,先切到第一个 # 为止,再去掉尾部空格
|
||||
sed -n -E 's/^ - name:[[:space:]]*([^#]*).*$/\1/p' "$f" | sed -E 's/[[:space:]]+$//'
|
||||
}
|
||||
|
||||
cfg_wizard() { # 生成一份全新的 config.yaml
|
||||
local f; f="$(conf_file)"
|
||||
mkdir -p "$CONF_DIR"
|
||||
@@ -533,21 +573,35 @@ cfg_wizard() { # 生成一份全新的 config.yaml
|
||||
log "${D} 生成的是一份最小可用配置:一个订阅、一组选择/自动测速分组、内网直连其余走代理。${N}"
|
||||
log ""
|
||||
|
||||
local sub port lan ctrl_mode ctrl secret ui rules_mode
|
||||
local old_sub old_port old_ctrl old_secret
|
||||
old_sub="$(cfg_sub_url || true)"; old_port="$(cfg_get 'mixed-port' || true)"
|
||||
local port lan ctrl_mode ctrl secret ui rules_mode
|
||||
local old_list old_port old_ctrl old_secret
|
||||
old_list="$(cfg_sub_list || true)"; old_port="$(cfg_get 'mixed-port' || true)"
|
||||
old_ctrl="$(cfg_get 'external-controller' || true)"; old_secret="$(cfg_get 'secret' || true)"
|
||||
|
||||
local tries=0
|
||||
# ── 订阅:可以填多个 ──
|
||||
local -a subs=()
|
||||
if [[ -n "$old_list" ]]; then
|
||||
log " ${D}现有订阅(回车=沿用,输入新地址=覆盖):${N}"
|
||||
while IFS=$'\t' read -r _i _n _u; do [[ -n "$_u" ]] && log " $_i. $_u"; done <<<"$old_list"
|
||||
fi
|
||||
local n=1 s dflt tries=0
|
||||
while :; do
|
||||
ask_default " 订阅链接(机场给的那个 http(s) 地址)" "${old_sub:-}" sub
|
||||
[[ -n "$sub" ]] && break
|
||||
if (( ++tries >= 3 )); then
|
||||
warn " 一直没拿到订阅链接,向导先退出了,配置没有改动"
|
||||
return 1
|
||||
dflt="$(sed -n "${n}p" <<<"$old_list" | cut -f3)"
|
||||
if (( n == 1 )); then
|
||||
ask_default " 订阅 #1(机场给的 http(s) 地址)" "$dflt" s
|
||||
if [[ -z "$s" ]]; then
|
||||
(( ++tries >= 3 )) && { warn " 一直没拿到订阅链接,向导先退出了,配置没有改动"; return 1; }
|
||||
warn " 至少要有一个订阅,一个节点都没有 mihomo 启动不了"
|
||||
continue
|
||||
fi
|
||||
else
|
||||
ask_default " 订阅 #${n}(有别的机场就继续加,回车=到此为止)" "$dflt" s
|
||||
[[ -z "$s" ]] && break
|
||||
fi
|
||||
warn " 订阅链接不能空,没有节点 mihomo 启动不了"
|
||||
subs+=("$s"); n=$((n + 1))
|
||||
(( n > 8 )) && { info " 最多 8 个订阅,够用了"; break; }
|
||||
done
|
||||
(( ${#subs[@]} > 1 )) && ok " 共 ${#subs[@]} 个订阅,节点会合并到同一组里"
|
||||
|
||||
ask_default " 本地代理端口(HTTP 和 SOCKS5 共用)" "${old_port:-7890}" port
|
||||
[[ "$port" =~ ^[0-9]+$ ]] || die "端口要是数字,收到的是: $port"
|
||||
@@ -571,18 +625,40 @@ cfg_wizard() { # 生成一份全新的 config.yaml
|
||||
ui="no"
|
||||
fi
|
||||
|
||||
# ── 按地区拆分组 ──
|
||||
log ""
|
||||
log " ${D}分流规则。内网地址在两种模式下都是直连。${N}"
|
||||
ask_default " 1=全部走代理 2=指定国家/地区直连,其余走代理" "1" rules_mode
|
||||
local direct_cc=""
|
||||
if [[ "$rules_mode" == "2" ]]; then
|
||||
ask_default " 哪个国家/地区直连(两位 ISO 代码,如 CN / US / JP / DE)" "CN" direct_cc
|
||||
direct_cc="$(printf '%s' "$direct_cc" | tr '[:lower:]' '[:upper:]')"
|
||||
if [[ ! "$direct_cc" =~ ^[A-Z]{2}$ ]]; then
|
||||
warn " «$direct_cc» 不像两位国家代码,这条直连规则就先不写了,需要的话之后手动加 GEOIP 规则"
|
||||
direct_cc=""
|
||||
log " ${D}可以按地区把节点拆成几个组,每组自动挑该地区里延迟最低的那个。${N}"
|
||||
log " ${D}支持 ${REGIONS_SUPPORTED},留空=不拆,所有节点放一组。${N}"
|
||||
local regions_in; ask_default " 拆哪些地区(空格分隔)" "" regions_in
|
||||
local -a rlist=(); local r
|
||||
for r in $regions_in; do
|
||||
r="$(printf '%s' "$r" | tr '[:lower:]' '[:upper:]')"
|
||||
if region_filter "$r" >/dev/null 2>&1; then
|
||||
rlist+=("$r")
|
||||
else
|
||||
warn " 不认识地区代码 «$r»,跳过它(支持: ${REGIONS_SUPPORTED})"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
(( ${#rlist[@]} )) && warn " 提醒: 某个地区一个节点都没匹配上的话,内核会拒绝启动。下面的校验会告诉你"
|
||||
|
||||
# ── 分流规则 ──
|
||||
log ""
|
||||
log " ${D}分流规则。三种模式下内网地址都是直连。${N}"
|
||||
log " ${C}1${N} 全部走代理"
|
||||
log " ${C}2${N} 指定地区直连、其余走代理 ${D}(常见的翻墙用法)${N}"
|
||||
log " ${C}3${N} 指定地区走代理、其余直连 ${D}(回国/分区代理)${N}"
|
||||
ask_default " 选哪个" "1" rules_mode
|
||||
local direct_cc="" proxy_cc=""
|
||||
case "$rules_mode" in
|
||||
2) ask_default " 哪个地区直连(两位 ISO 代码,如 CN / US / JP / DE)" "CN" direct_cc
|
||||
direct_cc="$(cc_normalize "$direct_cc")" ;;
|
||||
3) ask_default " 哪个地区走代理(两位 ISO 代码,回国填 CN)" "CN" proxy_cc
|
||||
proxy_cc="$(cc_normalize "$proxy_cc")"
|
||||
if [[ -n "$proxy_cc" && "$proxy_cc" != "CN" ]]; then
|
||||
warn " 只有 CN 有成体系的域名列表,$proxy_cc 只能按 IP 匹配"
|
||||
warn " 后果: 该地区挂在境外 CDN 上的站点会漏掉,走直连而不是代理"
|
||||
fi ;;
|
||||
esac
|
||||
|
||||
# 先写到临时文件,确认无误再落盘
|
||||
local tmp; tmp="$(mktemp)"
|
||||
@@ -624,34 +700,53 @@ geox-url:
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
# 订阅:每个机场一个 provider,行尾的标记是脚本回头找它的依据
|
||||
local i use_list=""
|
||||
printf '\nproxy-providers:\n'
|
||||
for ((i = 1; i <= ${#subs[@]}; i++)); do
|
||||
use_list+="${use_list:+, }sub${i}"
|
||||
printf ' sub%d:\n' "$i"
|
||||
printf ' type: http\n'
|
||||
printf ' url: "%s" # mihomo-up:sub:sub%d\n' "${subs[i-1]}" "$i"
|
||||
printf ' path: ./providers/sub%d.yaml\n' "$i"
|
||||
printf ' interval: 86400 # 每 24 小时自动拉取一次订阅\n'
|
||||
printf ' health-check:\n enable: true\n url: %s\n interval: 300\n' "$HEALTH_URL"
|
||||
done
|
||||
|
||||
proxy-providers:
|
||||
subs:
|
||||
type: http
|
||||
url: "${sub}" # mihomo-up:sub
|
||||
path: ./providers/subs.yaml
|
||||
interval: 86400 # 每 24 小时自动拉取一次订阅
|
||||
health-check:
|
||||
enable: true
|
||||
url: https://www.gstatic.com/generate_204
|
||||
interval: 300
|
||||
# 分组:PROXY 是总开关,AUTO 自动测速,地区组各管一片
|
||||
local proxies_list="AUTO"
|
||||
for r in ${rlist[@]+"${rlist[@]}"}; do proxies_list+=", $r"; done
|
||||
proxies_list+=", DIRECT"
|
||||
|
||||
proxy-groups:
|
||||
- name: PROXY # 手动选节点,默认跟着 AUTO 走
|
||||
type: select
|
||||
proxies: [AUTO, DIRECT]
|
||||
use: [subs]
|
||||
- name: AUTO # 自动挑延迟最低的节点
|
||||
type: url-test
|
||||
use: [subs]
|
||||
url: https://www.gstatic.com/generate_204
|
||||
interval: 300
|
||||
tolerance: 50
|
||||
printf '\nproxy-groups:\n'
|
||||
printf ' - name: PROXY # 总开关,手动选;默认跟着 AUTO 走\n'
|
||||
printf ' type: select\n proxies: [%s]\n use: [%s]\n' "$proxies_list" "$use_list"
|
||||
printf ' - name: AUTO # 全部节点里自动挑延迟最低的\n'
|
||||
printf ' type: url-test\n use: [%s]\n url: %s\n interval: 300\n tolerance: 50\n' \
|
||||
"$use_list" "$HEALTH_URL"
|
||||
for r in ${rlist[@]+"${rlist[@]}"}; do
|
||||
printf ' - name: %s # 只看名字里带 %s 关键词的节点\n' "$r" "$r"
|
||||
printf ' type: url-test\n use: [%s]\n filter: "%s"\n' "$use_list" "$(region_filter "$r")"
|
||||
printf ' url: %s\n interval: 300\n tolerance: 50\n' "$HEALTH_URL"
|
||||
done
|
||||
|
||||
rules:
|
||||
EOF
|
||||
# 回国模式要按域名和 IP 两个维度匹配,用 .mrs 规则集,只下这一两个列表
|
||||
local cc_l=""
|
||||
if [[ -n "$proxy_cc" ]]; then
|
||||
cc_l="$(printf '%s' "$proxy_cc" | tr '[:upper:]' '[:lower:]')"
|
||||
printf '\n# 规则集:内核按 interval 自动更新;proxy: DIRECT 指下载规则集本身走直连\n'
|
||||
printf 'rule-providers:\n'
|
||||
if [[ "$proxy_cc" == "CN" ]]; then
|
||||
printf ' %s-domain:\n type: http\n behavior: domain\n format: mrs\n' "$cc_l"
|
||||
printf ' url: "%s"\n path: ./rules/%s-domain.mrs\n interval: 86400\n proxy: DIRECT\n' \
|
||||
"$(mrs_url geosite "$cc_l")" "$cc_l"
|
||||
fi
|
||||
printf ' %s-ip:\n type: http\n behavior: ipcidr\n format: mrs\n' "$cc_l"
|
||||
printf ' url: "%s"\n path: ./rules/%s-ip.mrs\n interval: 86400\n proxy: DIRECT\n' \
|
||||
"$(mrs_url geoip "$cc_l")" "$cc_l"
|
||||
fi
|
||||
|
||||
printf '\nrules:\n'
|
||||
cat <<'EOF'
|
||||
# 本机和内网地址一律直连,不然会绕出去再回来
|
||||
- IP-CIDR,127.0.0.0/8,DIRECT,no-resolve
|
||||
@@ -660,11 +755,19 @@ EOF
|
||||
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
|
||||
EOF
|
||||
|
||||
if [[ -n "$direct_cc" ]]; then
|
||||
printf ' # 目标 IP 属于 %s 就直连;用到 GEOIP 的话内核首次启动会去下载 GeoIP 数据库\n' "$direct_cc"
|
||||
if [[ -n "$proxy_cc" ]]; then
|
||||
# 回国:命中的走代理,其余直连。域名规则排在前面,因为挂境外 CDN 的
|
||||
# 国内站点解析出来的 IP 不在本国段,只靠 GEOIP 会漏。
|
||||
[[ "$proxy_cc" == "CN" ]] && printf ' - RULE-SET,%s-domain,PROXY\n' "$cc_l"
|
||||
printf ' - RULE-SET,%s-ip,PROXY,no-resolve\n' "$cc_l"
|
||||
printf ' - MATCH,DIRECT\n'
|
||||
elif [[ -n "$direct_cc" ]]; then
|
||||
printf ' # 目标 IP 属于 %s 就直连。用到 GEOIP,内核首次启动会去下载 GeoIP 数据库\n' "$direct_cc"
|
||||
printf ' - GEOIP,%s,DIRECT\n' "$direct_cc"
|
||||
printf ' - MATCH,PROXY\n'
|
||||
else
|
||||
printf ' - MATCH,PROXY\n'
|
||||
fi
|
||||
printf ' - MATCH,PROXY\n'
|
||||
} > "$tmp"
|
||||
|
||||
local bak=""
|
||||
@@ -679,31 +782,69 @@ EOF
|
||||
cfg_apply "$bak" || warn "配置写进去了但校验没过,可以用「编辑配置」改,或重新运行一遍向导"
|
||||
}
|
||||
|
||||
cfg_sub_url() { # 读回本脚本写的订阅链接
|
||||
# 订阅行由本脚本写入,行尾带 "# mihomo-up:sub:<provider名>" 标记。
|
||||
# 认这个标记而不是猜 YAML 结构,所以手写配置里别处的 url: 不会被误伤。
|
||||
cfg_sub_list() { # 每行输出「序号 <TAB> provider名 <TAB> 订阅地址」
|
||||
local f; f="$(conf_file)"
|
||||
[[ -f "$f" ]] || return 0
|
||||
sed -n -E 's|^[[:space:]]*url:[[:space:]]*"?([^"#]*)"?[[:space:]]*#[[:space:]]*mihomo-up:sub.*|\1|p' "$f" \
|
||||
| sed -n '1p' | sed -E 's/[[:space:]]+$//'
|
||||
sed -n -E 's|^[[:space:]]*url:[[:space:]]*"?([^"#]*)"?[[:space:]]*#[[:space:]]*mihomo-up:sub:?([A-Za-z0-9_-]*).*|\2\t\1|p' "$f" \
|
||||
| sed -E 's/\t[[:space:]]*/\t/; s/[[:space:]]+$//' \
|
||||
| awk -F'\t' '{ printf "%d\t%s\t%s\n", NR, ($1 == "" ? "subs" : $1), $2 }'
|
||||
}
|
||||
|
||||
cfg_change_sub() {
|
||||
local f; f="$(conf_file)"
|
||||
[[ -f "$f" ]] || { warn "还没有配置文件,先用「生成配置」运行一遍向导"; return 1; }
|
||||
local cur; cur="$(cfg_sub_url)"
|
||||
if [[ -z "$cur" ]]; then
|
||||
warn "在 $f 里没找到本脚本写的订阅行(标记是行尾的 # mihomo-up:sub)"
|
||||
warn "这份配置多半是手写或外部导入的,修改订阅请用「编辑配置」,或重新运行向导生成新配置"
|
||||
cfg_set_sub() { # cfg_set_sub <序号> <新地址>
|
||||
local idx="$1" new="$2" f tmp; f="$(conf_file)"; tmp="$(mktemp)"
|
||||
# 整行重建(缩进 + 新地址 + 原样保留行尾标记),比在原行上做替换更不容易出错
|
||||
awk -v idx="$idx" -v url="$new" '
|
||||
($0 ~ /url:/ && $0 ~ /mihomo-up:sub/) {
|
||||
n++
|
||||
if (n == idx) {
|
||||
indent = $0; sub(/[^ ].*$/, "", indent)
|
||||
tag = $0; sub(/^.*#/, "#", tag)
|
||||
print indent "url: \"" url "\" " tag
|
||||
next
|
||||
}
|
||||
}
|
||||
{ print }
|
||||
' "$f" > "$tmp" && cat "$tmp" > "$f"
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
cfg_sub_menu() {
|
||||
cfg_require || return 1
|
||||
local f list; f="$(conf_file)"
|
||||
list="$(cfg_sub_list)"
|
||||
if [[ -z "$list" ]]; then
|
||||
warn "在 $f 里没找到本脚本写的订阅行(标记是行尾的 # mihomo-up:sub:...)"
|
||||
warn "这份配置多半是手写或外部导入的。改订阅请用「手动编辑配置」,或重新运行向导生成一份新的"
|
||||
return 1
|
||||
fi
|
||||
local new; ask_default " 新的订阅链接" "$cur" new
|
||||
|
||||
log ""
|
||||
log " ${D}当前订阅:${N}"
|
||||
local i nm u
|
||||
while IFS=$'\t' read -r i nm u; do
|
||||
[[ -n "$u" ]] && printf ' %s%s.%s %s %s(%s)%s\n' "$C" "$i" "$N" "$u" "$D" "$nm" "$N" >&2
|
||||
done <<<"$list"
|
||||
log " ${D}增加或删除订阅要重新生成配置,向导会预先填好现有这几条。${N}"
|
||||
|
||||
local total; total="$(grep -c . <<<"$list")"
|
||||
local pick="1"
|
||||
(( total > 1 )) && ask_default " 改哪一条(序号)" "1" pick
|
||||
if [[ ! "$pick" =~ ^[0-9]+$ ]] || (( pick < 1 || pick > total )); then
|
||||
warn "序号要在 1 到 $total 之间,收到的是「$pick」"; return 1
|
||||
fi
|
||||
|
||||
nm="$(sed -n "${pick}p" <<<"$list" | cut -f2)"
|
||||
local cur; cur="$(sed -n "${pick}p" <<<"$list" | cut -f3)"
|
||||
local new; ask_default " 第 $pick 条的新地址" "$cur" new
|
||||
[[ -n "$new" && "$new" != "$cur" ]] || { info "没有改动"; return 0; }
|
||||
|
||||
local bak; bak="$(cfg_backup)"
|
||||
local esc; esc="$(printf '%s' "$new" | sed -e 's/[\\&|]/\\&/g')"
|
||||
sed -i -E "s|^([[:space:]]*url:[[:space:]]*).*(#[[:space:]]*mihomo-up:sub.*)|\1\"${esc}\" \2|" "$f"
|
||||
ok "订阅链接已更新"
|
||||
# 订阅缓存留着会继续用旧节点,删掉让内核下次启动重新拉
|
||||
rm -f "$CONF_DIR/providers/subs.yaml" 2>/dev/null || true
|
||||
cfg_set_sub "$pick" "$new"
|
||||
ok "第 $pick 条订阅已更新"
|
||||
# 旧节点缓存留着的话内核会继续用它,删掉才会去拉取新地址
|
||||
rm -f "$CONF_DIR/providers/${nm}.yaml" 2>/dev/null || true
|
||||
cfg_apply "$bak"
|
||||
}
|
||||
|
||||
@@ -803,6 +944,234 @@ cfg_backup_menu() {
|
||||
cfg_apply ""
|
||||
}
|
||||
|
||||
# ───────────────────────── 入站端口 ─────────────────────────
|
||||
# 一个端口对应一个出站:从 7801 进来的全走香港,从 7802 进来的全走日本。
|
||||
# 每条入站写成一行 YAML 流式映射并带标记,加/删就是加一行/删一行,不动别处。
|
||||
LISTENER_TAG="# mihomo-up:listener"
|
||||
|
||||
cfg_listener_list() { # 每行输出「序号 <TAB> 整行内容」
|
||||
local f; f="$(conf_file)"
|
||||
[[ -f "$f" ]] || return 0
|
||||
grep -n "$LISTENER_TAG" "$f" 2>/dev/null \
|
||||
| sed -E 's/^([0-9]+):[[:space:]]*/\1\t/' \
|
||||
| awk -F'\t' '{ printf "%d\t%s\t%s\n", NR, $1, $2 }'
|
||||
}
|
||||
|
||||
cfg_listener_show() {
|
||||
local list; list="$(cfg_listener_list)"
|
||||
if [[ -z "$list" ]]; then
|
||||
log " ${D}还没有额外的入站端口。主代理端口 $(cfg_get 'mixed-port') 照常走 rules。${N}"
|
||||
return 0
|
||||
fi
|
||||
log " ${D}额外的入站端口:${N}"
|
||||
local i _ln body
|
||||
while IFS=$'\t' read -r i _ln body; do
|
||||
[[ -n "$body" ]] || continue
|
||||
local nm pt px
|
||||
nm="$(sed -E 's/.*name:[[:space:]]*([^,}]*).*/\1/' <<<"$body")"
|
||||
pt="$(sed -E 's/.*port:[[:space:]]*([^,}]*).*/\1/' <<<"$body")"
|
||||
px="$(grep -o 'proxy:[^,}]*' <<<"$body" | sed -E 's/proxy:[[:space:]]*//' || true)"
|
||||
printf ' %s%s.%s 端口 %-6s → %s %s(%s)%s\n' \
|
||||
"$C" "$i" "$N" "$pt" "${px:-按 rules 分流}" "$D" "$nm" "$N" >&2
|
||||
done <<<"$list"
|
||||
}
|
||||
|
||||
cfg_listener_add() {
|
||||
cfg_require || return 1
|
||||
local port name type out
|
||||
ask_default " 新入站的端口" "7801" port
|
||||
[[ "$port" =~ ^[0-9]+$ ]] || { warn "端口要是数字,收到的是「$port」"; return 1; }
|
||||
if grep -q "port: ${port}," "$(conf_file)" 2>/dev/null; then
|
||||
warn "端口 $port 已经被另一条入站占了"; return 1
|
||||
fi
|
||||
[[ "$port" == "$(cfg_get 'mixed-port')" ]] && { warn "端口 $port 就是主代理端口,换一个"; return 1; }
|
||||
|
||||
ask_default " 入站类型(mixed=HTTP+SOCKS5 / http / socks)" "mixed" type
|
||||
case "$type" in mixed|http|socks) ;; *) warn "类型只能是 mixed / http / socks"; return 1 ;; esac
|
||||
|
||||
log ""
|
||||
log " ${D}这个端口的流量走哪个出站:${N}"
|
||||
local -a groups=(); local g
|
||||
while IFS= read -r g; do [[ -n "$g" ]] && groups+=("$g"); done < <(cfg_group_names)
|
||||
local idx=1
|
||||
printf ' %s0.%s 按 rules 分流 %s(和主端口一样)%s\n' "$C" "$N" "$D" "$N" >&2
|
||||
for g in ${groups[@]+"${groups[@]}"}; do
|
||||
printf ' %s%s.%s %s\n' "$C" "$idx" "$N" "$g" >&2; idx=$((idx + 1))
|
||||
done
|
||||
printf ' %s%s.%s DIRECT %s(不走代理)%s\n' "$C" "$idx" "$N" "$D" "$N" >&2
|
||||
|
||||
local pick; ask_default " 选一个" "0" pick
|
||||
[[ "$pick" =~ ^[0-9]+$ ]] || { warn "要填序号"; return 1; }
|
||||
if (( pick == 0 )); then out=""
|
||||
elif (( pick == idx )); then out="DIRECT"
|
||||
elif (( pick >= 1 && pick < idx )); then out="${groups[pick-1]}"
|
||||
else warn "没有这个序号"; return 1; fi
|
||||
|
||||
name="in-${port}"
|
||||
local line=" - { name: ${name}, type: ${type}, listen: 0.0.0.0, port: ${port}"
|
||||
[[ -n "$out" ]] && line+=", proxy: ${out}"
|
||||
line+=" } ${LISTENER_TAG}"
|
||||
|
||||
local bak; bak="$(cfg_backup)"
|
||||
local f tmp; f="$(conf_file)"; tmp="$(mktemp)"
|
||||
if grep -q '^listeners:' "$f"; then
|
||||
awk -v l="$line" '{ print } /^listeners:[[:space:]]*$/ && !d { print l; d=1 }' "$f" > "$tmp"
|
||||
else
|
||||
{ cat "$f"; printf '\nlisteners:\n%s\n' "$line"; } > "$tmp"
|
||||
fi
|
||||
cat "$tmp" > "$f"; rm -f "$tmp"
|
||||
|
||||
if [[ -n "$out" ]]; then
|
||||
ok "入站 $port → $out"
|
||||
warn "注意: 指定了出站就完全跳过 rules,从这个端口进来的流量一律走 $out"
|
||||
else
|
||||
ok "入站 $port 已添加,按 rules 分流"
|
||||
fi
|
||||
cfg_apply "$bak"
|
||||
}
|
||||
|
||||
cfg_listener_del() {
|
||||
cfg_require || return 1
|
||||
local list; list="$(cfg_listener_list)"
|
||||
[[ -n "$list" ]] || { warn "没有可删除的额外入站端口"; return 1; }
|
||||
cfg_listener_show
|
||||
local total; total="$(grep -c . <<<"$list")"
|
||||
local pick; ask_default " 删除哪一条(序号)" "" pick
|
||||
if [[ ! "$pick" =~ ^[0-9]+$ ]] || (( pick < 1 || pick > total )); then
|
||||
warn "序号要在 1 到 $total 之间,收到的是「$pick」"; return 1
|
||||
fi
|
||||
|
||||
local bak; bak="$(cfg_backup)"
|
||||
local f tmp; f="$(conf_file)"; tmp="$(mktemp)"
|
||||
awk -v idx="$pick" -v tag="$LISTENER_TAG" '
|
||||
index($0, tag) { n++; if (n == idx) next }
|
||||
{ print }
|
||||
' "$f" > "$tmp"
|
||||
# 删光了就把空的 listeners: 标题也去掉,留着它是个没有值的键
|
||||
if ! grep -q "$LISTENER_TAG" "$tmp"; then
|
||||
awk '!/^listeners:[[:space:]]*$/' "$tmp" > "${tmp}.2" && mv "${tmp}.2" "$tmp"
|
||||
fi
|
||||
cat "$tmp" > "$f"; rm -f "$tmp"
|
||||
ok "第 $pick 条入站已删除"
|
||||
cfg_apply "$bak"
|
||||
}
|
||||
|
||||
cfg_listener_menu() {
|
||||
while true; do
|
||||
log ""
|
||||
log "${W} 入站端口${N} ${D}— 多开几个本地端口,各自绑不同出站${N}"
|
||||
cfg_listener_show
|
||||
cat >&2 <<EOF
|
||||
|
||||
${C}1.${N} ➕ 添加一个入站端口
|
||||
${C}2.${N} ➖ 删除一个入站端口
|
||||
${C}0.${N} ↩️ 返回
|
||||
EOF
|
||||
local c; ask "$(printf '\n %s>%s ' "$W" "$N")" c
|
||||
echo >&2
|
||||
case "$c" in
|
||||
1) cfg_listener_add || true; pause ;;
|
||||
2) cfg_listener_del || true; pause ;;
|
||||
0|q|"") return 0 ;;
|
||||
*) warn "没有「$c」这个选项,输入上面列出的数字,或 0 返回"; sleep 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ───────────────────────── GEO / 规则集 ─────────────────────────
|
||||
# 这些数据文件决定 GEOIP / GEOSITE / RULE-SET 规则能不能匹配上。
|
||||
# 内核缺文件时会自己去下载,但下载失败是静默的:规则一条都匹配不上,
|
||||
# 流量全落到最后的 MATCH,看起来就像"配置没生效"。所以这里给出可见的状态。
|
||||
ctrl_api() { # ctrl_api <方法> <路径> [数据];管理接口没开就返回 1
|
||||
local ctrl secret host port
|
||||
ctrl="$(cfg_get 'external-controller')"; secret="$(cfg_get 'secret')"
|
||||
[[ -n "$ctrl" ]] || return 1
|
||||
host="${ctrl%:*}"; port="${ctrl##*:}"
|
||||
[[ -z "$host" || "$host" == "0.0.0.0" || "$host" == "[::]" ]] && host="127.0.0.1"
|
||||
local -a args=(-fsS -m 30 -X "$1")
|
||||
[[ -n "$secret" ]] && args+=(-H "Authorization: Bearer $secret")
|
||||
[[ -n "${3:-}" ]] && args+=(-H "Content-Type: application/json" -d "$3")
|
||||
curl "${args[@]}" "http://${host}:${port}$2"
|
||||
}
|
||||
|
||||
geo_show() {
|
||||
log " ${D}$CONF_DIR 里的数据文件:${N}"
|
||||
local found=0 p
|
||||
for p in "$CONF_DIR"/geoip.dat "$CONF_DIR"/geosite.dat "$CONF_DIR"/geoip.metadb \
|
||||
"$CONF_DIR"/country.mmdb "$CONF_DIR"/ASN.mmdb "$CONF_DIR"/rules/*.mrs; do
|
||||
[[ -f "$p" ]] || continue
|
||||
found=1
|
||||
printf ' %-22s %8s %s%s%s\n' "$(basename "$p")" \
|
||||
"$(du -h "$p" 2>/dev/null | cut -f1)" "$D" "$(date -r "$p" '+%F %T')" "$N" >&2
|
||||
done
|
||||
if (( found == 0 )); then
|
||||
log " ${Y}一个都没有${N}"
|
||||
log " ${D}配置里没用到 GEOIP/GEOSITE/RULE-SET 的话,这是正常的。${N}"
|
||||
log " ${D}用到了却没有文件,说明内核还没下载成功,规则会全部匹配不上。${N}"
|
||||
fi
|
||||
local au; au="$(cfg_get 'geo-auto-update')"
|
||||
log " ${D}自动更新 geo-auto-update: ${au:-未设置(默认关)}${N}"
|
||||
}
|
||||
|
||||
geo_refresh() {
|
||||
cfg_require || return 1
|
||||
info "让内核重新下载 GEO 数据库…"
|
||||
if ctrl_api POST /configs/geo '{"path":"","payload":""}' >/dev/null 2>&1; then
|
||||
ok "已通过管理接口触发更新,不用重启服务"
|
||||
return 0
|
||||
fi
|
||||
if ctrl_api POST /upgrade/geo '{"path":"","payload":""}' >/dev/null 2>&1; then
|
||||
ok "已通过管理接口触发更新,不用重启服务"
|
||||
return 0
|
||||
fi
|
||||
warn "管理接口没响应(没开启,或者服务没在运行),改用删文件 + 重启的办法"
|
||||
confirm_default_yes " 删掉现有的 GEO 文件和 .mrs 规则集,然后重启服务重新下载?" || { info "已取消"; return 0; }
|
||||
rm -f "$CONF_DIR"/geoip.dat "$CONF_DIR"/geosite.dat "$CONF_DIR"/geoip.metadb \
|
||||
"$CONF_DIR"/country.mmdb "$CONF_DIR"/ASN.mmdb 2>/dev/null || true
|
||||
rm -f "$CONF_DIR"/rules/*.mrs 2>/dev/null || true
|
||||
ok "已删除,内核下次启动会重新下载"
|
||||
if [[ "$(svc_state)" == "running" ]]; then
|
||||
systemctl restart "$SERVICE" && ok "$SERVICE 已重启" || warn "重启失败,看日志: journalctl -u $SERVICE -n 50"
|
||||
fi
|
||||
}
|
||||
|
||||
geo_toggle_autoupdate() {
|
||||
cfg_require || return 1
|
||||
local cur; cur="$(cfg_get 'geo-auto-update')"
|
||||
local bak; bak="$(cfg_backup)"
|
||||
if [[ "$cur" == "true" ]]; then
|
||||
cfg_set 'geo-auto-update' 'false'
|
||||
ok "已关闭自动更新,GEO 数据库只在缺文件时下载一次"
|
||||
else
|
||||
cfg_set 'geo-auto-update' 'true'
|
||||
[[ -n "$(cfg_get 'geo-update-interval')" ]] || cfg_set 'geo-update-interval' '24'
|
||||
ok "已打开自动更新,每 $(cfg_get 'geo-update-interval') 小时刷新一次"
|
||||
fi
|
||||
cfg_apply "$bak"
|
||||
}
|
||||
|
||||
geo_menu() {
|
||||
while true; do
|
||||
log ""
|
||||
log "${W} GEO / 规则集${N} ${D}— GEOIP、GEOSITE、RULE-SET 规则靠这些数据文件匹配${N}"
|
||||
geo_show
|
||||
cat >&2 <<EOF
|
||||
|
||||
${C}1.${N} 🔄 立即更新
|
||||
${C}2.${N} ⏱️ 自动更新 开/关
|
||||
${C}0.${N} ↩️ 返回
|
||||
EOF
|
||||
local c; ask "$(printf '\n %s>%s ' "$W" "$N")" c
|
||||
echo >&2
|
||||
case "$c" in
|
||||
1) geo_refresh || true; pause ;;
|
||||
2) geo_toggle_autoupdate || true; pause ;;
|
||||
0|q|"") return 0 ;;
|
||||
*) warn "没有「$c」这个选项,输入上面列出的数字,或 0 返回"; sleep 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ───────────────────────── 一键安装 ─────────────────────────
|
||||
do_install() {
|
||||
(( DRY_RUN )) || require_root "安装 mihomo"
|
||||
@@ -1029,7 +1398,7 @@ ${W} 选个操作吧:${N}
|
||||
${C}4.${N} 📌 安装指定版本内核
|
||||
${C}5.${N} ⏪ 回滚到上一版内核
|
||||
${C}6.${N} 📦 查看内核备份
|
||||
${C}7.${N} ⚙️ 配置管理 ${D}(订阅 / 端口 / 面板 / 校验)${N}
|
||||
${C}7.${N} ⚙️ 配置管理 ${D}(订阅 / 端口 / 入站 / GEO / 面板)${N}
|
||||
${C}8.${N} 🎛️ 服务控制 ${D}(启动 / 停止 / 日志 / 开机自启)${N}
|
||||
${C}9.${N} 🗑️ 卸载
|
||||
${C}0.${N} 👋 退出
|
||||
@@ -1066,39 +1435,45 @@ EOF
|
||||
|
||||
conf_menu() {
|
||||
while true; do
|
||||
local sub port lan ctrl
|
||||
sub="$(cfg_sub_url || true)"; port="$(cfg_get 'mixed-port' || true)"
|
||||
local port lan ctrl nsub nlis
|
||||
port="$(cfg_get 'mixed-port' || true)"
|
||||
lan="$(cfg_get 'allow-lan' || true)"; ctrl="$(cfg_get 'external-controller' || true)"
|
||||
nsub="$(cfg_sub_list 2>/dev/null | grep -c . || true)"
|
||||
nlis="$(cfg_listener_list 2>/dev/null | grep -c . || true)"
|
||||
|
||||
cat >&2 <<EOF
|
||||
|
||||
${W} 配置管理${N} ${D}$(conf_file)${N}
|
||||
${D}代理端口 ${N}${port:-—} ${D}局域网共享 ${N}${lan:-—} ${D}管理接口 ${N}${ctrl:-未开}
|
||||
${D}订阅 ${N}${sub:-—}
|
||||
${D}订阅 ${N}${nsub:-0} 个 ${D}额外入站 ${N}${nlis:-0} 个
|
||||
|
||||
${C}1.${N} 🧙 生成配置 ${D}(向导, 会覆盖现有配置)${N}
|
||||
${C}2.${N} 🔗 更换订阅链接
|
||||
${C}2.${N} 🔗 订阅管理 ${D}(查看 / 更换地址)${N}
|
||||
${C}3.${N} 🔌 修改代理端口
|
||||
${C}4.${N} 📡 局域网共享 开/关
|
||||
${C}5.${N} 🎚️ 管理接口 / 面板密钥
|
||||
${C}6.${N} 📥 从 URL 导入完整配置
|
||||
${C}7.${N} 🧪 校验配置
|
||||
${C}8.${N} 📝 手动编辑配置
|
||||
${C}9.${N} 💾 配置备份 / 还原
|
||||
${C}6.${N} 🚪 入站端口 ${D}(多开端口, 各绑各的出站)${N}
|
||||
${C}7.${N} 🌍 GEO / 规则集 ${D}(状态 / 更新)${N}
|
||||
${C}8.${N} 📥 从 URL 导入完整配置
|
||||
${C}9.${N} 🧪 校验配置
|
||||
${C}10.${N} 📝 手动编辑配置
|
||||
${C}11.${N} 💾 配置备份 / 还原
|
||||
${C}0.${N} ↩️ 返回
|
||||
EOF
|
||||
local c; ask "$(printf '\n %s>%s ' "$W" "$N")" c
|
||||
echo >&2
|
||||
case "$c" in
|
||||
1) cfg_wizard || true; pause ;;
|
||||
2) cfg_change_sub || true; pause ;;
|
||||
3) cfg_change_port || true; pause ;;
|
||||
4) cfg_toggle_lan || true; pause ;;
|
||||
5) cfg_change_ctrl || true; pause ;;
|
||||
6) cfg_import_url || true; pause ;;
|
||||
7) cfg_validate || true; pause ;;
|
||||
8) cfg_edit || true; pause ;;
|
||||
9) cfg_backup_menu || true; pause ;;
|
||||
1) cfg_wizard || true; pause ;;
|
||||
2) cfg_sub_menu || true; pause ;;
|
||||
3) cfg_change_port || true; pause ;;
|
||||
4) cfg_toggle_lan || true; pause ;;
|
||||
5) cfg_change_ctrl || true; pause ;;
|
||||
6) cfg_listener_menu ;;
|
||||
7) geo_menu ;;
|
||||
8) cfg_import_url || true; pause ;;
|
||||
9) cfg_validate || true; pause ;;
|
||||
10) cfg_edit || true; pause ;;
|
||||
11) cfg_backup_menu || true; pause ;;
|
||||
0|q|"") return 0 ;;
|
||||
*) warn "没有「$c」这个选项,输入上面列出的数字,或 0 退出/返回"; sleep 1 ;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user