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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
|
#!/usr/bin/env bash
set -Eeuo pipefail
PROGRAM=mihomo-webui-setup
SUBSCRIPTION_COMMAND=/usr/bin/mihomo-subscription
CONTROL_FILE=/etc/mihomo-subscription/subscriptions.yaml
SUBCONVERTER_UNIT=subconverter.service
SUBCONVERTER_DIR=/opt/subconverter
log() {
printf '[%s] %s\n' "$PROGRAM" "$*" >&2
}
die() {
log "错误:$*"
exit 1
}
show_help() {
cat <<'EOF'
用法:
mihomo-webui-setup [setup] [mihomo-subscription init 参数]
mihomo-webui-setup subscription [mihomo-subscription subscription 参数]
mihomo-webui-setup update [mihomo-subscription update 参数]
mihomo-webui-setup reset [--yes] [--dry-run]
一次性初始化订阅、准备本地 subconverter、执行首次严格更新,
并启用 mihomo.service 与 mihomo-subscription-update.timer。
推荐不带参数运行,按隐藏式交互提示输入订阅 URL:
mihomo-webui-setup
也可传递 init 参数,例如:
mihomo-webui-setup --subscription-url URL --converter direct
mihomo-webui-setup --controller-listen 192.168.1.10:9090
mihomo-webui-setup --controller-listen 0.0.0.0:9090 --allow-proxy-lan
日常手工更新:
mihomo-webui-setup update
mihomo-webui-setup update --strict
安全修改 main 订阅(隐藏输入 URL,默认保留现有订阅格式):
mihomo-webui-setup subscription
mihomo-webui-setup subscription --converter subconverter
mihomo-webui-setup subscription --dry-run
软件包升级后按最新模板重置主配置:
mihomo-webui-setup reset # 显示保留与重置的项目并确认
mihomo-webui-setup reset --yes # 跳过确认,用于自动化
mihomo-webui-setup reset --dry-run # 只预览和校验,不修改文件
reset 会保留订阅 URL、subscriptions.yaml、API secret、external-controller、
allow-lan 及 bind-address/lan-allowed-ips/lan-disallowed-ips、认证与
skip-auth-prefixes、代理监听端口和 tun 配置;DNS、代理组、规则和 provider
定义按软件包最新模板重建,校验或替换失败时保留原配置。
注意:在命令行传 URL 可能被 shell 历史和进程列表记录。
controller 与 allow-proxy-lan 参数只在新建 config.yaml 时应用。
EOF
}
source_uses_local_subconverter() {
python - "$CONTROL_FILE" <<'PY'
import ipaddress
import sys
import urllib.parse
import yaml
with open(sys.argv[1], encoding="utf-8") as file:
control = yaml.safe_load(file)
if not isinstance(control, dict):
raise SystemExit(1)
for source in control.get("sources", []):
if not isinstance(source, dict) or not source.get("enabled", False):
continue
converter = source.get("converter") or {}
if converter.get("type", "direct") != "subconverter":
continue
endpoint = urllib.parse.urlparse(
str(converter.get("endpoint", "http://127.0.0.1:25500/sub"))
)
try:
is_loopback = endpoint.hostname == "localhost" or ipaddress.ip_address(
endpoint.hostname or ""
).is_loopback
except ValueError:
is_loopback = False
if is_loopback and (endpoint.port or 80) == 25500:
raise SystemExit(0)
raise SystemExit(1)
PY
}
has_enabled_source() {
[[ -f "$CONTROL_FILE" ]] || return 1
python - "$CONTROL_FILE" <<'PY'
import sys
import yaml
with open(sys.argv[1], encoding="utf-8") as file:
control = yaml.safe_load(file)
if isinstance(control, dict) and any(
isinstance(source, dict) and source.get("enabled", False)
for source in control.get("sources", [])
):
raise SystemExit(0)
raise SystemExit(1)
PY
}
subconverter_port_open() {
python - <<'PY'
import socket
try:
connection = socket.create_connection(("127.0.0.1", 25500), timeout=0.25)
except OSError:
raise SystemExit(1)
else:
connection.close()
PY
}
wait_for_subconverter() {
local attempt=0
for (( attempt = 0; attempt < 40; attempt++ )); do
if subconverter_port_open; then
return 0
fi
sleep 0.25
done
systemctl status --no-pager --full "$SUBCONVERTER_UNIT" >&2 || true
die "subconverter 未在 127.0.0.1:25500 就绪"
}
prepare_subconverter() {
if ! systemctl cat "$SUBCONVERTER_UNIT" >/dev/null 2>&1; then
die "当前订阅需要本地 subconverter;请先安装可提供 subconverter.service 的 subconverter-bin"
fi
if [[ ! -e "$SUBCONVERTER_DIR/pref.ini" ]]; then
[[ -f "$SUBCONVERTER_DIR/pref.example.ini" ]] || \
die "缺少 $SUBCONVERTER_DIR/pref.example.ini"
install -m 0644 \
"$SUBCONVERTER_DIR/pref.example.ini" \
"$SUBCONVERTER_DIR/pref.ini"
sed -i \
-e 's/^listen=0\.0\.0\.0$/listen=127.0.0.1/' \
-e 's/^log_level=info$/log_level=warn/' \
"$SUBCONVERTER_DIR/pref.ini"
grep -Fqx 'listen=127.0.0.1' "$SUBCONVERTER_DIR/pref.ini" || \
die "无法将 subconverter 限制到 127.0.0.1"
log "已从示例创建 subconverter 配置:$SUBCONVERTER_DIR/pref.ini"
else
log "保留已有 subconverter 配置:$SUBCONVERTER_DIR/pref.ini"
if grep -Fqx 'listen=0.0.0.0' "$SUBCONVERTER_DIR/pref.ini"; then
log "警告:已有 subconverter 配置监听 0.0.0.0,请确认防火墙或改为 127.0.0.1"
fi
fi
systemctl enable "$SUBCONVERTER_UNIT"
# 即使服务此前已启动,也要重启以确保新创建的 pref.ini 被加载。
systemctl restart "$SUBCONVERTER_UNIT"
wait_for_subconverter
log "本地 subconverter 已就绪"
}
# 只读探活,供 reset --dry-run 与确认前流程使用:不创建配置、不 enable/restart 服务。
check_subconverter_running() {
if ! systemctl cat "$SUBCONVERTER_UNIT" >/dev/null 2>&1; then
die "当前订阅需要本地 subconverter;请先安装可提供 subconverter.service 的 subconverter-bin"
fi
if systemctl is-active --quiet "$SUBCONVERTER_UNIT" && subconverter_port_open; then
return 0
fi
die "本地 subconverter 未在 127.0.0.1:25500 运行;当前流程不会修改系统,请先运行 mihomo-webui-setup setup 或 systemctl start $SUBCONVERTER_UNIT"
}
main() {
if [[ ${1-} == --help || ${1-} == -h ]]; then
show_help
return 0
fi
local command=setup
if [[ ${1-} == setup || ${1-} == subscription || ${1-} == update || ${1-} == reset ]]; then
command=$1
shift
elif [[ -n ${1-} && ${1-} != -* ]]; then
die "未知子命令:$1(支持 setup、subscription、update、reset)"
fi
if (( EUID != 0 )); then
exec sudo -- "$0" "$command" "$@"
fi
[[ -x "$SUBSCRIPTION_COMMAND" ]] || die "找不到 $SUBSCRIPTION_COMMAND"
if [[ $command == subscription ]]; then
log "安全修改 main 订阅"
"$SUBSCRIPTION_COMMAND" subscription "$@"
return 0
fi
if [[ $command == update ]]; then
log "更新订阅"
"$SUBSCRIPTION_COMMAND" update "$@"
return 0
fi
if [[ $command == reset ]]; then
log "按最新模板重置 Mihomo 主配置"
local dry_run=false
local assume_yes=false
local arg
for arg in "$@"; do
case $arg in
--dry-run) dry_run=true ;;
--yes) assume_yes=true ;;
esac
done
if ! $dry_run && ! $assume_yes; then
# 确认必须发生在任何副作用(prepare_subconverter、下载、写文件)之前,
# 保证取消时系统零改动。
if [[ ! -t 0 ]]; then
die "非交互环境必须使用 --yes 确认重置"
fi
local reply
read -r -p "[mihomo-webui-setup] 确认重置 Mihomo 主配置?[y/N] " reply
case $reply in
y | Y | yes | YES | Yes) ;;
*)
log "已取消,未修改任何文件"
return 0
;;
esac
set -- "$@" --yes
fi
if has_enabled_source && source_uses_local_subconverter; then
if $dry_run; then
check_subconverter_running
else
prepare_subconverter
fi
fi
"$SUBSCRIPTION_COMMAND" reset "$@"
return 0
fi
log "初始化 Mihomo 与订阅配置"
if (( $# == 0 )) && has_enabled_source; then
"$SUBSCRIPTION_COMMAND" init --non-interactive
log "检测到已有启用的订阅来源,保留现有来源配置"
else
log "首次运行时请按提示配置 main 订阅 URL 与格式"
"$SUBSCRIPTION_COMMAND" init --require-subscription "$@"
fi
if source_uses_local_subconverter; then
prepare_subconverter
fi
log "执行首次严格订阅更新与配置校验"
"$SUBSCRIPTION_COMMAND" update --strict --no-reload
systemctl enable --now mihomo.service
systemctl enable --now mihomo-subscription-update.timer
log "安装完成"
python - <<'PY'
from pathlib import Path
import yaml
config = yaml.safe_load(Path("/etc/mihomo/config.yaml").read_text(encoding="utf-8"))
controller = str(config.get("external-controller", "127.0.0.1:9090"))
print(f"[mihomo-webui-setup] WebUI/API 监听:{controller}")
if not controller.startswith(("0.0.0.0:", "[::]:")):
print(f"[mihomo-webui-setup] WebUI:http://{controller}/ui/")
else:
print("[mihomo-webui-setup] WebUI:请用服务器实际 IP 与上述端口访问 /ui/")
PY
log "API secret:/etc/mihomo/config.yaml"
}
main "$@"
|