Skip to content

Commit

Permalink
feat: support override in config file
Browse files Browse the repository at this point in the history
Signed-off-by: Benyamin-Tehrani <[email protected]>
  • Loading branch information
Benyamin-Tehrani committed Oct 4, 2024
1 parent 4a16d22 commit ece423e
Show file tree
Hide file tree
Showing 5 changed files with 339 additions and 0 deletions.
16 changes: 16 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
}
Expand Down Expand Up @@ -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
Expand Down
96 changes: 96 additions & 0 deletions config/override.go
Original file line number Diff line number Diff line change
@@ -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
}
224 changes: 224 additions & 0 deletions config/override_test.go
Original file line number Diff line number Diff line change
@@ -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())
})
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down

0 comments on commit ece423e

Please sign in to comment.