diff --git a/config/config.go b/config/config.go index ce1e0fb7..b2a5f8ce 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 map[string]any `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"` } @@ -574,6 +584,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 { + return nil, 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..8fec5947 --- /dev/null +++ b/config/override.go @@ -0,0 +1,84 @@ +package config + +import ( + "errors" + "fmt" + "github.com/metacubex/mihomo/log" + "gopkg.in/yaml.v3" + "os" + "os/user" + "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" + Override ListMergeStrategy = "override" + Default ListMergeStrategy = "" +) + +func ApplyOverride(rawCfg *RawConfig, overrides []RawOverride) error { + for id, override := range overrides { + // check override conditions + 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 + } + } + + // 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) + return err + } + + // 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: + 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) + return err + } + } + return nil +} diff --git a/config/override_test.go b/config/override_test.go new file mode 100644 index 00000000..3f7fb0fe --- /dev/null +++ b/config/override_test.go @@ -0,0 +1,271 @@ +package config + +import ( + "fmt" + "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" + "github.com/stretchr/testify/assert" + "os" + "os/user" + "runtime" + "strings" + "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("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 +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()) + }) + + 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:")) + }) +} diff --git a/go.sum b/go.sum index df7b6028..80e65e1c 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=