mihomo/mh
2026-02-11 16:19:50 +08:00

963 lines
28 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="7890" # 默认main
SOCK_PORT="7891"
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
}
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
}
#===============测试小工具,上======================================
#==============================配置文件读取,下======================================
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: global
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}
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}"
}
start_end(){ # start/end name/none 文件
local start0Rend=$1
local name_file=${2:-}
local cfg=$MIHOMO_ROOT/$name_file
if [[ "$start0Rend" == "start" ]]; then
start_end "end" # 先停止当前
if [[ -d $cfg ]];then
# 进入的前提配置没有问题,可以直接运行
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" # 保存到定点位置
fi
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 启动文件(见下方“该改哪个文件”):
# ====== 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
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 -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
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"
speedtest_now 1000
}
#====================tun控制下=========================
current_profile() {
[[ -f "${MIHOMO_ROOT}/current.profile" ]] || return 1
cat "${MIHOMO_ROOT}/current.profile"
}
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" ]]; then # 只有“要开启 tun”才提示/要求 sudo
need_root "sudo -E \"$0\" tun ${action}" || return 1
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\"])
" "$cfg"
info "TUN set to ${next} (profile=${prof}), restarting..."
start_end end
start_end start "$prof"
wait_api 5 || warn "mihomo API not ready"
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() {
if [[ -n "${HTTP_PROXY:-}" || -n "${HTTPS_PROXY:-}" ]]; then
echo "Proxy ENV: ON (HTTP_PROXY=${HTTP_PROXY:-<unset>} HTTPS_PROXY=${HTTPS_PROXY:-<unset>})"
else
echo "Proxy ENV: OFF"
fi
}
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|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/7890 端口监听:"
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
}
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() {
proxy_on
# unset http_proxy https_proxy all_proxy HTTP_PROXY # 停止一些用法终止一些env
if [[ -z "${1:-}" ]]; then
tun_status
proxy_env_status
speedtest_now 1000
# print_mihomo_status
exit 0
fi
case "${1:-}" in
add)
cfg_add "$@";;
start|end)
start_end "$@";;
select)
select_one "$@";;
db)
debug_one "$@";;
help|-h|--help) help ;;
-l|--list|list) list_nodes ;;
tun) shift; tun "${1:-toggle}" ;;
*) pick_switch_with_delay "${1:-}" 1200 ;;
esac
}
main "$@"