diff --git a/README.md b/README.md index c60324b..c0db4d8 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ sudo ./mihomo-up.sh shortcut # 只安装 / 刷新 mihomo-up 快捷命令 1. 一键安装 内核 + systemd + 配置,缺哪步补哪步 2. 检查更新 3. 升级内核 4. 安装指定版本内核 5. 回滚内核 6. 查看内核备份 -7. 配置管理 更换订阅 / 修改端口 / 局域网共享 / 面板密钥 / 导入配置 / 校验 / 编辑 / 备份还原 +7. 配置管理 订阅 / 端口 / 局域网 / 面板密钥 / 入站端口 / GEO / 导入 / 校验 / 编辑 / 备份 8. 服务控制 启动 / 停止 / 重启 / 状态 / 最近日志 / 开机自启 / 重写服务文件 9. 卸载 ``` @@ -102,21 +102,92 @@ fork 之后想让快捷命令指向你自己那份,安装的时候加 `--scrip | 问题 | 说明 | |---|---| -| 订阅链接 | 机场给的 http(s) 地址,写成 `proxy-providers`,每 24 小时自动更新一次 | +| 订阅链接 | 机场给的 http(s) 地址。**可以填多个**,一次一个,回车结束(最多 8 个) | | 本地代理端口 | `mixed-port`,HTTP 和 SOCKS5 共用,默认 7890 | | 局域网共享 | `allow-lan`,开了同网段其它设备也能连(还要放行防火墙) | | 管理接口 | 只允许本机 / 局域网公网可访问 / 不开启。面板和 API 都连这里 | | 访问密钥 | 回车即自动生成随机串;管理接口开在 `0.0.0.0` 时务必设 | | Web 面板 | 选是就写 `external-ui-url`,内核首次启动自己去下载 metacubexd | -| 分流规则 | 全部走代理(默认),或者指定某个国家/地区直连、其余走代理 | +| 地区分组 | 留空=不拆。填 `HK JP US` 之类就按地区各建一个自动测速组 | +| 分流规则 | 三选一,见下面的表 | -生成的分组是 `PROXY`(手动选,默认跟着 AUTO)和 `AUTO`(自动挑延迟最低的)。规则里内网地址一律直连。 +多个订阅的节点会合并进同一组。每个订阅是一条独立的 `proxy-providers`,各自按 24 小时周期更新。 -选了「指定国家/地区直连」会额外写一条 `GEOIP,<两位国家代码>,DIRECT`,代码填 `CN`、`US`、`JP`、`DE` 都行,小写会自动转大写。 +**地区分组**支持 `HK TW JP SG US KR UK DE FR`。每个地区一个 `url-test` 组,靠节点名里的关键词匹配,中英文和旗帜 emoji 都认。 -**修改配置不用手写 YAML**:菜单「配置管理」能直接更换订阅、修改端口、开关局域网共享、改面板密钥。 +注意:某个地区一个节点都没匹配上的话内核会拒绝启动,紧接着的校验会当场告诉你。 -每次改动之前自动备份成 `config.yaml.bak-<时间戳>`,改完立刻用内核 `-t` 校验。**校验不过会问你要不要还原**,服务正在运行的话再问一句要不要重启生效。 +**分流规则**三选一,内网地址在哪种模式下都直连: + +| 模式 | 生成的规则 | 用途 | +|---|---|---| +| 1 全部走代理 | `MATCH,PROXY` | 默认 | +| 2 指定地区直连、其余走代理 | `GEOIP,,DIRECT` + `MATCH,PROXY` | 常见的翻墙用法 | +| 3 指定地区走代理、其余直连 | `RULE-SET,...` + `MATCH,DIRECT` | 回国 / 分区代理 | + +地区填两位 ISO 代码(`CN`、`US`、`JP`、`DE`…),小写自动转大写。 + +生成的分组是 `PROXY`(总开关,手动选,默认跟着 AUTO)、`AUTO`(全部节点里自动测速),加上你要的那几个地区组。 + +## 回国 / 分区代理 + +上面的模式 3 是反过来的:命中的地区走代理,其余直连。适合人在境外、只想让国内站点走国内节点。 + +它不用 `GEOIP`,而是生成两条 `.mrs` 规则集: + +```yaml +rules: + - RULE-SET,cn-domain,PROXY # 域名维度 + - RULE-SET,cn-ip,PROXY,no-resolve # IP 维度兜底 + - MATCH,DIRECT +``` + +**为什么必须有域名那份**:很多国内站挂着境外 CDN,域名解析出来的 IP 不在国内段,只靠 `GEOIP` 会整片漏掉。域名列表才是主力,IP 那条只兜住「直接连 IP、没有域名」的连接。 + +`.mrs` 是二进制规则集,只下载你要用的那一两个列表,不用拖整包 `geosite.dat`。下载走 `proxy: DIRECT`,因为你人在境外,直连最快。 + +有个限制脚本会当场告诉你:**只有 CN 有成体系的域名列表**。换成别的国家代码就只能按 IP 匹配,该地区挂境外 CDN 的站点会漏掉。 + +## 多开入站端口 + +一个端口对应一个出站——从 7801 进来的全走香港节点,从 7802 进来的全走日本。菜单「配置管理 → 入站端口」增删,写成 mihomo 的 `listeners`: + +```yaml +listeners: + - { name: in-7801, type: mixed, listen: 0.0.0.0, port: 7801, proxy: HK } + - { name: in-7802, type: mixed, listen: 0.0.0.0, port: 7802 } +``` + +用法就是把代理地址指到不同端口: + +```bash +curl -x http://127.0.0.1:7801 https://example.com # 走 HK 组 +curl -x http://127.0.0.1:7802 https://example.com # 按 rules 分流 +``` + +出站可以选任意一个已有策略组、`DIRECT`,或者「按 rules 分流」。 + +**选了具体出站就完全跳过 rules**:这个端口的流量一律走它,包括本该直连的内网以外流量。脚本在你选完之后会提醒一次。想保留分流就选「按 rules」。 + +类型支持 `mixed`(HTTP + SOCKS5 同口)、`http`、`socks`。端口和主代理端口或其它入站冲突会被当场拒绝。 + +## GEO 数据和规则集 + +`GEOIP`、`GEOSITE`、`RULE-SET` 规则都要靠数据文件才能匹配。内核缺文件时会自己去下载,但**下载失败是静默的**。 + +失败的表现是:规则一条都匹配不上,流量全落到最后的 `MATCH`,看起来就像"配置没生效"——你会先去怀疑节点,而不是怀疑一个几天前静默失败的下载。 + +菜单「配置管理 → GEO / 规则集」把这件事变可见:列出配置目录里的 `geoip.dat`、`geosite.dat`、`*.mmdb` 和 `rules/*.mrs`,带大小和上次更新时间。几天没变就是没更新成功。 + +「立即更新」优先走管理接口的 `POST /configs/geo`,**不用重启内核**。管理接口没开、或服务没在运行时,回退到删掉数据文件再重启服务重新下载(动手前会问你)。 + +「自动更新 开/关」写的是 `geo-auto-update` 和 `geo-update-interval: 24`。 + +## 改配置的安全网 + +上面这些改动——换订阅、改端口、加入站、开关自动更新——都不用手写 YAML,也都走同一套保护。 + +每次动手之前自动备份成 `config.yaml.bak-<时间戳>`,改完立刻用内核 `-t` 校验。**校验不过会问你要不要还原**,服务正在运行的话再问一句要不要重启生效。 需要手写的复杂配置(DNS、TUN、自定义规则)请用菜单里的「手动编辑配置」,或者用「从 URL 导入完整配置」整份换掉——这两条路同样有备份、校验、还原兜底。 diff --git a/mihomo-up.sh b/mihomo-up.sh index 3d14833..cd0bda9 100644 --- a/mihomo-up.sh +++ b/mihomo-up.sh @@ -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 < "$tmp" local bak="" @@ -679,31 +782,69 @@ EOF cfg_apply "$bak" || warn "配置写进去了但校验没过,可以用「编辑配置」改,或重新运行一遍向导" } -cfg_sub_url() { # 读回本脚本写的订阅链接 +# 订阅行由本脚本写入,行尾带 "# mihomo-up:sub:" 标记。 +# 认这个标记而不是猜 YAML 结构,所以手写配置里别处的 url: 不会被误伤。 +cfg_sub_list() { # 每行输出「序号 provider名 订阅地址」 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() { # 每行输出「序号 整行内容」 + 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 <%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 <%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 <%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