From ddd05309028125b53f51e160b7c50a18562314d5 Mon Sep 17 00:00:00 2001 From: Benyamin-Tehrani <149029835+Benyamin-Tehrani@users.noreply.github.com> Date: Fri, 4 Oct 2024 20:40:48 +0800 Subject: [PATCH 1/3] feat: support override in config file Signed-off-by: Benyamin-Tehrani <149029835+Benyamin-Tehrani@users.noreply.github.com> --- config/config.go | 16 +++ config/override.go | 96 +++++++++++++++++ config/override_test.go | 224 ++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + 5 files changed, 339 insertions(+) create mode 100644 config/override.go create mode 100644 config/override_test.go diff --git a/config/config.go b/config/config.go index ba6097bc..b1669aad 100644 --- a/config/config.go +++ b/config/config.go @@ -359,6 +359,15 @@ type RawTLS struct { CustomTrustCert []string `yaml:"custom-certifactes" json:"custom-certifactes"` } +type RawOverride struct { + OS string `yaml:"os" json:"os"` + Arch string `yaml:"arch" json:"arch"` + Hostname string `yaml:"hostname" json:"hostname"` + Username string `yaml:"username" json:"username"` + ListStrategy ListMergeStrategy `yaml:"list-strategy" json:"list-strategy"` + Content *RawConfig `yaml:"content" json:"content"` +} + type RawConfig struct { Port int `yaml:"port" json:"port"` SocksPort int `yaml:"socks-port" json:"socks-port"` @@ -424,6 +433,7 @@ type RawConfig struct { GeoXUrl RawGeoXUrl `yaml:"geox-url" json:"geox-url"` Sniffer RawSniffer `yaml:"sniffer" json:"sniffer"` TLS RawTLS `yaml:"tls" json:"tls"` + Override []RawOverride `yaml:"override" json:"override"` ClashForAndroid RawClashForAndroid `yaml:"clash-for-android" json:"clash-for-android"` } @@ -580,6 +590,12 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { log.Infoln("Start initial configuration in progress") //Segment finished in xxm startTime := time.Now() + // apply overrides + err := ApplyOverride(rawCfg, rawCfg.Override) + if err != nil { + log.Errorln("Error when applying overrides: %v", err) + } + general, err := parseGeneral(rawCfg) if err != nil { return nil, err diff --git a/config/override.go b/config/override.go new file mode 100644 index 00000000..5893e8a2 --- /dev/null +++ b/config/override.go @@ -0,0 +1,96 @@ +package config + +import ( + "dario.cat/mergo" + "fmt" + "github.com/metacubex/mihomo/log" + "os" + "os/user" + "reflect" + "runtime" +) + +type ListMergeStrategy string + +const ( + InsertFront ListMergeStrategy = "insert-front" + Append ListMergeStrategy = "append" + Override ListMergeStrategy = "override" + Default ListMergeStrategy = "" +) + +// overrideTransformer is to merge slices with give strategy instead of the default behavior +// - insert-front: [old slice] -> [new slice, old slice] +// - append: [old slice] -> [old slice, new slice] +// - override: [old slice] -> [new slice] (Default) +type overrideTransformer struct { + listStrategy ListMergeStrategy +} + +func (t overrideTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error { + if typ.Kind() == reflect.Slice { + return func(dst, src reflect.Value) error { + if src.IsNil() || !dst.CanSet() { + return nil + } + if src.Kind() != reflect.Slice || dst.Kind() != reflect.Slice { + return nil + } + // merge slice according to strategy + switch t.listStrategy { + case InsertFront: + newSlice := reflect.AppendSlice(src, dst) + dst.Set(newSlice) + case Append: + newSlice := reflect.AppendSlice(dst, src) + dst.Set(newSlice) + case Override, Default: + dst.Set(src) + default: + return fmt.Errorf("unknown list override strategy: %s", t.listStrategy) + } + return nil + } + } + return nil +} + +func ApplyOverride(rawCfg *RawConfig, overrides []RawOverride) error { + for id, override := range overrides { + if override.OS != "" && override.OS != runtime.GOOS { + continue + } + if override.Arch != "" && override.Arch != runtime.GOARCH { + continue + } + if override.Hostname != "" { + hName, err := os.Hostname() + if err != nil { + log.Warnln("Failed to get hostname when applying override #%v: %v", id, err) + continue + } + if override.Hostname != hName { + continue + } + } + if override.Username != "" { + u, err := user.Current() + if err != nil { + log.Warnln("Failed to get current user when applying override #%v: %v", id, err) + continue + } + if override.Username != u.Username { + continue + } + } + + // merge rawConfig override + err := mergo.Merge(rawCfg, *override.Content, mergo.WithTransformers(overrideTransformer{ + listStrategy: override.ListStrategy, + }), mergo.WithOverride) + if err != nil { + log.Errorln("Error when applying override #%v: %v", id, err) + } + } + return nil +} diff --git a/config/override_test.go b/config/override_test.go new file mode 100644 index 00000000..1e07b612 --- /dev/null +++ b/config/override_test.go @@ -0,0 +1,224 @@ +package config + +import ( + "fmt" + "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" + "github.com/stretchr/testify/assert" + "os" + "os/user" + "runtime" + "testing" +) + +func TestMihomo_Config_Override(t *testing.T) { + t.Run("override_existing", func(t *testing.T) { + config_file := ` +mixed-port: 7890 +ipv6: true +log-level: debug +allow-lan: false +unified-delay: false +tcp-concurrent: true +external-controller: 127.0.0.1:9090 +default-nameserver: + - "223.5.5.5" +override: + - content: + external-controller: 0.0.0.0:9090 + allow-lan: true` + rawCfg, err := UnmarshalRawConfig([]byte(config_file)) + assert.NoError(t, err) + cfg, err := ParseRawConfig(rawCfg) + assert.NoError(t, err) + assert.Equal(t, log.DEBUG, cfg.General.LogLevel) + assert.Equal(t, true, cfg.General.AllowLan) + assert.Equal(t, "0.0.0.0:9090", cfg.Controller.ExternalController) + }) + + t.Run("add_new", func(t *testing.T) { + config_file := ` +mixed-port: 7890 +ipv6: true +log-level: debug +unified-delay: false +tcp-concurrent: true +override: + - content: + external-controller: 0.0.0.0:9090 + - content: + allow-lan: true` + rawCfg, err := UnmarshalRawConfig([]byte(config_file)) + assert.NoError(t, err) + cfg, err := ParseRawConfig(rawCfg) + assert.NoError(t, err) + assert.Equal(t, log.DEBUG, cfg.General.LogLevel) + assert.Equal(t, true, cfg.General.AllowLan) + assert.Equal(t, "0.0.0.0:9090", cfg.Controller.ExternalController) + }) + + t.Run("conditions", func(t *testing.T) { + hName, err := os.Hostname() + assert.NoError(t, err) + u, err := user.Current() + assert.NoError(t, err) + + config_file := fmt.Sprintf(` +mixed-port: 7890 +ipv6: true +log-level: debug +allow-lan: false +unified-delay: false +tcp-concurrent: true +external-controller: 127.0.0.1:9090 +default-nameserver: + - "223.5.5.5" +override: + - os: %v + arch: %v + hostname: %v + username: %v + content: + external-controller: 0.0.0.0:9090 + allow-lan: true`, runtime.GOOS, runtime.GOARCH, hName, u.Username) + rawCfg, err := UnmarshalRawConfig([]byte(config_file)) + assert.NoError(t, err) + cfg, err := ParseRawConfig(rawCfg) + assert.NoError(t, err) + assert.Equal(t, log.DEBUG, cfg.General.LogLevel) + assert.Equal(t, true, cfg.General.AllowLan) + assert.Equal(t, "0.0.0.0:9090", cfg.Controller.ExternalController) + }) + + t.Run("invalid_condition", func(t *testing.T) { + config_file := ` +mixed-port: 7890 +log-level: debug +ipv6: true +allow-lan: false +unified-delay: false +tcp-concurrent: true +external-controller: 127.0.0.1:9090 +override: + - os: lw2eiru20f923j + content: + external-controller: 0.0.0.0:9090 + - arch: 32of9u8p3jrp + content: + allow-lan: true` + rawCfg, err := UnmarshalRawConfig([]byte(config_file)) + assert.NoError(t, err) + cfg, err := ParseRawConfig(rawCfg) + assert.NoError(t, err) + assert.Equal(t, log.DEBUG, cfg.General.LogLevel) + assert.Equal(t, false, cfg.General.AllowLan) + assert.Equal(t, "127.0.0.1:9090", cfg.Controller.ExternalController) + }) + + t.Run("list_insert_front", func(t *testing.T) { + config_file := ` +log-level: debug +rules: + - DOMAIN-SUFFIX,foo.com,DIRECT + - DOMAIN-SUFFIX,bar.org,DIRECT + - DOMAIN-SUFFIX,bazz.io,DIRECT +override: + - list-strategy: insert-front + content: + rules: + - GEOIP,lan,DIRECT,no-resolve` + rawCfg, err := UnmarshalRawConfig([]byte(config_file)) + assert.NoError(t, err) + cfg, err := ParseRawConfig(rawCfg) + assert.NoError(t, err) + assert.Equal(t, log.DEBUG, cfg.General.LogLevel) + assert.Equal(t, 4, len(cfg.Rules)) + assert.Equal(t, constant.GEOIP, cfg.Rules[0].RuleType()) + assert.Equal(t, false, cfg.Rules[0].ShouldResolveIP()) + }) + + t.Run("list_append", func(t *testing.T) { + config_file := ` +log-level: debug +rules: + - DOMAIN-SUFFIX,foo.com,DIRECT + - DOMAIN-SUFFIX,bar.org,DIRECT + - DOMAIN-SUFFIX,bazz.io,DIRECT +override: + - list-strategy: append + content: + rules: + - GEOIP,lan,DIRECT,no-resolve` + rawCfg, err := UnmarshalRawConfig([]byte(config_file)) + assert.NoError(t, err) + cfg, err := ParseRawConfig(rawCfg) + assert.NoError(t, err) + assert.Equal(t, log.DEBUG, cfg.General.LogLevel) + assert.Equal(t, 4, len(cfg.Rules)) + assert.Equal(t, constant.GEOIP, cfg.Rules[3].RuleType()) + assert.Equal(t, false, cfg.Rules[3].ShouldResolveIP()) + }) + + t.Run("list_override", func(t *testing.T) { + config_file := ` +log-level: debug +proxies: + - name: "DIRECT-PROXY" + type: direct + udp: true + - name: "SOCKS-PROXY" + type: socks5 + server: foo.com + port: 443 +override: + - list-strategy: override + content: + proxies: + - name: "HTTP-PROXY" + type: http + server: bar.org + port: 443` + rawCfg, err := UnmarshalRawConfig([]byte(config_file)) + assert.NoError(t, err) + cfg, err := ParseRawConfig(rawCfg) + assert.NoError(t, err) + assert.Equal(t, log.DEBUG, cfg.General.LogLevel) + assert.NotContains(t, cfg.Proxies, "DIRECT-PROXY") + assert.NotContains(t, cfg.Proxies, "SOCKS-PROXY") + assert.Contains(t, cfg.Proxies, "HTTP-PROXY") + assert.Equal(t, constant.Http, cfg.Proxies["HTTP-PROXY"].Type()) + }) + + t.Run("map_merge", func(t *testing.T) { + config_file := ` +log-level: debug +proxy-providers: + provider1: + url: "foo.com" + type: http + interval: 86400 + health-check: {enable: true,url: "https://www.gstatic.com/generate_204", interval: 300} + provider2: + url: "bar.com" + type: http + interval: 86400 + health-check: {enable: true,url: "https://www.gstatic.com/generate_204", interval: 300} +override: + - content: + proxy-providers: + provider3: + url: "buzz.com" + type: http + interval: 86400 + health-check: {enable: true,url: "https://www.google.com", interval: 300}` + rawCfg, err := UnmarshalRawConfig([]byte(config_file)) + assert.NoError(t, err) + cfg, err := ParseRawConfig(rawCfg) + assert.NoError(t, err) + assert.Equal(t, log.DEBUG, cfg.General.LogLevel) + assert.Contains(t, cfg.Providers, "provider1") + assert.Contains(t, cfg.Providers, "provider2") + assert.Contains(t, cfg.Providers, "provider3") + assert.Equal(t, "https://www.google.com", cfg.Providers["provider3"].HealthCheckURL()) + }) +} diff --git a/go.mod b/go.mod index 9405a2f1..429e4beb 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/metacubex/mihomo go 1.20 require ( + dario.cat/mergo v1.0.1 github.com/3andne/restls-client-go v0.1.6 github.com/bahlo/generic-list-go v0.2.0 github.com/coreos/go-iptables v0.8.0 diff --git a/go.sum b/go.sum index 9660cf30..492cda72 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08= github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY= github.com/RyuaNerin/elliptic2 v1.0.0/go.mod h1:wWB8fWrJI/6EPJkyV/r1Rj0hxUgrusmqSj8JN6yNf/A= From 83279c3772fc8112e71a86566b4f9a752020fa09 Mon Sep 17 00:00:00 2001 From: Benyamin-Tehrani <149029835+Benyamin-Tehrani@users.noreply.github.com> Date: Sun, 6 Oct 2024 20:25:30 +0800 Subject: [PATCH 2/3] fix: fix zero-value problem in config override --- config/config.go | 2 +- config/override.go | 70 ++++++++++++++++------------------------- config/override_test.go | 24 ++++++++++++++ go.mod | 1 - 4 files changed, 52 insertions(+), 45 deletions(-) diff --git a/config/config.go b/config/config.go index b1669aad..ac4ba4f1 100644 --- a/config/config.go +++ b/config/config.go @@ -365,7 +365,7 @@ type RawOverride struct { Hostname string `yaml:"hostname" json:"hostname"` Username string `yaml:"username" json:"username"` ListStrategy ListMergeStrategy `yaml:"list-strategy" json:"list-strategy"` - Content *RawConfig `yaml:"content" json:"content"` + Content map[string]any `yaml:"content" json:"content"` } type RawConfig struct { diff --git a/config/override.go b/config/override.go index 5893e8a2..971bf5ac 100644 --- a/config/override.go +++ b/config/override.go @@ -1,17 +1,19 @@ package config import ( - "dario.cat/mergo" - "fmt" "github.com/metacubex/mihomo/log" + "gopkg.in/yaml.v3" "os" "os/user" - "reflect" "runtime" ) type ListMergeStrategy string +// insert-front: [old slice] -> [new slice, old slice] +// append: [old slice] -> [old slice, new slice] +// override: [old slice] -> [new slice] (Default) + const ( InsertFront ListMergeStrategy = "insert-front" Append ListMergeStrategy = "append" @@ -19,44 +21,9 @@ const ( Default ListMergeStrategy = "" ) -// overrideTransformer is to merge slices with give strategy instead of the default behavior -// - insert-front: [old slice] -> [new slice, old slice] -// - append: [old slice] -> [old slice, new slice] -// - override: [old slice] -> [new slice] (Default) -type overrideTransformer struct { - listStrategy ListMergeStrategy -} - -func (t overrideTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error { - if typ.Kind() == reflect.Slice { - return func(dst, src reflect.Value) error { - if src.IsNil() || !dst.CanSet() { - return nil - } - if src.Kind() != reflect.Slice || dst.Kind() != reflect.Slice { - return nil - } - // merge slice according to strategy - switch t.listStrategy { - case InsertFront: - newSlice := reflect.AppendSlice(src, dst) - dst.Set(newSlice) - case Append: - newSlice := reflect.AppendSlice(dst, src) - dst.Set(newSlice) - case Override, Default: - dst.Set(src) - default: - return fmt.Errorf("unknown list override strategy: %s", t.listStrategy) - } - return nil - } - } - return nil -} - func ApplyOverride(rawCfg *RawConfig, overrides []RawOverride) error { for id, override := range overrides { + // check override conditions if override.OS != "" && override.OS != runtime.GOOS { continue } @@ -84,12 +51,29 @@ func ApplyOverride(rawCfg *RawConfig, overrides []RawOverride) error { } } - // merge rawConfig override - err := mergo.Merge(rawCfg, *override.Content, mergo.WithTransformers(overrideTransformer{ - listStrategy: override.ListStrategy, - }), mergo.WithOverride) + // marshal override content back to text + overrideContent, err := yaml.Marshal(override.Content) if err != nil { log.Errorln("Error when applying override #%v: %v", id, err) + continue + } + + // unmarshal override content into rawConfig, with custom list merge strategy + switch override.ListStrategy { + case Append: + options := yaml.NewDecodeOptions().ListDecodeOption(yaml.ListDecodeAppend) + err = yaml.UnmarshalWith(options, overrideContent, rawCfg) + case InsertFront: + options := yaml.NewDecodeOptions().ListDecodeOption(yaml.ListDecodeInsertFront) + err = yaml.UnmarshalWith(options, overrideContent, rawCfg) + case Override, Default: + err = yaml.Unmarshal(overrideContent, rawCfg) + default: + log.Errorln("Bad list strategy in override #%v: %v", id, override.ListStrategy) + } + if err != nil { + log.Errorln("Error when applying override #%v: %v", id, err) + continue } } return nil diff --git a/config/override_test.go b/config/override_test.go index 1e07b612..f564ef39 100644 --- a/config/override_test.go +++ b/config/override_test.go @@ -36,6 +36,30 @@ override: assert.Equal(t, "0.0.0.0:9090", cfg.Controller.ExternalController) }) + t.Run("override_zero_value_test", func(t *testing.T) { + config_file := ` +mixed-port: 7890 +ipv6: true +log-level: debug +allow-lan: true +unified-delay: false +tcp-concurrent: true +external-controller: 127.0.0.1:9090 +default-nameserver: + - "223.5.5.5" +override: + - content: + external-controller: "" + allow-lan: false` + rawCfg, err := UnmarshalRawConfig([]byte(config_file)) + assert.NoError(t, err) + cfg, err := ParseRawConfig(rawCfg) + assert.NoError(t, err) + assert.Equal(t, log.DEBUG, cfg.General.LogLevel) + assert.Equal(t, false, cfg.General.AllowLan) + assert.Equal(t, "", cfg.Controller.ExternalController) + }) + t.Run("add_new", func(t *testing.T) { config_file := ` mixed-port: 7890 diff --git a/go.mod b/go.mod index 429e4beb..9405a2f1 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/metacubex/mihomo go 1.20 require ( - dario.cat/mergo v1.0.1 github.com/3andne/restls-client-go v0.1.6 github.com/bahlo/generic-list-go v0.2.0 github.com/coreos/go-iptables v0.8.0 From 53b5c8e859bb54f688c9489f64f12aaeb1288223 Mon Sep 17 00:00:00 2001 From: Benyamin-Tehrani <149029835+Benyamin-Tehrani@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:20:33 +0800 Subject: [PATCH 3/3] chore: return error when override fails --- config/config.go | 2 +- config/override.go | 10 +++++++--- config/override_test.go | 23 +++++++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index ac4ba4f1..f41e004d 100644 --- a/config/config.go +++ b/config/config.go @@ -593,7 +593,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { // apply overrides err := ApplyOverride(rawCfg, rawCfg.Override) if err != nil { - log.Errorln("Error when applying overrides: %v", err) + return nil, err } general, err := parseGeneral(rawCfg) diff --git a/config/override.go b/config/override.go index 971bf5ac..8fec5947 100644 --- a/config/override.go +++ b/config/override.go @@ -1,6 +1,8 @@ package config import ( + "errors" + "fmt" "github.com/metacubex/mihomo/log" "gopkg.in/yaml.v3" "os" @@ -55,7 +57,7 @@ func ApplyOverride(rawCfg *RawConfig, overrides []RawOverride) error { overrideContent, err := yaml.Marshal(override.Content) if err != nil { log.Errorln("Error when applying override #%v: %v", id, err) - continue + return err } // unmarshal override content into rawConfig, with custom list merge strategy @@ -69,11 +71,13 @@ func ApplyOverride(rawCfg *RawConfig, overrides []RawOverride) error { case Override, Default: err = yaml.Unmarshal(overrideContent, rawCfg) default: - log.Errorln("Bad list strategy in override #%v: %v", id, override.ListStrategy) + err = errors.New(fmt.Sprintf("Bad list strategy in override #%v: %v", id, override.ListStrategy)) + log.Errorln(err.Error()) + return err } if err != nil { log.Errorln("Error when applying override #%v: %v", id, err) - continue + return err } } return nil diff --git a/config/override_test.go b/config/override_test.go index f564ef39..3f7fb0fe 100644 --- a/config/override_test.go +++ b/config/override_test.go @@ -8,6 +8,7 @@ import ( "os" "os/user" "runtime" + "strings" "testing" ) @@ -245,4 +246,26 @@ override: assert.Contains(t, cfg.Providers, "provider3") assert.Equal(t, "https://www.google.com", cfg.Providers["provider3"].HealthCheckURL()) }) + + t.Run("bad_override", func(t *testing.T) { + config_file := ` +mixed-port: 7890 +ipv6: true +log-level: debug +allow-lan: false +unified-delay: false +tcp-concurrent: true +external-controller: 127.0.0.1:9090 +default-nameserver: + - "223.5.5.5" +override: + - list-strategy: 12wlfiu3o + content: + external-controller: 0.0.0.0:9090 + allow-lan: true` + rawCfg, err := UnmarshalRawConfig([]byte(config_file)) + assert.NoError(t, err) + _, err = ParseRawConfig(rawCfg) + assert.True(t, strings.HasPrefix(err.Error(), "Bad list strategy in override #0:")) + }) }