feat: support rule disabling and hit/miss count/at tracking in restful api (#2502)

This commit is contained in:
potoo0 2026-01-11 19:37:08 +08:00 committed by GitHub
parent efb800866e
commit 19a6b5d6f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 150 additions and 2 deletions

View File

@ -34,6 +34,7 @@ import (
R "github.com/metacubex/mihomo/rules"
RC "github.com/metacubex/mihomo/rules/common"
RP "github.com/metacubex/mihomo/rules/provider"
RW "github.com/metacubex/mihomo/rules/wrapper"
T "github.com/metacubex/mihomo/tunnel"
orderedmap "github.com/wk8/go-ordered-map/v2"
@ -1083,6 +1084,10 @@ func parseRules(rulesConfig []string, proxies map[string]C.Proxy, ruleProviders
}
}
if format == "rules" { // only wrap top level rules
parsed = RW.NewRuleWrapper(parsed)
}
rules = append(rules, parsed)
}

View File

@ -1,5 +1,7 @@
package constant
import "time"
// Rule Type
const (
Domain RuleType = iota
@ -126,6 +128,27 @@ type Rule interface {
ProviderNames() []string
}
type RuleWrapper interface {
Rule
// SetDisabled to set enable/disable rule
SetDisabled(v bool)
// IsDisabled return rule is disabled or not
IsDisabled() bool
// HitCount for statistics
HitCount() uint64
// HitAt for statistics
HitAt() time.Time
// MissCount for statistics
MissCount() uint64
// MissAt for statistics
MissAt() time.Time
// Unwrap return Rule
Unwrap() Rule
}
type RuleMatchHelper struct {
ResolveIP func()
FindProcess func()

View File

@ -1,6 +1,8 @@
package route
import (
"time"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/tunnel"
@ -12,26 +14,46 @@ import (
func ruleRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getRules)
if !embedMode { // disallow update/patch rules in embed mode
r.Patch("/disable", disableRules)
}
return r
}
type Rule struct {
Index int `json:"index"`
Type string `json:"type"`
Payload string `json:"payload"`
Proxy string `json:"proxy"`
Size int `json:"size"`
// from RuleWrapper
Disabled bool `json:"disabled,omitempty"`
HitCount uint64 `json:"hitCount,omitempty"`
HitAt time.Time `json:"hitAt,omitempty"`
MissCount uint64 `json:"missCount,omitempty"`
MissAt time.Time `json:"missAt,omitempty"`
}
func getRules(w http.ResponseWriter, r *http.Request) {
rawRules := tunnel.Rules()
rules := []Rule{}
for _, rule := range rawRules {
rules := make([]Rule, 0, len(rawRules))
for index, rule := range rawRules {
r := Rule{
Index: index,
Type: rule.RuleType().String(),
Payload: rule.Payload(),
Proxy: rule.Adapter(),
Size: -1,
}
if ruleWrapper, ok := rule.(constant.RuleWrapper); ok {
r.Disabled = ruleWrapper.IsDisabled()
r.HitCount = ruleWrapper.HitCount()
r.HitAt = ruleWrapper.HitAt()
r.MissCount = ruleWrapper.MissCount()
r.MissAt = ruleWrapper.MissAt()
rule = ruleWrapper.Unwrap() // unwrap RuleWrapper
}
if rule.RuleType() == constant.GEOIP || rule.RuleType() == constant.GEOSITE {
r.Size = rule.(constant.RuleGroup).GetRecodeSize()
}
@ -43,3 +65,29 @@ func getRules(w http.ResponseWriter, r *http.Request) {
"rules": rules,
})
}
// disableRules disable or enable rules by their indexes.
func disableRules(w http.ResponseWriter, r *http.Request) {
// key: rule index, value: disabled
var payload map[int]bool
if err := render.DecodeJSON(r.Body, &payload); err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
if len(payload) != 0 {
rules := tunnel.Rules()
for index, disabled := range payload {
if index < 0 || index >= len(rules) {
continue
}
rule := rules[index]
if ruleWrapper, ok := rule.(constant.RuleWrapper); ok {
ruleWrapper.SetDisabled(disabled)
}
}
}
render.NoContent(w, r)
}

72
rules/wrapper/wrapper.go Normal file
View File

@ -0,0 +1,72 @@
package wrapper
import (
"time"
"github.com/metacubex/mihomo/common/atomic"
C "github.com/metacubex/mihomo/constant"
)
type RuleWrapper struct {
C.Rule
disabled atomic.Bool
hitCount atomic.Uint64
hitAt atomic.TypedValue[time.Time]
missCount atomic.Uint64
missAt atomic.TypedValue[time.Time]
}
func (r *RuleWrapper) IsDisabled() bool {
return r.disabled.Load()
}
func (r *RuleWrapper) SetDisabled(v bool) {
r.disabled.Store(v)
}
func (r *RuleWrapper) HitCount() uint64 {
return r.hitCount.Load()
}
func (r *RuleWrapper) HitAt() time.Time {
return r.hitAt.Load()
}
func (r *RuleWrapper) MissCount() uint64 {
return r.missCount.Load()
}
func (r *RuleWrapper) MissAt() time.Time {
return r.missAt.Load()
}
func (r *RuleWrapper) Unwrap() C.Rule {
return r.Rule
}
func (r *RuleWrapper) Hit() {
r.hitCount.Add(1)
r.hitAt.Store(time.Now())
}
func (r *RuleWrapper) Miss() {
r.missCount.Add(1)
r.missAt.Store(time.Now())
}
func (r *RuleWrapper) Match(metadata *C.Metadata, helper C.RuleMatchHelper) (bool, string) {
if r.IsDisabled() {
return false, ""
}
ok, adapter := r.Rule.Match(metadata, helper)
if ok {
r.Hit()
} else {
r.Miss()
}
return ok, adapter
}
func NewRuleWrapper(rule C.Rule) C.RuleWrapper {
return &RuleWrapper{Rule: rule}
}