diff --git a/component/trie/domain_set_bin.go b/component/trie/domain_set_bin.go new file mode 100644 index 00000000..e32d4e1a --- /dev/null +++ b/component/trie/domain_set_bin.go @@ -0,0 +1,127 @@ +package trie + +import ( + "encoding/binary" + "errors" + "io" +) + +func (ss *DomainSet) WriteBin(w io.Writer, count int64) (err error) { + // version + _, err = w.Write([]byte{1}) + if err != nil { + return err + } + + // count + err = binary.Write(w, binary.BigEndian, count) + if err != nil { + return err + } + + // leaves + err = binary.Write(w, binary.BigEndian, int64(len(ss.leaves))) + if err != nil { + return err + } + for _, d := range ss.leaves { + err = binary.Write(w, binary.BigEndian, d) + if err != nil { + return err + } + } + + // labelBitmap + err = binary.Write(w, binary.BigEndian, int64(len(ss.labelBitmap))) + if err != nil { + return err + } + for _, d := range ss.labelBitmap { + err = binary.Write(w, binary.BigEndian, d) + if err != nil { + return err + } + } + + // labels + err = binary.Write(w, binary.BigEndian, int64(len(ss.labels))) + if err != nil { + return err + } + _, err = w.Write(ss.labels) + if err != nil { + return err + } + + return nil +} + +func ReadDomainSetBin(r io.Reader) (ds *DomainSet, count int64, err error) { + // version + version := make([]byte, 1) + _, err = io.ReadFull(r, version) + if err != nil { + return nil, 0, err + } + if version[0] != 1 { + return nil, 0, errors.New("version is invalid") + } + + // count + err = binary.Read(r, binary.BigEndian, &count) + if err != nil { + return nil, 0, err + } + + ds = &DomainSet{} + var length int64 + + // leaves + err = binary.Read(r, binary.BigEndian, &length) + if err != nil { + return nil, 0, err + } + if length < 1 { + return nil, 0, errors.New("length is invalid") + } + ds.leaves = make([]uint64, length) + for i := int64(0); i < length; i++ { + err = binary.Read(r, binary.BigEndian, &ds.leaves[i]) + if err != nil { + return nil, 0, err + } + } + + // labelBitmap + err = binary.Read(r, binary.BigEndian, &length) + if err != nil { + return nil, 0, err + } + if length < 1 { + return nil, 0, errors.New("length is invalid") + } + ds.labelBitmap = make([]uint64, length) + for i := int64(0); i < length; i++ { + err = binary.Read(r, binary.BigEndian, &ds.labelBitmap[i]) + if err != nil { + return nil, 0, err + } + } + + // labels + err = binary.Read(r, binary.BigEndian, &length) + if err != nil { + return nil, 0, err + } + if length < 1 { + return nil, 0, errors.New("length is invalid") + } + ds.labels = make([]byte, length) + _, err = io.ReadFull(r, ds.labels) + if err != nil { + return nil, 0, err + } + + ds.init() + return ds, count, nil +} diff --git a/constant/provider/interface.go b/constant/provider/interface.go index f7dfc9cc..c86e6163 100644 --- a/constant/provider/interface.go +++ b/constant/provider/interface.go @@ -1,6 +1,8 @@ package provider import ( + "fmt" + "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/constant" ) @@ -110,9 +112,24 @@ func (rt RuleBehavior) String() string { } } +func ParseBehavior(s string) (behavior RuleBehavior, err error) { + switch s { + case "domain": + behavior = Domain + case "ipcidr": + behavior = IPCIDR + case "classical": + behavior = Classical + default: + err = fmt.Errorf("unsupported behavior type: %s", s) + } + return +} + const ( YamlRule RuleFormat = iota TextRule + MrsRule ) type RuleFormat int @@ -123,11 +140,27 @@ func (rf RuleFormat) String() string { return "YamlRule" case TextRule: return "TextRule" + case MrsRule: + return "MrsRule" default: return "Unknown" } } +func ParseRuleFormat(s string) (format RuleFormat, err error) { + switch s { + case "", "yaml": + format = YamlRule + case "text": + format = TextRule + case "mrs": + format = MrsRule + default: + err = fmt.Errorf("unsupported format type: %s", s) + } + return +} + type Tunnel interface { Providers() map[string]ProxyProvider RuleProviders() map[string]RuleProvider diff --git a/docs/config.yaml b/docs/config.yaml index 4e4b9b16..2d3343cf 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -942,6 +942,12 @@ rule-providers: interval: 259200 path: /path/to/save/file.yaml type: file + rule3: # mrs类型ruleset,目前仅支持domain,可以通过“mihomo convert-ruleset domain yaml XXX.yaml XXX.mrs”转换得到 + type: http + url: "url" + format: mrs + behavior: domain + path: /path/to/save/file.mrs rules: - RULE-SET,rule1,REJECT - IP-ASN,1,PROXY diff --git a/go.mod b/go.mod index 9c3a2180..d5ac6bee 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/gobwas/ws v1.4.0 github.com/gofrs/uuid/v5 v5.2.0 github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 + github.com/klauspost/compress v1.17.9 github.com/klauspost/cpuid/v2 v2.2.8 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 github.com/mdlayher/netlink v1.7.2 @@ -82,7 +83,6 @@ require ( github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/klauspost/compress v1.17.4 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mdlayher/socket v0.4.1 // indirect diff --git a/go.sum b/go.sum index ea3b6b53..7e9cd5d8 100644 --- a/go.sum +++ b/go.sum @@ -81,8 +81,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/main.go b/main.go index 61f1d683..cd903ce6 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "github.com/metacubex/mihomo/hub" "github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/rules/provider" "go.uber.org/automaxprocs/maxprocs" ) @@ -48,6 +49,12 @@ func init() { func main() { _, _ = maxprocs.Set(maxprocs.Logger(func(string, ...any) {})) + + if len(os.Args) > 1 && os.Args[1] == "convert-ruleset" { + provider.ConvertMain(os.Args[2:]) + return + } + if version { fmt.Printf("Mihomo Meta %s %s %s with %s %s\n", C.Version, runtime.GOOS, runtime.GOARCH, runtime.Version(), C.BuildTime) diff --git a/rules/provider/domain_strategy.go b/rules/provider/domain_strategy.go index c0787d58..0104fdf9 100644 --- a/rules/provider/domain_strategy.go +++ b/rules/provider/domain_strategy.go @@ -1,6 +1,9 @@ package provider import ( + "errors" + "io" + "github.com/metacubex/mihomo/component/trie" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" @@ -48,6 +51,25 @@ func (d *domainStrategy) FinishInsert() { d.domainTrie = nil } +func (d *domainStrategy) FromMrs(r io.Reader) error { + domainSet, count, err := trie.ReadDomainSetBin(r) + if err != nil { + return err + } + d.count = int(count) + d.domainSet = domainSet + return nil +} + +func (d *domainStrategy) WriteMrs(w io.Writer) error { + if d.domainSet == nil { + return errors.New("nil domainSet") + } + return d.domainSet.WriteBin(w, int64(d.count)) +} + +var _ mrsRuleStrategy = (*domainStrategy)(nil) + func NewDomainStrategy() *domainStrategy { return &domainStrategy{} } diff --git a/rules/provider/mrs_converter.go b/rules/provider/mrs_converter.go new file mode 100644 index 00000000..3b93b4a4 --- /dev/null +++ b/rules/provider/mrs_converter.go @@ -0,0 +1,71 @@ +package provider + +import ( + "io" + "os" + + P "github.com/metacubex/mihomo/constant/provider" + + "github.com/klauspost/compress/zstd" +) + +func ConvertToMrs(buf []byte, behavior P.RuleBehavior, format P.RuleFormat, w io.Writer) (err error) { + strategy := newStrategy(behavior, nil) + strategy, err = rulesParse(buf, strategy, format) + if err != nil { + return err + } + if _strategy, ok := strategy.(mrsRuleStrategy); ok { + var encoder *zstd.Encoder + encoder, err = zstd.NewWriter(w) + if err != nil { + return err + } + defer func() { + zstdErr := encoder.Close() + if err == nil { + err = zstdErr + } + }() + return _strategy.WriteMrs(encoder) + } else { + return ErrInvalidFormat + } +} + +func ConvertMain(args []string) { + if len(args) > 3 { + behavior, err := P.ParseBehavior(args[0]) + if err != nil { + panic(err) + } + format, err := P.ParseRuleFormat(args[1]) + if err != nil { + panic(err) + } + source := args[2] + target := args[3] + + sourceFile, err := os.ReadFile(source) + if err != nil { + panic(err) + } + + targetFile, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + panic(err) + } + + err = ConvertToMrs(sourceFile, behavior, format, targetFile) + if err != nil { + panic(err) + } + + err = targetFile.Close() + if err != nil { + panic(err) + } + } else { + panic("Usage: convert-ruleset ") + } +} diff --git a/rules/provider/parse.go b/rules/provider/parse.go index a20da28d..227debb3 100644 --- a/rules/provider/parse.go +++ b/rules/provider/parse.go @@ -32,28 +32,13 @@ func ParseRuleProvider(name string, mapping map[string]interface{}, parse func(t if err := decoder.Decode(mapping, schema); err != nil { return nil, err } - var behavior P.RuleBehavior - - switch schema.Behavior { - case "domain": - behavior = P.Domain - case "ipcidr": - behavior = P.IPCIDR - case "classical": - behavior = P.Classical - default: - return nil, fmt.Errorf("unsupported behavior type: %s", schema.Behavior) + behavior, err := P.ParseBehavior(schema.Behavior) + if err != nil { + return nil, err } - - var format P.RuleFormat - - switch schema.Format { - case "", "yaml": - format = P.YamlRule - case "text": - format = P.TextRule - default: - return nil, fmt.Errorf("unsupported format type: %s", schema.Format) + format, err := P.ParseRuleFormat(schema.Format) + if err != nil { + return nil, err } var vehicle P.Vehicle diff --git a/rules/provider/provider.go b/rules/provider/provider.go index 6c03c6e5..a4d8883d 100644 --- a/rules/provider/provider.go +++ b/rules/provider/provider.go @@ -4,16 +4,18 @@ import ( "bytes" "encoding/json" "errors" + "io" "runtime" "strings" "time" - "gopkg.in/yaml.v3" - "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/component/resource" C "github.com/metacubex/mihomo/constant" P "github.com/metacubex/mihomo/constant/provider" + + "github.com/klauspost/compress/zstd" + "gopkg.in/yaml.v3" ) var tunnel P.Tunnel @@ -52,6 +54,12 @@ type ruleStrategy interface { FinishInsert() } +type mrsRuleStrategy interface { + ruleStrategy + FromMrs(r io.Reader) error + WriteMrs(w io.Writer) error +} + func (rp *ruleSetProvider) Type() P.ProviderType { return P.Rule } @@ -152,9 +160,23 @@ func newStrategy(behavior P.RuleBehavior, parse func(tp, payload, target string, } var ErrNoPayload = errors.New("file must have a `payload` field") +var ErrInvalidFormat = errors.New("invalid format") func rulesParse(buf []byte, strategy ruleStrategy, format P.RuleFormat) (ruleStrategy, error) { strategy.Reset() + if format == P.MrsRule { + if _strategy, ok := strategy.(mrsRuleStrategy); ok { + reader, err := zstd.NewReader(bytes.NewReader(buf)) + if err != nil { + return nil, err + } + defer reader.Close() + err = _strategy.FromMrs(reader) + return strategy, err + } else { + return nil, ErrInvalidFormat + } + } schema := &RulePayload{} @@ -228,6 +250,8 @@ func rulesParse(buf []byte, strategy ruleStrategy, format P.RuleFormat) (ruleStr if len(schema.Payload) > 0 { str = schema.Payload[0] } + default: + return nil, ErrInvalidFormat } if str == "" {