diff --git a/config/config.go b/config/config.go index ba6097bcf7..f41e004d07 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"` } @@ -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 { + 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 0000000000..8fec594708 --- /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 0000000000..3f7fb0fe3c --- /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 d7b3c64001..ca78f961b4 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ 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/Benyamin-Tehrani/yaml v0.0.0-20241006120305-e828807d2bb4 h1:vigdFfe/DSTa7zMjHe0sARO+essqTXalu1TDC9eyeWw= +github.com/Benyamin-Tehrani/yaml v0.0.0-20241006120305-e828807d2bb4/go.mod h1:bkAoTiD6Ix8zpcA5uv8QVKRC57RlrfVyis3mEik1Y0M= github.com/RyuaNerin/elliptic2 v1.0.0/go.mod h1:wWB8fWrJI/6EPJkyV/r1Rj0hxUgrusmqSj8JN6yNf/A= github.com/RyuaNerin/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4b4Go= github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8= @@ -81,7 +83,10 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2 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.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= @@ -276,10 +281,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=