Skip to content

Commit 445c11f

Browse files
mapleafgoclaude
andcommitted
feat(core): 确保三个基础 Clash 模式始终可用
注入无害的 .invalid 域名路由规则,使 sing-box 的 CalculateClashModeList 始终发现 Rule/Global/Direct 模式, 同时 handler 层兜底补全缺失的模式并注册 CommandClashMode。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 66d7196 commit 445c11f

3 files changed

Lines changed: 61 additions & 3 deletions

File tree

core/handler.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,11 +226,21 @@ func (h *ClientHandler) WriteGroups(message libbox.OutboundGroupIterator) {
226226
}
227227

228228
// InitializeClashMode is called once with the available clash modes.
229+
// Ensure all three base modes are always present regardless of rule config.
229230
func (h *ClientHandler) InitializeClashMode(modeList libbox.StringIterator, currentMode string) {
230231
var modes []string
231232
for modeList.HasNext() {
232233
modes = append(modes, modeList.Next())
233234
}
235+
modeSet := make(map[string]bool, len(modes))
236+
for _, m := range modes {
237+
modeSet[m] = true
238+
}
239+
for _, m := range []string{"Rule", "Global", "Direct"} {
240+
if !modeSet[m] {
241+
modes = append(modes, m)
242+
}
243+
}
234244
h.emit(EventModeUpdate, map[string]any{
235245
"modes": modes,
236246
"current_mode": currentMode,

core/handler_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ func TestHandler_InitializeClashMode(t *testing.T) {
266266
}
267267
})
268268

269-
h.InitializeClashMode(&stringIterator{items: []string{"rule", "global", "direct"}}, "rule")
269+
h.InitializeClashMode(&stringIterator{items: []string{"Rule", "Global", "Direct"}}, "Rule")
270270

271271
var result map[string]any
272272
if err := json.Unmarshal([]byte(got), &result); err != nil {
@@ -276,8 +276,8 @@ func TestHandler_InitializeClashMode(t *testing.T) {
276276
if len(modes) != 3 {
277277
t.Errorf("modes count = %d, want 3", len(modes))
278278
}
279-
if result["current_mode"] != "rule" {
280-
t.Errorf("current_mode = %v, want rule", result["current_mode"])
279+
if result["current_mode"] != "Rule" {
280+
t.Errorf("current_mode = %v, want Rule", result["current_mode"])
281281
}
282282
}
283283

core/service.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,56 @@ func (s *Service) startWithContent(content, ruleSetProxy string) error {
211211
}
212212
slog.Debug("[startWithContent] config translated", "jsonBytes", len(jsonContent))
213213

214+
jsonContent = ensureClashModes(jsonContent)
214215
return s.startWithJSON(jsonContent, &libbox.OverrideOptions{}, false, nil, nil)
215216
}
216217

218+
// ensureClashModes injects harmless route rules that reference all three base
219+
// clash modes so that sing-box's CalculateClashModeList always discovers them.
220+
// The rules use .invalid domains that never match real traffic.
221+
func ensureClashModes(jsonContent string) string {
222+
var top map[string]json.RawMessage
223+
if err := json.Unmarshal([]byte(jsonContent), &top); err != nil {
224+
return jsonContent
225+
}
226+
routeRaw, ok := top["route"]
227+
if !ok {
228+
return jsonContent
229+
}
230+
var route map[string]json.RawMessage
231+
if err := json.Unmarshal(routeRaw, &route); err != nil {
232+
return jsonContent
233+
}
234+
235+
type modeRule struct {
236+
Domain []string `json:"domain"`
237+
ClashMode string `json:"clash_mode"`
238+
Outbound string `json:"outbound"`
239+
}
240+
indicators := []modeRule{
241+
{Domain: []string{"mode-rule.invalid"}, ClashMode: "Rule", Outbound: "direct"},
242+
{Domain: []string{"mode-direct.invalid"}, ClashMode: "Direct", Outbound: "direct"},
243+
{Domain: []string{"mode-global.invalid"}, ClashMode: "Global", Outbound: "direct"},
244+
}
245+
246+
var rules []json.RawMessage
247+
if raw, ok := route["rules"]; ok {
248+
_ = json.Unmarshal(raw, &rules)
249+
}
250+
for _, r := range indicators {
251+
b, _ := json.Marshal(r)
252+
rules = append(rules, b)
253+
}
254+
255+
rulesJSON, _ := json.Marshal(rules)
256+
route["rules"] = json.RawMessage(rulesJSON)
257+
routeJSON, _ := json.Marshal(route)
258+
top["route"] = json.RawMessage(routeJSON)
259+
260+
out, _ := json.Marshal(top)
261+
return string(out)
262+
}
263+
217264
func parseOverrideOptions(jsonStr string) (*libbox.OverrideOptions, OverrideConfig, error) {
218265
if jsonStr == "" {
219266
return &libbox.OverrideOptions{}, OverrideConfig{}, nil
@@ -290,6 +337,7 @@ func (s *Service) startWithJSON(jsonContent string, override *libbox.OverrideOpt
290337
opts.AddCommand(libbox.CommandStatus)
291338
opts.AddCommand(libbox.CommandGroup)
292339
opts.AddCommand(libbox.CommandConnections)
340+
opts.AddCommand(libbox.CommandClashMode)
293341

294342
newClient := libbox.NewCommandClient(s.handler, opts)
295343
slog.Debug("[startWithJSON] calling CommandClient.Connect")

0 commit comments

Comments
 (0)