diff --git a/config/config.go b/config/config.go index 3117853cbd..2544235024 100644 --- a/config/config.go +++ b/config/config.go @@ -355,6 +355,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"` @@ -420,6 +429,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"` } @@ -576,6 +586,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 0000000000..5893e8a2bc --- /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 0000000000..1e07b61287 --- /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 4b066c9017..f903d50650 100644 --- a/go.mod +++ b/go.mod @@ -61,6 +61,7 @@ require ( ) require ( + dario.cat/mergo v1.0.1 // indirect github.com/RyuaNerin/go-krypto v1.2.4 // indirect github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect github.com/ajg/form v1.5.1 // indirect diff --git a/go.sum b/go.sum index 60818034d1..ff7bc70145 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=