mihomo/mh

1296 lines
38 KiB
Bash
Executable file
Raw 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
shopt -s nullglob
MIHOMO_ROOT=/home/lht/bfile/mihomo
MIHOMO_BIN=${MIHOMO_ROOT}/mihomo # 指向二进制文件
PID_FILE=${MIHOMO_ROOT}/run.pid # 固定pid文件
API_HOST="127.0.0.1"
API_PORT="9090"
MIXED_PORT="7897" # 默认main
SOCK_PORT="7898"
API_BASE="http://${API_HOST}:${API_PORT}"
API_SECRET="" # 这是密钥用于验证对应yaml中设置的secret字段保持一致即可如果不设置默认空字符串表示不验证
# 简单彩色(不喜欢颜色就把这些变量都设为空字符串)
urlenc() {
python3 - <<'PY' "$1"
import sys, urllib.parse
print(urllib.parse.quote(sys.argv[1], safe=''))
PY
}
#===============测试小工具,下======================================
log() {
local level=$1
shift
echo "[$(date +%H:%M:%S)] [$level] $*"
}
debug() { [[ "${DEBUG:-0}" == "1" ]] && log "DEBUG" "$@"; }
info() { log "INFO" "$@"; }
warn() { log "WARN" "$@"; }
error() { log "ERROR" "$@"; }
die() { error "$@"; exit 1; }
ok() { log "OK" "$@"; }
need() {
command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"
}
test_env() { # 测试一下环境
need ss
need yq
need jq
need curl
need find
[[ -x "$MIHOMO_BIN" ]] || die "mihomo not executable: $MIHOMO_BIN"
echo "环境正确 ; env is ok"
}
test_port() {
local port
for port in "$@"; do
if ss -lnt 2>/dev/null | awk '{print $4}' | grep -qE "(:|\\.)${port}\$"; then
die "port already in use: $port"
fi
done
}
api_get() {
local path="$1"
if [[ -n "${API_SECRET}" ]]; then
curl -fsS --noproxy '*' -H "Authorization: Bearer ${API_SECRET}" "${API_BASE}${path}"
else
curl -fsS --noproxy '*' "${API_BASE}${path}"
fi
}
api_put() {
local path="$1"
local data="$2"
if [[ -n "${API_SECRET}" ]]; then
curl -fsS --noproxy '*' -X PUT \
-H "Authorization: Bearer ${API_SECRET}" \
-H 'Content-Type: application/json' \
-d "$data" "${API_BASE}${path}"
else
curl -fsS --noproxy '*' -X PUT \
-H 'Content-Type: application/json' \
-d "$data" "${API_BASE}${path}"
fi
}
sync_global_group() {
local pjson
pjson="$(api_get "/proxies" 2>/dev/null)" || return 1
jq -e '.proxies["GLOBAL"]?.all? | index("Proxy") != null' >/dev/null <<<"$pjson" || return 0
local now
now="$(jq -r '.proxies["GLOBAL"].now // empty' <<<"$pjson")"
if [[ "$now" != "Proxy" ]]; then
api_put "/proxies/GLOBAL" '{"name":"Proxy"}' >/dev/null
info "Synced global selector [GLOBAL] -> [Proxy]"
fi
}
api_delay() {
local name="$1"
local timeout="${2:-2000}"
local url="http://www.gstatic.com/generate_204"
local enc_name enc_url
enc_name="$(urlenc "$name")"
enc_url="$(urlenc "$url")"
local out
out="$(api_get "/proxies/${enc_name}/delay?timeout=${timeout}&url=${enc_url}" 2>/dev/null || true)"
jq -r 'if (.delay|type)=="number" then (.delay|tostring) else "-" end' <<<"$out" 2>/dev/null || echo "-"
}
switch_group_node() {
# 用法switch_group_node "<group>" "<node>"
local group="$1"
local node="$2"
[[ -n "$group" && -n "$node" ]] || return 1
local enc_group
enc_group="$(urlenc "$group")"
api_put "/proxies/${enc_group}" "$(jq -nc --arg name "$node" '{name:$name}')" >/dev/null
info "Switched group [$group] -> [$node]"
}
need_root() {
# 用法need_root "sudo -E $0 tun on"
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
warn "此操作需要 sudo/root 权限"
warn "请用 sudo 运行:"
warn " $1"
warn "或给 mihomo 二进制加能力(不必整进程 root"
warn " sudo setcap cap_net_admin,cap_net_raw+ep \"$MIHOMO_BIN\""
return 1
fi
return 0
}
has_mihomo_cap_net_admin() {
command -v getcap >/dev/null 2>&1 || return 1
getcap "$MIHOMO_BIN" 2>/dev/null | grep -q 'cap_net_admin'
}
disable_dns_fallback_geoip() {
local cfg="$1"
[[ -f "$cfg" ]] || return 1
yq -y -i '
.dns.enable = false |
.dns.fallback = [] |
.dns."fallback-filter" = null
' "$cfg"
}
#===============测试小工具,上======================================
#==============================配置文件读取,下======================================
get_main_group() {
local pjson
pjson="$(api_get "/proxies" 2>/dev/null)" || return 1
# 1) prefer group named "Proxy"
if jq -e '.proxies["Proxy"]? != null and .proxies["Proxy"].all? != null' >/dev/null <<<"$pjson"; then
echo "Proxy"
return 0
fi
# 2) fallback to your heuristic
jq -r '
.proxies
| to_entries
| map(select(.value.all? != null))
| (map(select(.value.type?=="Selector" or .value.type?=="URLTest" or .value.type?=="Fallback" or .value.type?=="LoadBalance")) + .)
| .[0].key // empty
' <<<"$pjson"
}
get_now_node() {
# 输出“当前节点名”;失败输出空并返回非 0
local group pjson
group="$(get_main_group)" || return 1
[[ -n "$group" ]] || return 1
pjson="$(api_get "/proxies" 2>/dev/null)" || return 1
jq -r --arg g "$group" '.proxies[$g].now // empty' <<<"$pjson"
}
#==============================配置文件读取,上======================================
#============================测速、测速结果处理,下=================================
speedtest_node_delay() {
# 用 mihomo 内置 delay API 测某个节点,输出 ms 或 "-"
# 用法speedtest_node_delay "<node_name>" [timeout_ms]
local node="${1:-}"
local timeout_ms="${2:-1000}"
[[ -n "$node" ]] || { echo "-"; return 1; }
# DIRECT/REJECT 没必要测
if [[ "$node" == "DIRECT" || "$node" == "REJECT" ]]; then
echo "-"
return 0
fi
api_delay "$node" "$timeout_ms"
}
speedtest_now() {
# 用法speedtest_now [timeout_ms]
local timeout_ms="${1:-1000}"
local node delay
node="$(get_now_node || true)"
if [[ -z "$node" ]]; then
echo "⚠️ 无法获取当前节点mihomo API 不通或没有可用 group"
return 1
fi
delay="$(speedtest_node_delay "$node" "$timeout_ms")"
# 简单打个标
local badge="🟡"
if [[ "$delay" =~ ^[0-9]+$ ]]; then
if (( delay <= 150 )); then badge="🟢"
elif (( delay <= 400 )); then badge="🟡"
else badge="🔴"
fi
else
badge="⚪"
fi
echo "👉 当前节点:${node}"
echo "${badge} 延迟:${delay} ms (timeout=${timeout_ms})"
}
wait_api() {
local t="${1:-5}" # seconds
local i=0
while (( i < t*10 )); do
if api_get "/proxies" >/dev/null 2>&1; then
return 0
fi
sleep 0.1
i=$((i+1))
done
return 1
}
#============================测速、测速结果处理,上=================================
#============================mihomo配置下==================
write_cfg() { # name url file
local name="$1"
local url="$2"
cat > "${MIHOMO_ROOT}/$name/config.yaml" <<YAML # 直接使用这个即可
mixed-port: ${MIXED_PORT}
socks-port: ${SOCK_PORT}
allow-lan: false
bind-address: ${API_HOST}
mode: rule
log-level: info
external-controller: ${API_HOST}:${API_PORT}
secret: ""
proxy-providers:
${name}:
type: http
url: "${url}"
interval: 3600
path: "${MIHOMO_ROOT}/$name/providers/$name.yaml"
proxy-groups:
- name: Proxy
type: select
use:
- ${name}
- name: Final
type: select
proxies:
- Proxy
- DIRECT
rules:
- DOMAIN-SUFFIX,localhost,DIRECT
- DOMAIN-SUFFIX,local,DIRECT
- 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,100.64.0.0/10,DIRECT,no-resolve
- IP-CIDR,224.0.0.0/4,DIRECT,no-resolve
- IP-CIDR6,::1/128,DIRECT,no-resolve
- IP-CIDR6,fc00::/7,DIRECT,no-resolve
- IP-CIDR6,fe80::/10,DIRECT,no-resolve
- MATCH,Final
YAML
}
#=====================mihomo配置上====================================
print_mihomo_status() {
local cfg pjson
cfg="$(api_get "/configs" 2>/dev/null)" || { echo "❌ API 不通: ${API_BASE}"; return 1; }
pjson="$(api_get "/proxies" 2>/dev/null)" || { echo "❌ 读取 /proxies 失败"; return 1; }
local allow_lan bind_addr mode mixed_port ec
allow_lan="$(jq -r '."allow-lan" // false' <<<"$cfg")"
bind_addr="$(jq -r '."bind-address" // "-"' <<<"$cfg")"
mode="$(jq -r '.mode // "-"' <<<"$cfg")"
mixed_port="$(jq -r '."mixed-port" // .port // "-"' <<<"$cfg")"
ec="$(jq -r '."external-controller" // "-"' <<<"$cfg")"
local group now node_count
group="$(get_main_group 2>/dev/null || true)"
[[ -n "$group" ]] || { echo "❌ 找不到主组(/proxies 里没有可用 group"; return 1; }
now="$(jq -r --arg g "$group" '.proxies[$g].now // "-"' <<<"$pjson")"
node_count="$(jq -r --arg g "$group" '.proxies[$g].all | length' <<<"$pjson" 2>/dev/null || echo "-")"
local lan_icon="🔒"
[[ "$allow_lan" == "true" ]] && lan_icon="📡"
echo "🧠 Mihomo Status"
echo "🌐 API: ${API_BASE} (EC=${ec})"
echo "🚪 Port: mixed-port=${mixed_port}"
echo "${lan_icon} LAN: allow-lan=${allow_lan} bind=${bind_addr}"
echo "🧭 Mode: ${mode}"
echo "🧩 Group: ${group}"
echo "📦 Nodes: ${node_count}"
echo "✅ Now: ${now}"
}
port_open_local() {
local port="$1"
ss -lnt 2>/dev/null | awk '{print $4}' | grep -qE "(:|\\.)${port}\$"
}
show_runtime_routing_status() {
local cfg pjson
cfg="$(api_get "/configs" 2>/dev/null)" || {
echo "Routing: API unavailable"
return 0
}
pjson="$(api_get "/proxies" 2>/dev/null)" || {
echo "Routing: /proxies unavailable"
return 0
}
local mode global_now final_now proxy_now
mode="$(jq -r '.mode // "-"' <<<"$cfg")"
global_now="$(jq -r '.proxies["GLOBAL"].now // empty' <<<"$pjson")"
final_now="$(jq -r '.proxies["Final"].now // empty' <<<"$pjson")"
proxy_now="$(jq -r '.proxies["Proxy"].now // empty' <<<"$pjson")"
echo "Mode: ${mode}"
[[ -n "$global_now" ]] && echo "GLOBAL -> ${global_now}"
[[ -n "$final_now" ]] && echo "Final -> ${final_now}"
[[ -n "$proxy_now" ]] && echo "Proxy -> ${proxy_now}"
}
extract_proxy_port() {
local value="${1:-}"
[[ -n "$value" ]] || return 1
sed -E 's#^[[:alpha:]][[:alnum:]+.-]*://[^:/]+:([0-9]+)/*$#\1#' <<<"$value"
}
proxy_env_status_line() {
local name="$1"
local value="$2"
if [[ -z "$value" ]]; then
echo "${name}: OFF"
return 0
fi
local port
port="$(extract_proxy_port "$value" 2>/dev/null || true)"
if [[ -n "$port" ]] && port_open_local "$port"; then
echo "${name}: ON (${value}, port ${port} reachable)"
elif [[ -n "$port" ]]; then
echo "${name}: STALE (${value}, port ${port} not listening)"
else
echo "${name}: ON (${value})"
fi
}
start_end(){ # start/end name/none 文件
local start0Rend="${1:-}"
local name_file=${2:-}
local cfg=$MIHOMO_ROOT/$name_file
[[ -n "$start0Rend" ]] || die "usage: $0 start|end"
if [[ "$start0Rend" == "start" ]]; then
start_end "end" # 先停止当前
[[ -d "$cfg" ]] || die "profile not found: $cfg"
[[ -f "$cfg/config.yaml" ]] || die "missing config: $cfg/config.yaml"
local provider_name
provider_name="$(yq -r '."proxy-providers" | keys | .[0] // empty' "$cfg/config.yaml" 2>/dev/null || true)"
if [[ -n "$provider_name" ]]; then
yq -y -i "
.\"proxy-providers\".\"${provider_name}\".path = \"${cfg}/providers/${provider_name}.yaml\"
" "$cfg/config.yaml"
fi
yq -y -i "
.[\"mixed-port\"] = ${MIXED_PORT} |
.[\"socks-port\"] = ${SOCK_PORT} |
.[\"external-controller\"] = \"${API_HOST}:${API_PORT}\"
" "$cfg/config.yaml"
if [[ "$(yq -r '.tun.enable // false' "$cfg/config.yaml" 2>/dev/null || echo false)" != "true" ]]; then
disable_dns_fallback_geoip "$cfg/config.yaml"
fi
# 进入的前提配置没有问题,可以直接运行
info "start mihomo with config=$cfg/config.yaml data=$cfg"
# 前台跑不方便,这里后台启动并记录 pid
test_port "$API_PORT" "$MIXED_PORT" "$SOCK_PORT"
"$MIHOMO_BIN" -d "$cfg" -f "$cfg/config.yaml" >"$cfg/mihomo.log" 2>&1 & # 每次启动情况记录
echo $! >"$PID_FILE" # 保存到定点位置
elif [[ "$start0Rend" == "end" ]]; then
if [[ -f "$PID_FILE" ]]; then # 存在就终止
local pid
pid="$(cat "$PID_FILE" || true)"
if [[ -n "${pid}" ]] && kill "$pid" 2>/dev/null; then
info "stop process pid=$pid"
kill "$pid" 2>/dev/null || true
# 等一下让它退出
for _ in {1..20}; do
kill -0 "$pid" 2>/dev/null || break
sleep 0.1
done
if kill -0 "$pid" 2>/dev/null; then
warn "force kill pid=$pid"
kill -9 "$pid" 2>/dev/null || true
fi
fi
rm -f "$PID_FILE"
fi
else
die "usage: $0 start|end"
fi
}
cfg_add() { # add name src
if [[ "$1" == "add" ]]; then
shift # 去掉 "add" 参数
fi
local name="${1:-}"
local src="${2:-}"
local cfg_root="${MIHOMO_ROOT}/${name}"
[[ -n "$name" ]] || die "usage: $0 add <name> <url|file>"
[[ -n "$src" ]] || die "usage: $0 add <name> <url|file>"
if [[ -d "$cfg_root" ]]; then
die "profile exists: $cfg_root ,请换一个名称"
fi
mkdir -p "$cfg_root"
mkdir -p "$cfg_root/providers" #
write_cfg "$name" "$src"
if [[ -f "$src" ]]; then #file: 直接复制,不用下载了
info "copy file -> ${cfg_root}/providers/$name.yaml"
cp -f "$src" "$cfg_root/providers/$name.yaml"
yq -y -i "
.[\"proxy-providers\"][\"$name\"].type = \"file\" |
del(.[\"proxy-providers\"][\"$name\"].url) |
del(.[\"proxy-providers\"][\"$name\"].interval)
" "$cfg_root/config.yaml"
info "saved: ${cfg_root}/providers/$name.yaml" # 有了文件,等会挂载
fi
# 启动这个配置
select_one "select" "$name" # 启动这个新的节点
print_mihomo_status 1000
}
get_group_nodes_json() {
local pjson group nodes
pjson="$(api_get "/proxies")" || return 1
group="$(get_main_group)" || return 1
[[ -n "$group" ]] || return 1
nodes="$(jq -c --arg g "$group" '.proxies[$g].all' <<<"$pjson")" || return 1
jq -nc --arg group "$group" --argjson nodes "$nodes" '{group:$group, nodes:$nodes}'
}
help() {
cat <<EOF
✅ 复制粘贴到你的 shell 启动文件(见下方“该改哪个文件”),或直接执行 mh env
# ====== MIHOMO PROXY ENV BEGIN ======
export http_proxy="http://127.0.0.1:${MIXED_PORT}"
export https_proxy="http://127.0.0.1:${MIXED_PORT}"
export all_proxy="socks5h://127.0.0.1:${MIXED_PORT}"
export HTTP_PROXY="\$http_proxy"
export HTTPS_PROXY="\$https_proxy"
export ALL_PROXY="\$all_proxy"
# 避免本地/内网/控制端口走代理(按需加减)
export NO_PROXY="127.0.0.1,localhost,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
export no_proxy="\$NO_PROXY"
# ====== MIHOMO PROXY ENV END ======
应用方法(改完文件后执行其一):
source ~/.bashrc
source ~/.zshrc
exec \$SHELL -l
-------------------------------------------------------------------------------
该改哪个文件?(尽量准确 + 兜底)
1) 先看你当前是什么 shell
echo "\$SHELL"
ps -p \$\$ -o comm=
2) 尝试自动判断“这次会读哪个 rc 文件”(不保证 100% 准确,但很实用):
# bash看看是不是 login shell
shopt -q login_shell && echo "login shell" || echo "non-login shell"
# zsh看看 ZDOTDIR / 以及默认读 ~/.zshrc
echo "ZDOTDIR=\${ZDOTDIR:-<unset>}"
3) 常见规则(按你当前情况选一个文件写入):
- bash + 交互式非登录 shell多数终端 tab~/.bashrc
- bash + 登录 shellssh 登录 / 有些终端配置):~/.bash_profile 或 ~/.profile
- zshmac 常见):~/.zshrc
- 如果你不确定:
a) bash同时写 ~/.bashrc 和 ~/.bash_profilebash_profile 里最好加一行:[[ -f ~/.bashrc ]] && source ~/.bashrc
b) zsh写 ~/.zshrc
验证是否生效(新开终端或 source 后):
echo \$http_proxy
env | grep -iE 'http_proxy|https_proxy|all_proxy|no_proxy'
-------------------------------------------------------------------------------
Usage:
mh [command] [args...]
Commands:
add <name> <url|file>
新建一个 profile 目录:${MIHOMO_ROOT}/<name>/
- 生成 config.yaml
- 把订阅作为 proxy-provider
- <url> 在线订阅type=http
- <file> 本地文件type=file会自动删 url/interval
select [name]
切换/启动某个 profile
- 不带 name列出目录输入序号选择
- 带 name直接启动 ${MIHOMO_ROOT}/<name>/
start <name>
直接启动指定 profile等价于select <name>
end
停止当前 mihomo通过 ${PID_FILE} 记录的 pid
env
输出代理环境变量 export 片段
用法示例:
eval "\$(mh env)"
mode [rule|global|direct]
切换当前 profile 的运行模式,并重启
- rule 使用内建基础规则
- global 全部走代理
- direct 全部直连
rules [show|reset]
查看或重置当前 profile 的基础规则
- show 显示当前规则
- reset 写回一套最简规则,并切回 rule 模式
doctor
输出当前 mh/mihomo 的诊断信息
- capability
- 端口监听
- TUN/模式/代理环境
- API / 路由 / DNS
- 最近日志
tun [on|off|toggle]
开关 TUN会写入当前 profile 的 config.yaml 并重启)
- 开启 TUN 通常需要 sudo/root或给 mihomo 加能力:
sudo setcap cap_net_admin,cap_net_raw+ep "<mihomo_bin>"
-l | --list | list
列出主组(优先 Proxy)的所有节点,并标记当前节点
<keyword>
模糊搜索节点名称并测速排序后让你选择切换
例如:
mh 美国
mh 香港
mh JP
mh SG
No-args (默认行为):
直接显示:
- TUN 状态
- 当前环境是否存在 HTTP_PROXY/HTTPS_PROXY
- 当前节点延迟测速(通过 mihomo API
-------------------------------------------------------------------------------
Examples:
# 添加订阅(在线)
mh add tnt "https://example.com/sub.yaml"
# 添加订阅(本地文件)
mh add local "/path/to/sub.yaml"
# 启动/切换 profile
mh select
mh select tnt
# 停止 mihomo
mh end
# 输出代理环境变量
mh env
# 切换模式
mh mode rule
mh mode global
mh mode direct
# 查看/重置基础规则
mh rules show
mh rules reset
# 一键诊断
mh doctor
# 查看节点列表
mh -l
# 按关键词筛选并测速排序后切换
mh 美国
# TUN 开关
sudo mh tun on
sudo mh tun off
mh tun toggle
Notes:
- API 不通时,很多功能会失败:确保 mihomo 正在运行且 external-controller 为 ${API_HOST}:${API_PORT}
- 如果刚启动就查询失败wait_api 会稍等 mihomo API 就绪(避免 race
- `mh` 是子进程,不能直接修改你当前 shell 的代理环境;需要 `eval "\$(mh env)"` 或写入 rc 文件
EOF
}
select_one(){ # select name
shift
local name
if [[ -z "${1:-}" ]];then
local -a files
mapfile -t files < <(find "$MIHOMO_ROOT" -mindepth 1 -maxdepth 1 -type d -exec test -f '{}/config.yaml' ';' -print | sort)
if (( ${#files[@]} == 0 )); then
echo "⚠️ ${MIHOMO_ROOT} 下没有找到任何 目录"
return 1
fi
echo "📄 可用配置(输入序号选择,例如 1"
local i
for i in "${!files[@]}"; do
printf " %d) %s\n" "$((i+1))" "$(basename "${files[$i]}")"
done
local pick
while true; do
read -r -p "👉 请输入序号 (1-${#files[@]}): " pick
[[ "$pick" =~ ^[0-9]+$ ]] || { echo "❌ 请输入数字"; continue; }
(( pick>=1 && pick<=${#files[@]} )) || { echo "❌ 超出范围"; continue; }
name="$(basename "${files[$((pick-1))]}")"
break
done
else
local file_cfg="$MIHOMO_ROOT/$1"
if [[ -d $file_cfg ]];then
name="$1"
else
echo "❌ 不存在: $file_cfg"# 输入name有无
return 1
fi
fi
echo "$name" > "${MIHOMO_ROOT}/current.profile"
start_end "start" "$name"
wait_api 5 || warn "mihomo API not ready yet"
sync_global_group || warn "failed to sync GLOBAL -> Proxy"
speedtest_now 1000
}
#====================tun控制下=========================
current_profile() {
[[ -f "${MIHOMO_ROOT}/current.profile" ]] || return 1
cat "${MIHOMO_ROOT}/current.profile"
}
current_profile_cfg() {
local prof
prof="$(current_profile)" || return 1
printf '%s/%s/config.yaml\n' "$MIHOMO_ROOT" "$prof"
}
reset_basic_rules() {
local cfg="$1"
[[ -f "$cfg" ]] || return 1
local provider_name
provider_name="$(yq -r '."proxy-providers" | keys | .[0] // empty' "$cfg" 2>/dev/null)"
[[ -n "$provider_name" ]] || { warn "cannot detect proxy-provider name from $cfg"; return 1; }
yq -y -i "
.mode = \"rule\" |
.[\"proxy-groups\"] = [
{\"name\":\"Proxy\",\"type\":\"select\",\"use\":[\"${provider_name}\"]},
{\"name\":\"Final\",\"type\":\"select\",\"proxies\":[\"Proxy\",\"DIRECT\"]}
] |
.rules = [
\"DOMAIN-SUFFIX,localhost,DIRECT\",
\"DOMAIN-SUFFIX,local,DIRECT\",
\"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,100.64.0.0/10,DIRECT,no-resolve\",
\"IP-CIDR,224.0.0.0/4,DIRECT,no-resolve\",
\"IP-CIDR6,::1/128,DIRECT,no-resolve\",
\"IP-CIDR6,fc00::/7,DIRECT,no-resolve\",
\"IP-CIDR6,fe80::/10,DIRECT,no-resolve\",
\"MATCH,Final\"
]
" "$cfg"
}
mode_set() {
local next_mode="${1:-}"
case "$next_mode" in
rule|global|direct) ;;
*) die "usage: $0 mode rule|global|direct" ;;
esac
local prof cfg
prof="$(current_profile)" || die "no current profile"
cfg="${MIHOMO_ROOT}/${prof}/config.yaml"
[[ -f "$cfg" ]] || die "missing config: $cfg"
if [[ "$next_mode" == "rule" ]] && ! yq -e '.rules | length > 0' "$cfg" >/dev/null 2>&1; then
info "No rules found in profile=${prof}, applying basic rules first..."
reset_basic_rules "$cfg" || die "failed to initialize basic rules"
fi
yq -y -i ".mode = \"${next_mode}\"" "$cfg"
info "Mode set to ${next_mode} (profile=${prof}), restarting..."
start_end end
start_end start "$prof"
wait_api 5 || warn "mihomo API not ready"
if [[ "$next_mode" == "global" ]]; then
sync_global_group || warn "failed to sync GLOBAL -> Proxy"
fi
print_mihomo_status || true
}
rules_cmd() {
local action="${1:-show}"
local cfg prof
prof="$(current_profile)" || die "no current profile"
cfg="${MIHOMO_ROOT}/${prof}/config.yaml"
[[ -f "$cfg" ]] || die "missing config: $cfg"
case "$action" in
show)
echo "Rules (profile=${prof}):"
yq -r '.rules[]?' "$cfg"
;;
reset)
reset_basic_rules "$cfg" || die "failed to reset basic rules"
info "Basic rules reset (profile=${prof}), restarting..."
start_end end
start_end start "$prof"
wait_api 5 || warn "mihomo API not ready"
print_mihomo_status || true
;;
*)
die "usage: $0 rules show|reset"
;;
esac
}
tun() {
# 用法sudo mh tun on|off|toggle
local action="${1:-toggle}"
local prof
prof="$(current_profile)" || { warn "no current profile"; return 1; }
local cfg="${MIHOMO_ROOT}/${prof}/config.yaml"
[[ -f "$cfg" ]] || { warn "missing config: $cfg"; return 1; }
local cur
cur="$(yq -r '.tun.enable // false' "$cfg" 2>/dev/null || echo false)"
local next
case "$action" in
on) next=true ;;
off) next=false ;;
toggle) [[ "$cur" == "true" ]] && next=false || next=true ;;
*) warn "usage: mh tun on|off|toggle"; return 1 ;;
esac
if [[ "$next" == "true" && "${EUID:-$(id -u)}" -ne 0 ]] && ! has_mihomo_cap_net_admin; then
warn "开启 TUN 通常需要 root或提前给 mihomo 设置 cap_net_admin/cap_net_raw"
warn "继续尝试启动若失败请执行sudo setcap cap_net_admin,cap_net_raw+ep \"$MIHOMO_BIN\""
fi
yq -y -i "
.tun.enable = ${next} |
.tun.stack = (.tun.stack // \"system\") |
.tun.device = (.tun.device // \"mihomo\") |
.tun[\"auto-route\"] = (.tun[\"auto-route\"] // true) |
.tun[\"auto-redir\"] = (.tun[\"auto-redir\"] // true) |
.tun[\"auto-detect-interface\"] = (.tun[\"auto-detect-interface\"] // true) |
.tun[\"dns-hijack\"] = (.tun[\"dns-hijack\"] // []) |
( .tun[\"dns-hijack\"] |= ( . + [\"any:53\"] | unique ) ) |
.dns.enable = ${next} |
.dns.listen = (.dns.listen // \"127.0.0.1:1053\") |
.dns[\"enhanced-mode\"] = \"redir-host\" |
.dns.nameserver = (.dns.nameserver // [\"223.5.5.5\",\"119.29.29.29\"]) |
.dns.fallback = (.dns.fallback // [\"1.1.1.1\",\"8.8.8.8\"]) |
.dns[\"fallback-filter\"].geoip = false
" "$cfg"
if [[ "$next" != "true" ]]; then
disable_dns_fallback_geoip "$cfg"
fi
info "TUN set to ${next} (profile=${prof}), restarting..."
start_end end
start_end start "$prof"
wait_api 5 || warn "mihomo API not ready"
sync_global_group || warn "failed to sync GLOBAL -> Proxy"
print_mihomo_status 1200 || true
}
#=============================tun控制上===================
list_nodes() {
# 用法mh -l
wait_api 5 || { warn "mihomo API not ready"; return 1; }
local pjson group now
pjson="$(api_get "/proxies")" || { warn "cannot read /proxies"; return 1; }
group="$(get_main_group)" || { warn "cannot detect main group"; return 1; }
now="$(jq -r --arg g "$group" '.proxies[$g].now // ""' <<<"$pjson")"
echo "📌 Group: ${group}"
[[ -n "$now" ]] && echo "⭐ Now: ${now}"
echo
# 列出 all 节点,排除 DIRECT/REJECT 的话可以加过滤;这里默认全列出并标记当前
jq -r --arg g "$group" --arg now "$now" '
.proxies[$g].all
| to_entries[]
| "\(.key + 1)) " + (if .value == $now then "⭐ " else " " end) + .value
' <<<"$pjson"
}
#===============================搜索测速排序,下======================
# 批量测延迟(默认并发 6输出"<delay>\t<node>"
batch_delay() {
# 用法batch_delay <timeout_ms> <max_jobs> <node1> <node2> ...
local timeout_ms="${1:-1200}"; shift || true
local max_jobs="${1:-6}"; shift || true
local -a nodes=( "$@" )
# 兼容:没有节点直接返回
(( ${#nodes[@]} == 0 )) && return 0
# 并发测
local tmp
tmp="$(mktemp)"
rm -f "$tmp"; : >"$tmp"
# 简单并发控制:后台跑,超过 max_jobs 就 wait 一个
local running=0
for n in "${nodes[@]}"; do
(
local d
d="$(api_delay "$n" "$timeout_ms")"
# 统一成数值:"-" 变成 999999便于排序
if [[ "$d" =~ ^[0-9]+$ ]]; then
printf "%s\t%s\n" "$d" "$n"
else
printf "999999\t%s\n" "$n"
fi
) >>"$tmp" &
running=$((running+1))
if (( running >= max_jobs )); then
wait -n 2>/dev/null || wait
running=$((running-1))
fi
done
wait
# 排序输出
sort -n "$tmp"
rm -f "$tmp"
}
# 关键词匹配 + 测速排序 + 选择切换
pick_switch_with_delay() {
# 用法pick_switch_with_delay "<keyword>" [timeout_ms]
local kw="${1:-}"
local timeout_ms="${2:-1200}"
[[ -n "$kw" ]] || { warn "usage: mh <keyword>"; return 1; }
wait_api 5 || { warn "mihomo API not ready"; return 1; }
local gj group
gj="$(get_group_nodes_json)" || { warn "cannot read proxies/groups"; return 1; }
group="$(jq -r '.group' <<<"$gj")"
# 匹配候选:中文用 contains英文用 lower-case contains
local candidates
candidates="$(jq -r --arg kw "$kw" '
def lc: ascii_downcase;
.nodes
| map(select(. != "DIRECT" and . != "REJECT"))
| map(select(
(contains($kw)) or ((lc) | contains($kw | lc))
))
| .[]
' <<<"$gj")"
if [[ -z "$candidates" ]]; then
warn "No nodes matched keyword: $kw"
info "Tip: try: 美国 / 香港 / 日本 / 狮城 / 韩国 / 台湾 / US / HK / JP / SG"
return 1
fi
mapfile -t arr <<<"$candidates"
info "Testing delay for ${#arr[@]} nodes (timeout=${timeout_ms}ms)..."
# 并发 6你可以改大/改小
local lines
lines="$(batch_delay "$timeout_ms" 6 "${arr[@]}")"
# 组装排序后的节点数组(按 delay asc
local -a sorted_nodes=()
local -a sorted_delay=()
while IFS=$'\t' read -r d n; do
sorted_nodes+=( "$n" )
sorted_delay+=( "$d" )
done <<<"$lines"
echo "🔎 Matched nodes in group [$group] for keyword: [$kw] (sorted by delay)"
local i showd
for i in "${!sorted_nodes[@]}"; do
showd="${sorted_delay[$i]}"
[[ "$showd" == "999999" ]] && showd="-" # 显示时还原
printf " %d) %s\t%s ms\n" "$((i+1))" "${sorted_nodes[$i]}" "$showd"
done
local pick
while true; do
read -r -p "👉 choose (1-${#sorted_nodes[@]}, 0 cancel): " pick
[[ "$pick" =~ ^[0-9]+$ ]] || { echo "❌ 请输入数字"; continue; }
(( pick==0 )) && { echo "cancelled"; return 0; }
(( pick>=1 && pick<=${#sorted_nodes[@]} )) || { echo "❌ 超出范围"; continue; }
break
done
switch_group_node "$group" "${sorted_nodes[$((pick-1))]}"
speedtest_now 1500 || true
}
#==============================搜索测速排序,上======================
#============================状态检查,下=====================
tun_status() {
local prof cfg enabled dev
prof="$(current_profile 2>/dev/null || true)"
[[ -n "$prof" ]] || { echo "TUN: unknown (no current profile)"; return 0; }
cfg="${MIHOMO_ROOT}/${prof}/config.yaml"
[[ -f "$cfg" ]] || { echo "TUN: unknown (missing config)"; return 0; }
enabled="$(yq -r '.tun.enable // false' "$cfg" 2>/dev/null || echo false)"
dev="$(yq -r '.tun.device // "mihomo"' "$cfg" 2>/dev/null || echo mihomo)"
if [[ "$enabled" != "true" ]]; then
echo "TUN: OFF (profile=${prof})"
return 0
fi
echo "TUN: ON (profile=${prof})"
echo "Expect iface: ${dev}"
if ip link show "$dev" >/dev/null 2>&1; then
echo "Iface: ✅ ${dev} exists"
else
echo "Iface: ❌ ${dev} not found (mihomo tun may not be running/privileged)"
fi
echo "Default route:"
ip route show default 2>/dev/null | sed 's/^/ /' || true
}
proxy_env_status() {
proxy_env_status_line "HTTP_PROXY" "${HTTP_PROXY:-}"
proxy_env_status_line "HTTPS_PROXY" "${HTTPS_PROXY:-}"
proxy_env_status_line "ALL_PROXY" "${ALL_PROXY:-}"
}
show_proxy_exports() {
cat <<EOF
export http_proxy="http://127.0.0.1:${MIXED_PORT}"
export https_proxy="http://127.0.0.1:${MIXED_PORT}"
export all_proxy="socks5h://127.0.0.1:${MIXED_PORT}"
export HTTP_PROXY="\$http_proxy"
export HTTPS_PROXY="\$https_proxy"
export ALL_PROXY="\$all_proxy"
export NO_PROXY="127.0.0.1,localhost,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
export no_proxy="\$NO_PROXY"
EOF
}
net_test() {
# 常用网站连通性测试:输出 “URL -> OK/FAIL”
local -a urls=(
"https://www.google.com"
"https://www.cloudflare.com"
"https://www.github.com"
"https://www.youtube.com"
"https://www.baidu.com"
"https://www.bing.com"
)
echo "== Net Test (timeout=10s) =="
local url
for url in "${urls[@]}"; do
if curl -fsS -I -m 10 "$url" >/dev/null 2>&1; then
printf "%s -> ✅ OK\n" "$url"
else
printf "%s -> ❌ FAIL\n" "$url"
fi
done
}
net_test_double() {
local -a urls=(
"https://www.google.com"
"https://www.cloudflare.com"
"https://www.github.com"
"https://www.youtube.com"
"https://www.baidu.com"
"https://www.bing.com"
)
echo "== Net Test (timeout=10s) =="
echo "Format: URL -> DIRECT | PROXY(mixed-port=${MIXED_PORT})"
local url ok1 ok2
for url in "${urls[@]}"; do
# 直连(完全忽略环境代理)
if curl -fsS -I -m 10 --noproxy '*' "$url" >/dev/null 2>&1; then
ok1="✅"
else
ok1="❌"
fi
# 强制走 mihomo mixed-port明确指定代理
if curl -fsS -I -m 10 -x "http://127.0.0.1:${MIXED_PORT}" "$url" >/dev/null 2>&1; then
ok2="✅"
else
ok2="❌"
fi
printf "%-28s -> DIRECT:%s PROXY:%s\n" "$url" "$ok1" "$ok2"
done
}
#============================状态检查,上=====================
#=========================debug,下===============================
debug_one() {
shift
local -a debug_code=()
debug_code[1]="ss -lntp | grep -E ':(9090|9091|7897|7898|7890|7891)'|| echo \"❌ 未找到代理服务\""
debug_code[2]="ps -fp \${1:-} -o pid,ppid,cmd"
debug_code[3]="net_test"
debug_code[4]="net_test_double"
debug_code[5]="test_env"
debug_code[6]="ip -o link show"
debug_code[7]="resolvectl status 2>/dev/null | sed -n '1,120p' || cat /etc/resolv.conf"
local -a debug_desc=()
debug_desc[1]="检查 9090/9091/7897/7898 端口监听:"
debug_desc[2]="查看指定 PID 的进程信息:"
debug_desc[3]="测试一下网络(仅proxy)"
debug_desc[4]="测试一下网络(直连+proxy)"
debug_desc[5]="测试需要的软件是否安装:"
debug_desc[6]="看看是否有虚拟网卡 mihomo:"
debug_desc[7]="dns debug"
echo "== debug list =="
local i
for i in "${!debug_code[@]}"; do
printf " [%s] %s\n %s\n" "$i" "${debug_desc[$i]:-}" "${debug_code[$i]}"
done
local idx
read -r -p "select index: " idx
for i in "${!debug_code[@]}"; do
if [[ "$i" == "$idx" ]]; then
echo "== run [$i] =="
eval "${debug_code[$i]}"
return $?
fi
done
echo "invalid index: $idx"
return 1
}
doctor() {
local prof cfg
prof="$(current_profile 2>/dev/null || true)"
cfg=""
[[ -n "$prof" ]] && cfg="${MIHOMO_ROOT}/${prof}/config.yaml"
echo "== mh doctor =="
echo "root: ${MIHOMO_ROOT}"
echo "bin: ${MIHOMO_BIN}"
echo "pid: ${PID_FILE}"
[[ -n "$prof" ]] && echo "profile: ${prof}" || echo "profile: <none>"
[[ -n "$cfg" ]] && echo "config: ${cfg}"
echo
echo "== capability =="
if command -v getcap >/dev/null 2>&1; then
getcap "$MIHOMO_BIN" 2>/dev/null || true
else
echo "getcap not found"
fi
echo
echo "== ports =="
ss -lnt 2>/dev/null | grep -E ":(9090|7897|7898|1053)\\b" || echo "no mh ports listening"
echo
echo "== runtime =="
tun_status
show_runtime_routing_status
proxy_env_status
echo
echo "== api =="
api_get "/configs" 2>/dev/null | jq -r '{mode, "mixed-port": ."mixed-port", "socks-port": ."socks-port", "external-controller": ."external-controller"}' 2>/dev/null || echo "api unavailable"
echo
echo "== route =="
ip route show default 2>/dev/null || true
echo
echo "== dns =="
resolvectl status 2>/dev/null | sed -n '1,120p' || cat /etc/resolv.conf
echo
echo "== log tail =="
if [[ -n "$prof" && -f "${MIHOMO_ROOT}/${prof}/mihomo.log" ]]; then
tail -n 40 "${MIHOMO_ROOT}/${prof}/mihomo.log"
else
echo "no log file"
fi
}
proxy_on() {
export http_proxy="http://127.0.0.1:${MIXED_PORT}"
export https_proxy="http://127.0.0.1:${MIXED_PORT}"
export all_proxy="socks5h://127.0.0.1:${MIXED_PORT}"
export HTTP_PROXY="$http_proxy"
export HTTPS_PROXY="$https_proxy"
export ALL_PROXY="$all_proxy"
# 设置no是为了避免mihomo自己访问自己
export NO_PROXY="127.0.0.1,localhost,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
export no_proxy="$NO_PROXY"
}
proxy_off() {
unset http_proxy https_proxy all_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY no_proxy
}
# proxy_env() {
# local action="${1:-}"
# local bashrc="${HOME}/.bashrc"
# local port="${2:-${MIXED_PORT:-7890}}"
# [[ -f "$bashrc" ]] || { echo "❌ not found: $bashrc"; return 1; }
# # 备份
# local bak="${bashrc}.bak.$(date +%Y%m%d%H%M%S)"
# cp -a "$bashrc" "$bak" || { echo "❌ backup failed"; return 1; }
# # 1) 先清理:标记块 + 常见代理 export 行
# # - 标记块:# >>> MH PROXY BEGIN ... # <<< MH PROXY END
# # - 以及零散的 export http_proxy/HTTP_PROXY/NO_PROXY...(避免残留)
# perl -0777 -pe '
# s/\n?# >>> MH PROXY BEGIN\n.*?\n# <<< MH PROXY END\n?/\n/sg;
# ' -i "$bashrc"
# # 删除零散的代理行(只删我们关心的这些变量)
# sed -i \
# -e '/^[[:space:]]*export[[:space:]]\+\(http_proxy\|https_proxy\|all_proxy\|HTTP_PROXY\|HTTPS_PROXY\|ALL_PROXY\|NO_PROXY\|no_proxy\)=/d' \
# -e '/^[[:space:]]*\(http_proxy\|https_proxy\|all_proxy\|HTTP_PROXY\|HTTPS_PROXY\|ALL_PROXY\|NO_PROXY\|no_proxy\)=/d' \
# "$bashrc"
# case "$action" in
# on)
# cat >>"$bashrc" <<EOF
# # >>> MH PROXY BEGIN
# export http_proxy="http://127.0.0.1:${port}"
# export https_proxy="http://127.0.0.1:${port}"
# export all_proxy="socks5h://127.0.0.1:${port}"
# export HTTP_PROXY="\$http_proxy"
# export HTTPS_PROXY="\$https_proxy"
# export ALL_PROXY="\$all_proxy"
# export NO_PROXY="127.0.0.1,localhost,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
# export no_proxy="\$NO_PROXY"
# # <<< MH PROXY END
# EOF
# echo "✅ proxy_env on: wrote proxy to $bashrc (port=${port})"
# echo " apply with: source ~/.bashrc"
# ;;
# off)
# echo "✅ proxy_env off: removed proxy lines from $bashrc"
# echo " apply with: source ~/.bashrc"
# ;;
# *)
# echo "usage:"
# echo " proxy_env on [port] # write proxy exports into ~/.bashrc (default port=\${MIXED_PORT:-7890})"
# echo " proxy_env off # remove proxy exports from ~/.bashrc"
# echo "backup:"
# echo " $bak"
# return 1
# ;;
# esac
# }
#=========================debug,上===============================
main() {
if [[ -z "${1:-}" ]]; then
tun_status
show_runtime_routing_status
proxy_env_status
speedtest_now 1000 || true
# print_mihomo_status
exit 0
fi
case "${1:-}" in
add)
cfg_add "$@";;
start)
[[ -n "${2:-}" ]] || die "usage: $0 start <name>"
select_one "select" "${2}";;
end)
start_end "$@";;
select)
select_one "$@";;
db)
debug_one "$@";;
doctor)
doctor ;;
env)
show_proxy_exports ;;
mode)
shift; mode_set "${1:-}" ;;
rules)
shift; rules_cmd "${1:-show}" ;;
help|-h|--help) help ;;
-l|--list|list) list_nodes ;;
tun) shift; tun "${1:-toggle}" ;;
*) pick_switch_with_delay "${1:-}" 1200 ;;
esac
}
main "$@"