最近我在一台 Ubuntu 服务器上折腾 Clash(准确说是 Mihomo/Clash Meta 这一类内核),本来只是想解决一个很简单的问题:让服务器稳定走代理,然后安装工具、切换节点、测试出口国家,必要时再开 TUN 做全局接管

结果一路踩坑:

  • Clash 明明启动了,但 ping google.com 死活不通;
  • curl --proxy 能访问 Google,普通终端命令却还是不走代理;
  • API 能查到节点列表,但切换节点时报 proxy not exist
  • 交互式切换每次手敲 JSON 太麻烦;
  • 开始装 Claude Code 时,脚本不是卡住,就是拿到 HTML 页面;
  • 最后才发现:很多问题根本不是“代理坏了”,而是“代理方式没搞明白”。

这篇文章把我这次在 Linux 上把 Clash 用顺的过程完整记录下来,重点包括:

  1. 如何在终端里真正让命令走代理
  2. 如何查询当前节点与切换节点
  3. 如何查询当前出口国家/地区
  4. 如何写一个交互式切换脚本
  5. 如何开启 TUN,以及什么时候该用、什么时候不该用

如果你也在 Linux 服务器、远程开发、AI 工具、GitHub、安装脚本这些场景里折腾代理,希望这篇能帮你少踩一些坑。


关键结论先行

在 Linux 终端里使用 Clash 时,“Clash 已经在监听端口”并不等同于“所有命令都会走代理”。通常需要同时满足:你配置了命令所使用的代理(环境变量或显式 --proxy),以及 Clash 的端口/API 可用。

本文会用同一套可复现流程,把“代理端口是否可用”“当前命令是否走代理”“当前出口是否符合预期”逐层验证清楚。

graph LR
  Cmd[命令/工具] -->|"读取环境变量或显式指定代理"| LocalProxy[本地代理端口<br/>7890/7891]
  LocalProxy --> Clash[Clash/Mihomo]
  Clash --> Target[目标站点/接口]

前置条件

  1. 你已经启动了 Clash/Mihomo,并且本地端口可监听:
    • 7890(HTTP 代理)
    • 7891(SOCKS5 代理)
    • 9090(external controller / API)
  2. 你的环境里可用这些工具(至少其中部分):
    • curl
    • jq(用于解析 JSON)
  3. 下文示例默认使用策略组名 🔰 选择节点(如果你的组名不同,需要替换对应字段)。

可复现步骤(建议顺序)

  1. 先验证端口与服务是否正常(例如确认 7890/7891/9090 在监听)。
  2. 在当前 shell 导出代理环境变量(或在单次命令中显式指定 --proxy)。
  3. 用带代理的 curl 验证“代理端口是否真的能用”。
  4. 通过 API 查询当前节点与节点列表(读取 /proxies 返回的 allnow)。
  5. 通过 API 切换节点,并再次查询 now 进行确认。
  6. 用带代理的请求查询出口 IP/国家等信息,避免仅凭节点名判断。
  7. 只有在需要更接近“系统全局接管”的场景,才考虑启用 TUN。

常见误解与纠正(本文后续展开)

常见的误解是:用 ping 去验证 HTTP/SOCKS 代理链路是否可用。ping 测的是 ICMP,并不会自动通过你配置的代理端口转发流量。

一、先搞明白:Clash 启动了,不等于所有命令都在走代理

我一开始遇到的一个常见误区是:Clash 已经在监听 7890/7891 端口了,那系统应该已经“翻过去了”吧?

结果现实是:

1
2
ping www.google.com
# packet loss

image-20260319085553956

但另一边:

1
2
curl -I --proxy http://127.0.0.1:7890 https://www.google.com
# HTTP/2 200

这说明了一个关键事实:

你现在开的,只是本地 HTTP/SOCKS 代理端口,不是系统全局代理。

也就是说:

  • 127.0.0.1:7890 是 HTTP 代理端口
  • 127.0.0.1:7891 是 SOCKS5 代理端口
  • 程序只有“显式使用这些代理端口”时,流量才会走 Clash
  • 普通终端命令不会凭空自动走 7890/7891

这也是为什么:

  • curl --proxy ... 能通
  • ping 还是不通
  • aptpipgit 是否走代理,要看它们有没有读到代理环境变量

二、终端里如何真正开启代理

1. 当前 shell 临时生效

常用的做法是在当前终端导出环境变量:

1
2
3
4
5
6
7
8
export http_proxy=http://127.0.0.1:7890
export https_proxy=http://127.0.0.1:7890
export HTTP_PROXY=http://127.0.0.1:7890
export HTTPS_PROXY=http://127.0.0.1:7890
export ALL_PROXY=socks5h://127.0.0.1:7891
export all_proxy=socks5h://127.0.0.1:7891
export NO_PROXY=localhost,127.0.0.1,::1
export no_proxy=localhost,127.0.0.1,::1

image-20260319090752616

设置完之后,很多命令就会直接走代理,例如:

1
2
3
curl https://www.google.com -I
curl https://api.ip.sb/ip
git ls-remote https://github.com/git/git.git | head

2. 永久写入 ~/.bashrc

如果你希望每次打开终端都自动带上代理,可以写入:

1
2
3
4
5
6
7
8
9
10
11
12
cat >> ~/.bashrc <<'EOF'
export http_proxy=http://127.0.0.1:7890
export https_proxy=http://127.0.0.1:7890
export HTTP_PROXY=http://127.0.0.1:7890
export HTTPS_PROXY=http://127.0.0.1:7890
export ALL_PROXY=socks5h://127.0.0.1:7891
export all_proxy=socks5h://127.0.0.1:7891
export NO_PROXY=localhost,127.0.0.1,::1
export no_proxy=localhost,127.0.0.1,::1
EOF

source ~/.bashrc

三、为什么开了代理,ping google.com 还是不通?

结论其实很简单:

ping 测的是 ICMP,不会自动通过你这个 HTTP/SOCKS 代理端口。

所以:

  • curlgitpip 这类支持代理的应用,能走 7890/7891
  • ping 这种系统级网络探测工具,不会因为你设置了 HTTP 代理就自动穿过去

因此,验证 Clash 是否真的工作,不要优先用 ping,而应该优先用:

1
2
curl -I --proxy http://127.0.0.1:7890 https://www.google.com
curl -I --socks5-hostname 127.0.0.1:7891 https://www.google.com

如果这两个能通,说明:

  • 节点是通的
  • Clash 内核是正常工作的
  • 只是你的系统流量还没有全局接管

验证方式对照

你想确认的事情 更合适的验证方式 说明
代理端口是否可用 curl --proxy http://127.0.0.1:7890 ... / curl --socks5-hostname 127.0.0.1:7891 ... 直接把请求交给 Clash 本地端口处理
当前命令是否“默认走代理” 看环境变量是否存在,或检查命令是否显式带了代理参数 端口存在不代表每个命令都会自动走代理
当前节点/策略组状态 调用 /proxies 并读取 all / now 这属于 API/控制层验证
当前出口国家/地区 使用带代理的请求查询出口 IP(例如 api.ip.sb/geoip 避免只凭节点名猜测

四、如何查询当前节点与节点列表

我的 Clash 开了 external controller,监听在:

1
127.0.0.1:9090

所以可以直接通过 API 查询策略组和节点。

1. 查看某个组当前的节点信息

例如我常用的策略组叫 🔰 选择节点

1
curl -s http://127.0.0.1:9090/proxies | jq '.proxies["🔰 选择节点"]'

输出类似这样:

image-20260319090840388

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"alive": true,
"all": [
"🇭🇰 香港Z01",
"🇯🇵 日本Z01",
"🇺🇲 美国Z01",
"🇺🇲 美国Z02",
"DIRECT"
],
"name": "🔰 选择节点",
"now": "🇺🇲 美国Z01",
"type": "Selector",
"udp": true
}

这里需要关注两个字段:

  • all:这个组里所有可选节点
  • now:当前正在使用的节点

2. 只看当前节点

1
curl -s http://127.0.0.1:9090/proxies | jq -r '.proxies["🔰 选择节点"].now'

五、为什么切换节点时报 proxy not exist

我当时第一次切节点时,写的是:

1
curl -s -X PUT 'http://127.0.0.1:9090/proxies/%F0%9F%94%B0%20%E9%80%89%E6%8B%A9%E8%8A%82%E7%82%B9'   -H 'Content-Type: application/json'   -d '{"name":"美国Z02"}'

结果返回:

1
{"message":"Selector update error: proxy not exist"}

后来才反应过来:

切换时的 name 必须和节点完整名称完全一致。

也就是说,如果节点列表里是:

1
🇺🇲 美国Z02

那你就必须写:

1
curl -s -X PUT 'http://127.0.0.1:9090/proxies/%F0%9F%94%B0%20%E9%80%89%E6%8B%A9%E8%8A%82%E7%82%B9'   -H 'Content-Type: application/json'   -d '{"name":"🇺🇲 美国Z02"}'

切换成功后,再看当前节点:

1
curl -s http://127.0.0.1:9090/proxies | jq -r '.proxies["🔰 选择节点"].now'

六、如何查询当前节点位于哪个国家

节点名字写“日本”“美国”不一定可靠,通常更可靠的方式是:

让请求显式走当前代理,然后查出口 IP 的地理信息。

我这里用的是 ip.sb

1
curl -s --proxy http://127.0.0.1:7890 https://api.ip.sb/geoip | jq

如果只想看关键信息:

1
curl -s --proxy http://127.0.0.1:7890 https://api.ip.sb/geoip | jq -r '"IP: \(.ip)\n国家: \(.country)\n地区: \(.region)\n城市: \(.city)\n组织: \(.organization)"'

这样你就能很直观地看到:

  • 当前出口 IP
  • 国家
  • 地区
  • 城市
  • ASN / 组织

这比“看节点名猜国家”靠谱得多。


七、一个可复用的节点管理脚本

每次手敲 curl + jq + PUT + JSON 太累,所以我后来写了一个脚本,支持:

  • 列出节点
  • 显示当前节点
  • 模糊关键词切换
  • 精确名称切换
  • 查询当前出口国家

保存为 clash-node.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#!/usr/bin/env bash
set -euo pipefail

API="${CLASH_API:-http://127.0.0.1:9090}"
GROUP="${CLASH_GROUP:-🔰 选择节点}"
PROXY_HTTP="${CLASH_PROXY_HTTP:-http://127.0.0.1:7890}"
SECRET="${CLASH_SECRET:-}"

auth_args=()
if [[ -n "$SECRET" ]]; then
auth_args=(-H "Authorization: Bearer $SECRET")
fi

urlencode() {
jq -rn --arg v "$1" '$v|@uri'
}

api_get() {
curl -fsS "${auth_args[@]}" "$1"
}

api_put_json() {
curl -fsS -X PUT "${auth_args[@]}" -H 'Content-Type: application/json' -d "$2" "$1"
}

group_json() {
api_get "$API/proxies"
}

list_nodes() {
group_json | jq -r --arg g "$GROUP" '.proxies[$g].all[]'
}

current_node() {
group_json | jq -r --arg g "$GROUP" '.proxies[$g].now'
}

switch_exact() {
local name="$1"
local group_enc
group_enc="$(urlencode "$GROUP")"
api_put_json "$API/proxies/$group_enc" "$(jq -nc --arg name "$name" '{name:$name}')"
}

switch_fuzzy() {
local keyword="$1"
local matches
matches="$(list_nodes | grep -F "$keyword" || true)"

local count
count="$(printf "%s\n" "$matches" | sed '/^$/d' | wc -l | tr -d ' ')"

if [[ "$count" == "0" ]]; then
echo "未找到包含关键词的节点: $keyword" >&2
exit 1
elif [[ "$count" != "1" ]]; then
echo "匹配到多个节点,请更精确一些:" >&2
printf "%s\n" "$matches" >&2
exit 1
fi

local target
target="$(printf "%s\n" "$matches" | head -n1)"
echo "切换到: $target" >&2
switch_exact "$target"
}

show_country() {
curl -fsS --proxy "$PROXY_HTTP" https://api.ip.sb/geoip | jq -r '"IP: \(.ip)\n国家: \(.country)\n地区: \(.region)\n城市: \(.city)\nASN/组织: \(.organization)"'
}

赋予执行权限:

1
chmod +x clash-node.sh

常用方式:

1
2
3
4
5
./clash-node.sh current
./clash-node.sh list
./clash-node.sh switch "美国Z02"
./clash-node.sh switch-exact "🇺🇲 美国Z02"
./clash-node.sh country

八、交互式切换脚本:更适合日常使用

上面那个已经够用,但如果节点多、国旗多、名字长,手输还是不够舒服。

所以我又写了一个交互式版本:直接把节点全部编号列出来,输入数字即可切换。

保存为 clash-select.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#!/usr/bin/env bash
set -euo pipefail

API="${CLASH_API:-http://127.0.0.1:9090}"
GROUP="${CLASH_GROUP:-🔰 选择节点}"
PROXY_HTTP="${CLASH_PROXY_HTTP:-http://127.0.0.1:7890}"
SECRET="${CLASH_SECRET:-}"

auth_args=()
if [[ -n "$SECRET" ]]; then
auth_args=(-H "Authorization: Bearer $SECRET")
fi

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

api_get() {
curl -fsS "${auth_args[@]}" "$1"
}

api_put_json() {
curl -fsS -X PUT "${auth_args[@]}" -H 'Content-Type: application/json' -d "$2" "$1"
}

get_group_json() {
api_get "$API/proxies"
}

get_current() {
get_group_json | jq -r --arg g "$GROUP" '.proxies[$g].now'
}

mapfile -t NODES < <(get_group_json | jq -r --arg g "$GROUP" '.proxies[$g].all[]')

echo "策略组: $GROUP"
echo "当前节点: $(get_current)"
echo

for i in "${!NODES[@]}"; do
idx=$((i + 1))
marker=" "
if [[ "${NODES[$i]}" == "$(get_current)" ]]; then
marker="*"
fi
printf "%2d. [%s] %s\n" "$idx" "$marker" "${NODES[$i]}"
done

echo
read -rp "请输入要切换的编号(直接回车退出): " choice
[[ -z "${choice:-}" ]] && exit 0

target="${NODES[$((choice - 1))]}"
group_enc="$(urlencode "$GROUP")"

echo "切换到: $target"
api_put_json "$API/proxies/$group_enc" "$(jq -nc --arg name "$target" '{name:$name}')" >/dev/null

sleep 1
echo "当前节点: $(get_current)"
echo
echo "测试当前出口信息..."
curl -fsS --proxy "$PROXY_HTTP" https://api.ip.sb/geoip | jq -r '"IP: \(.ip)\n国家: \(.country)\n地区: \(.region)\n城市: \(.city)\n组织: \(.organization)"'

赋权并运行:

1
2
chmod +x clash-select.sh
./clash-select.sh

image-20260319090952539


九、什么时候该开启 TUN

flowchart TD
  NeedGlobal["是否需要让普通命令自动走代理?"] -->|"否"| UseEnv["优先使用环境变量或显式 --proxy"]
  NeedGlobal -->|"是"| NeedTun["是否需要更接近“系统全局接管”?"]
  NeedTun -->|"否"| UseProxy["继续使用 HTTP/SOCKS + 环境变量/显式参数"]
  NeedTun -->|"是"| EnableTun["启用 TUN(需要评估路由与 DNS 影响)"]

如果你只是在终端里:

  • 安装工具
  • 拉 GitHub 仓库
  • 调 API
  • curl
  • pipnpmapt

那其实很多时候:

HTTP/SOCKS 代理 + 环境变量 就够了。

但如果你希望的是:

  • 普通命令也自动走代理
  • 某些不读环境变量的程序也能走
  • 更接近“系统全局代理”的效果

那就应该开启 TUN。

一个最小可用的 TUN 配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
tun:
enable: true
stack: system
auto-route: true
auto-redirect: true
auto-detect-interface: true
strict-route: true
dns-hijack:
- any:53
- tcp://any:53

dns:
enable: true
listen: 0.0.0.0:53
ipv6: true
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
respect-rules: true
nameserver:
- https://dns.alidns.com/dns-query
- https://doh.pub/dns-query
proxy-server-nameserver:
- https://dns.alidns.com/dns-query
- https://doh.pub/dns-query

改完后,一般需要用更高权限启动 Clash,例如:

1
sudo ./clash -d .

十、我对 TUN 的理解:它不是“必须”,而是“更彻底”

经过这次测试后,我对 TUN 的理解可以概括为:

  • 只想让少数命令走代理:环境变量最轻量
  • 想在终端里精确控制:HTTP/SOCKS + curl --proxy 最可控
  • 想让系统级流量也尽量接管:再上 TUN
  • 想长期维护舒服:建议把查询、切换、验证做成脚本

需要注意的不是“节点不通”,而是:

你以为自己在走代理,实际上根本没走。


日常工作流:我的命令清单

1. 开启当前 shell 代理

1
2
3
export http_proxy=http://127.0.0.1:7890
export https_proxy=http://127.0.0.1:7890
export ALL_PROXY=socks5h://127.0.0.1:7891

2. 验证代理是否通

1
2
curl -I https://www.google.com
curl https://api.ip.sb/ip

3. 查看当前节点

1
./clash-node.sh current

4. 切到某个节点

1
./clash-node.sh switch "日本Z02"

5. 交互式切换

1
./clash-select.sh

6. 查看当前出口国家

1
./clash-node.sh country

总结

这次在 Linux 上折腾 Clash,表面上看是“换节点、开代理、开 TUN”,本质上其实是把三件事想明白了:

  1. 代理端口 != 系统全局代理
  2. 节点可用 != 当前命令真的在走代理
  3. 脚本化比手敲命令重要得多

当你把这三件事理顺之后,很多原本不易定位的问题,通常可以拆解成更可执行的步骤:

  • 先验证代理本身
  • 再验证当前程序是否走代理
  • 再验证出口国家
  • 最后决定要不要上 TUN

如果你也在 Linux 服务器、远程开发、AI 工具、GitHub、安装脚本这类场景下使用 Clash,希望这篇文章能帮你把代理从“能用”升级到“好用”。