这篇文章记录的是我在云服务器上使用 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