【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
相关推荐
eastyuxiao3 小时前
思维导图拆解项目范围 3 个真实落地案例
大数据·运维·人工智能·流程图
GanGanGanGan_4 小时前
RustDesk 安装指南 — Rocky Linux 9 + XFCE X11
linux·运维·centos
南境十里·墨染春水8 小时前
linux学习笔记 网络编程——Socket入门与TCP客户端/服务器实现
linux·服务器·网络
Sirens.8 小时前
twikoo:从MongoDB Atlas到本地部署
运维·服务器
Meya11278 小时前
别再人工硬扛机房管理!智能 U 位系统,让机房管理一键数字化
大数据·运维
DFT计算杂谈9 小时前
自动化脚本一键绘制三元化合物相图
java·运维·服务器·开发语言·前端·python·自动化
Yupureki9 小时前
《Linux网络编程》6.UDP原理
linux·运维·服务器·网络·udp
楼田莉子9 小时前
Linux网络:NAT_代理
linux·运维·服务器·开发语言·c++·后端
烛衔溟10 小时前
TypeScript 索引签名、只读数组与 keyof / typeof 入门
linux·ubuntu·typescript
Harvy_没救了11 小时前
【网络运维】 WordPress 部署复盘
运维·网络