diff --git a/config/config.go b/config/config.go index 31f54728..362e4e27 100644 --- a/config/config.go +++ b/config/config.go @@ -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) } diff --git a/constant/rule.go b/constant/rule.go index c6d1d8d6..256ef0fc 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -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() diff --git a/hub/route/rules.go b/hub/route/rules.go index e2d4d82a..c842d66d 100644 --- a/hub/route/rules.go +++ b/hub/route/rules.go @@ -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) +} diff --git a/rules/wrapper/wrapper.go b/rules/wrapper/wrapper.go new file mode 100644 index 00000000..6fa38a9d --- /dev/null +++ b/rules/wrapper/wrapper.go @@ -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} +}