【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
相关推荐
姜太小白2 小时前
【Linux】CentOS 7 XRDP 远程桌面配置
linux·运维·centos
信创DevOps先锋2 小时前
DevOps工具链选型新趋势:本土化适配与安全可控成企业核心考量
运维·安全·devops
嵌入式吴彦祖2 小时前
Luckfox Pico Ultra W 编译脚本分析(一)
linux
听风lighting2 小时前
RTT-SMART学习(一):环境搭建
linux·嵌入式·c·rtos·rtt-smart
顾喵2 小时前
SRIO通信总线
linux·windows·microsoft
Warren982 小时前
Windows 本地安装 Jenkins 教程
linux·运维·windows·功能测试·mysql·jenkins
习惯就好zz2 小时前
RK3588 Android 12 修改 NTP 服务器:从资源覆盖到时间同步验证
android·运维·服务器·aosp·ntp
汤愈韬2 小时前
ip-prefix(IP前缀列表)
linux·服务器·网络协议·tcp/ip
SPC的存折8 小时前
1、Redis数据库基础
linux·运维·服务器·数据库·redis·缓存