【Ubuntu】Mihomo 安装、systemd 托管、TUN 配置、API 测速与切换节点

这篇文章记录的是我在云服务器上使用 Mihomo 的一套实际流程。

我的做法很简单:从本地电脑上的 Clash 软件中取出已经在使用的 config 内容,复制到云服务器上,交给 Mihomo 运行;随后补充一段 tun 配置,再配合 systemd、控制 API 和 shell 脚本完成日常使用。


0. 我的使用场景

我在云服务器上主要做这些事情:

  • 访问外网资源
  • 拉取 GitHub 代码
  • 使用codex等 agent cli 工具
  • 安装 npm / pnpm 依赖
  • 给终端和开发工具提供代理环境
  • 在多个节点之间做切换和测速

围绕这些需求,我整理出了一套比较顺手的工作流。

下面按顺序执行即可。


1. 安装 Mihomo

bash 复制代码
sudo apt update
sudo apt install -y curl wget gzip python3 ca-certificates
bash 复制代码
ARCH="$(dpkg --print-architecture)"

case "$ARCH" in
  amd64) KEYWORD='mihomo-linux-amd64-compatible' ;;
  arm64) KEYWORD='mihomo-linux-arm64' ;;
  *)
    echo "不支持的架构: $ARCH"
    exit 1
    ;;
esac

URL="$(curl -fsSL https://api.github.com/repos/MetaCubeX/mihomo/releases/latest | python3 -c '
import json,sys
keyword=sys.argv[1]
data=json.load(sys.stdin)
for a in data["assets"]:
    u=a["browser_download_url"]
    if keyword in u and u.endswith(".gz"):
        print(u)
        break
' "$KEYWORD")"

echo "$URL"
wget -O /tmp/mihomo.gz "$URL"
gunzip -f /tmp/mihomo.gz
sudo install -m 755 /tmp/mihomo /usr/local/bin/mihomo
/usr/local/bin/mihomo -v

2. 放入 config 配置

bash 复制代码
sudo mkdir -p /etc/mihomo
sudo nano /etc/mihomo/config.yaml

把你本地 Clash 软件里的完整 config.yaml 内容粘进去。

然后确认配置里有下面这几段;没有就补进去:

yaml 复制代码
external-controller: 127.0.0.1:9090
secret: initial-secret

profile:
  store-selected: true

tun:
  enable: true
  stack: mixed
  auto-route: true
  auto-redirect: true
  auto-detect-interface: true

保存退出。


3. 配置 systemd

bash 复制代码
sudo tee /etc/systemd/system/mihomo.service >/dev/null <<'EOF'
[Unit]
Description=Mihomo
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/mihomo -d /etc/mihomo -f /etc/mihomo/config.yaml
Restart=always
RestartSec=3
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
NoNewPrivileges=false

[Install]
WantedBy=multi-user.target
EOF

4. 启动

bash 复制代码
sudo systemctl daemon-reload
sudo systemctl enable --now mihomo

5. 测试

bash 复制代码
sudo systemctl status mihomo --no-pager
bash 复制代码
curl -H 'Authorization: Bearer initial-secret' \
  http://127.0.0.1:9090/version
bash 复制代码
curl -H 'Authorization: Bearer initial-secret' \
  http://127.0.0.1:9090/group
bash 复制代码
journalctl -u mihomo -n 50 --no-pager

6. 写入切换节点脚本

bash 复制代码
sudo tee /usr/local/bin/mihomo-switch >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

CONTROLLER="${CONTROLLER:-http://127.0.0.1:9090}"
SECRET="${SECRET:-initial-secret}"
GROUP_NAME="${GROUP_NAME:-⚓ 节点选择}"

TEST_URL="${TEST_URL:-http://cp.cloudflare.com}"
TIMEOUT_MS="${TIMEOUT_MS:-5000}"
PARALLEL="${PARALLEL:-8}"

urlencode() {
  python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$1"
}

get_group_json() {
  local group_enc
  group_enc="$(urlencode "$GROUP_NAME")"
  curl -fsS \
    -H "Authorization: Bearer $SECRET" \
    "$CONTROLLER/proxies/$group_enc"
}

json_get_now() {
  python3 -c 'import sys, json; print(json.load(sys.stdin).get("now", ""))'
}

json_get_all() {
  python3 -c '
import sys, json
data = json.load(sys.stdin)
for item in data.get("all", []):
    print(item)
'
}

switch_node() {
  local target="$1"
  local group_enc
  group_enc="$(urlencode "$GROUP_NAME")"

  curl -fsS -X PUT \
    "$CONTROLLER/proxies/$group_enc" \
    -H "Authorization: Bearer $SECRET" \
    -H "Content-Type: application/json" \
    -d "$(printf '{"name":"%s"}' "$target")" > /dev/null
}

should_skip() {
  local name="$1"
  [[ "$name" == "DIRECT" ]] && return 0
  [[ "$name" == "REJECT" ]] && return 0
  [[ "$name" == *"官网"* ]] && return 0
  [[ "$name" == *"自动选择"* ]] && return 0
  [[ "$name" == *"直连"* ]] && return 0
  return 1
}

GROUP_JSON="$(get_group_json)"
CURRENT="$(printf '%s' "$GROUP_JSON" | json_get_now)"
mapfile -t RAW_OPTIONS < <(printf '%s' "$GROUP_JSON" | json_get_all)

WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT

NODES_FILE="$WORKDIR/nodes.txt"
RESULT_FILE="$WORKDIR/results.tsv"

for node in "${RAW_OPTIONS[@]}"; do
  if should_skip "$node"; then
    continue
  fi
  printf '%s\n' "$node" >> "$NODES_FILE"
done

export CONTROLLER SECRET TEST_URL TIMEOUT_MS RESULT_FILE

cat "$NODES_FILE" | xargs -I{} -P "$PARALLEL" bash -c '
node="$1"

urlencode() {
  python3 -c '"'"'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))'"'"' "$1"
}

node_enc="$(urlencode "$node")"
url_enc="$(urlencode "$TEST_URL")"

if resp="$(curl -fsS \
  -H "Authorization: Bearer $SECRET" \
  "$CONTROLLER/proxies/$node_enc/delay?url=$url_enc&timeout=$TIMEOUT_MS" 2>/dev/null)"; then
  delay="$(printf "%s" "$resp" | python3 -c '"'"'
import sys, json
try:
    data = json.load(sys.stdin)
    d = data.get("delay")
    print(d if isinstance(d, int) else "FAIL")
except Exception:
    print("FAIL")
'"'"')"
else
  delay="FAIL"
fi

if [[ "$delay" == "FAIL" ]]; then
  sort_key=999999
else
  sort_key="$delay"
fi

printf "%s\t%s\n" "$sort_key" "$node" >> "$RESULT_FILE"
' _ {}

mapfile -t SORTED_LINES < <(sort -n "$RESULT_FILE")

declare -a OPTIONS=()
declare -a DELAYS=()

for line in "${SORTED_LINES[@]}"; do
  delay="$(printf '%s' "$line" | cut -f1)"
  node="$(printf '%s' "$line" | cut -f2-)"

  if [[ "$delay" == "999999" ]]; then
    delay="FAIL"
  else
    delay="${delay}ms"
  fi

  OPTIONS+=("$node")
  DELAYS+=("$delay")
done

echo "当前节点: $CURRENT"
echo

for i in "${!OPTIONS[@]}"; do
  idx=$((i + 1))
  if [[ "${OPTIONS[$i]}" == "$CURRENT" ]]; then
    printf " %2d) %-12s %s   [当前]\n" "$idx" "${DELAYS[$i]}" "${OPTIONS[$i]}"
  else
    printf " %2d) %-12s %s\n" "$idx" "${DELAYS[$i]}" "${OPTIONS[$i]}"
  fi
done

echo
read -r -p "请输入编号(q 退出): " CHOICE

if [[ "$CHOICE" == "q" || "$CHOICE" == "Q" ]]; then
  exit 0
fi

if ! [[ "$CHOICE" =~ ^[0-9]+$ ]]; then
  echo "输入不是有效编号"
  exit 1
fi

if (( CHOICE < 1 || CHOICE > ${#OPTIONS[@]} )); then
  echo "编号超出范围"
  exit 1
fi

TARGET="${OPTIONS[$((CHOICE - 1))]}"

if [[ "$TARGET" == "$CURRENT" ]]; then
  echo "已是当前节点"
  exit 0
fi

switch_node "$TARGET"

AFTER_JSON="$(get_group_json)"
AFTER="$(printf '%s' "$AFTER_JSON" | json_get_now)"
echo "切换后节点: $AFTER"

if [[ "$AFTER" != "$TARGET" ]]; then
  exit 2
fi
EOF
bash 复制代码
sudo chmod +x /usr/local/bin/mihomo-switch

7. 更换节点

bash 复制代码
mihomo-switch

8. 常用命令

bash 复制代码
sudo systemctl restart mihomo
bash 复制代码
sudo systemctl status mihomo --no-pager
bash 复制代码
journalctl -u mihomo -f
bash 复制代码
curl -H 'Authorization: Bearer initial-secret' \
  http://127.0.0.1:9090/group
相关推荐
zandy10112 小时前
体验家 XMPlus 批量操作与自动化工作流引擎:让大规模运营从手动执行走向规则驱动
大数据·运维·自动化
Sagittarius_A*3 小时前
Docker:渗透前置容器化核心基础
运维·安全·docker·容器
代码AC不AC4 小时前
【Linux】线程池
linux·线程池
阿米亚波5 小时前
【EVE-NG 实战】防火墙基础(VLAN · VRRP · NAT · ACL · 静态路由)
运维·网络·笔记·网络协议·计算机网络·网络安全·运维开发
日取其半万世不竭5 小时前
云服务器迁移怎么少停机?DNS 切换前后的完整清单
运维·服务器
FREEDOM_X6 小时前
Linux 多线程编程——总结
java·linux·运维·ubuntu
名字还没想好☜6 小时前
Docker 多阶段构建:把镜像从 1.2GB 砍到 15MB
运维·docker·容器·多阶段构建·镜像优化
祸心依旧6 小时前
Visual Studio 五月更新 —— 计划、评审、优化
运维·人工智能·visual studio
Web4Browser6 小时前
Headless 浏览器自动化适合哪些任务?巡检、截图和提交动作要分级
运维·前端·自动化
難釋懷6 小时前
Nginx浏览器强制缓存
运维·nginx·缓存