这篇文章记录的是我在云服务器上使用 Mihomo 的一套实际流程。
我的做法很简单:从本地电脑上的 Clash 软件中取出已经在使用的config内容,复制到云服务器上,交给 Mihomo 运行;随后补充一段tun配置,再配合systemd、控制 API 和 shell 脚本完成日常使用。
0. 我的使用场景
我在云服务器上主要做这些事情:
- 访问外网资源
- 拉取 GitHub 代码
- 使用codex等 agent cli 工具
- 安装 npm / pnpm 依赖
- 给终端和开发工具提供代理环境
- 在多个节点之间做切换和测速
围绕这些需求,我整理出了一套比较顺手的工作流。
下面按顺序执行即可。
1. 安装 Mihomo
sudoaptupdatesudoaptinstall-ycurlwgetgzippython3 ca-certificatesARCH="$(dpkg --print-architecture)"case"$ARCH"inamd64)KEYWORD='mihomo-linux-amd64-compatible';;arm64)KEYWORD='mihomo-linux-arm64';;*)echo"不支持的架构:$ARCH"exit1;;esacURL="$(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.gzsudoinstall-m755/tmp/mihomo /usr/local/bin/mihomo /usr/local/bin/mihomo-v2. 放入 config 配置
sudomkdir-p/etc/mihomosudonano/etc/mihomo/config.yaml把你本地 Clash 软件里的完整config.yaml内容粘进去。
然后确认配置里有下面这几段;没有就补进去:
external-controller:127.0.0.1:9090secret:initial-secretprofile:store-selected:truetun:enable:truestack:mixedauto-route:trueauto-redirect:trueauto-detect-interface:true保存退出。
3. 配置 systemd
sudotee/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 EOF4. 启动
sudosystemctl daemon-reloadsudosystemctlenable--nowmihomo5. 测试
sudosystemctl status mihomo --no-pagercurl-H'Authorization: Bearer initial-secret'\http://127.0.0.1:9090/versioncurl-H'Authorization: Bearer initial-secret'\http://127.0.0.1:9090/groupjournalctl-umihomo-n50--no-pager6. 写入切换节点脚本
sudotee/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 EOFsudochmod+x /usr/local/bin/mihomo-switch7. 更换节点
mihomo-switch8. 常用命令
sudosystemctl restart mihomosudosystemctl status mihomo --no-pagerjournalctl-umihomo-fcurl-H'Authorization: Bearer initial-secret'\http://127.0.0.1:9090/group