Skip to content

Commit d3ed64a

Browse files
feat: support override in config file
Signed-off-by: Benyamin-Tehrani <[email protected]>
1 parent 4a16d22 commit d3ed64a

File tree

5 files changed

+339
-0
lines changed

5 files changed

+339
-0
lines changed

config/config.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,15 @@ type RawTLS struct {
355355
CustomTrustCert []string `yaml:"custom-certifactes" json:"custom-certifactes"`
356356
}
357357

358+
type RawOverride struct {
359+
OS string `yaml:"os" json:"os"`
360+
Arch string `yaml:"arch" json:"arch"`
361+
Hostname string `yaml:"hostname" json:"hostname"`
362+
Username string `yaml:"username" json:"username"`
363+
ListStrategy ListMergeStrategy `yaml:"list-strategy" json:"list-strategy"`
364+
Content *RawConfig `yaml:"content" json:"content"`
365+
}
366+
358367
type RawConfig struct {
359368
Port int `yaml:"port" json:"port"`
360369
SocksPort int `yaml:"socks-port" json:"socks-port"`
@@ -420,6 +429,7 @@ type RawConfig struct {
420429
GeoXUrl RawGeoXUrl `yaml:"geox-url" json:"geox-url"`
421430
Sniffer RawSniffer `yaml:"sniffer" json:"sniffer"`
422431
TLS RawTLS `yaml:"tls" json:"tls"`
432+
Override []RawOverride `yaml:"override" json:"override"`
423433

424434
ClashForAndroid RawClashForAndroid `yaml:"clash-for-android" json:"clash-for-android"`
425435
}
@@ -576,6 +586,12 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
576586
log.Infoln("Start initial configuration in progress") //Segment finished in xxm
577587
startTime := time.Now()
578588

589+
// apply overrides
590+
err := ApplyOverride(rawCfg, rawCfg.Override)
591+
if err != nil {
592+
log.Errorln("Error when applying overrides: %v", err)
593+
}
594+
579595
general, err := parseGeneral(rawCfg)
580596
if err != nil {
581597
return nil, err

config/override.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package config
2+
3+
import (
4+
"dario.cat/mergo"
5+
"fmt"
6+
"github.com/metacubex/mihomo/log"
7+
"os"
8+
"os/user"
9+
"reflect"
10+
"runtime"
11+
)
12+
13+
type ListMergeStrategy string
14+
15+
const (
16+
InsertFront ListMergeStrategy = "insert-front"
17+
Append ListMergeStrategy = "append"
18+
Override ListMergeStrategy = "override"
19+
Default ListMergeStrategy = ""
20+
)
21+
22+
// overrideTransformer is to merge slices with give strategy instead of the default behavior
23+
// - insert-front: [old slice] -> [new slice, old slice]
24+
// - append: [old slice] -> [old slice, new slice]
25+
// - override: [old slice] -> [new slice] (Default)
26+
type overrideTransformer struct {
27+
listStrategy ListMergeStrategy
28+
}
29+
30+
func (t overrideTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
31+
if typ.Kind() == reflect.Slice {
32+
return func(dst, src reflect.Value) error {
33+
if src.IsNil() || !dst.CanSet() {
34+
return nil
35+
}
36+
if src.Kind() != reflect.Slice || dst.Kind() != reflect.Slice {
37+
return nil
38+
}
39+
// merge slice according to strategy
40+
switch t.listStrategy {
41+
case InsertFront:
42+
newSlice := reflect.AppendSlice(src, dst)
43+
dst.Set(newSlice)
44+
case Append:
45+
newSlice := reflect.AppendSlice(dst, src)
46+
dst.Set(newSlice)
47+
case Override, Default:
48+
dst.Set(src)
49+
default:
50+
return fmt.Errorf("unknown list override strategy: %s", t.listStrategy)
51+
}
52+
return nil
53+
}
54+
}
55+
return nil
56+
}
57+
58+
func ApplyOverride(rawCfg *RawConfig, overrides []RawOverride) error {
59+
for id, override := range overrides {
60+
if override.OS != "" && override.OS != runtime.GOOS {
61+
continue
62+
}
63+
if override.Arch != "" && override.Arch != runtime.GOARCH {
64+
continue
65+
}
66+
if override.Hostname != "" {
67+
hName, err := os.Hostname()
68+
if err != nil {
69+
log.Warnln("Failed to get hostname when applying override #%v: %v", id, err)
70+
continue
71+
}
72+
if override.Hostname != hName {
73+
continue
74+
}
75+
}
76+
if override.Username != "" {
77+
u, err := user.Current()
78+
if err != nil {
79+
log.Warnln("Failed to get current user when applying override #%v: %v", id, err)
80+
continue
81+
}
82+
if override.Username != u.Username {
83+
continue
84+
}
85+
}
86+
87+
// merge rawConfig override
88+
err := mergo.Merge(rawCfg, *override.Content, mergo.WithTransformers(overrideTransformer{
89+
listStrategy: override.ListStrategy,
90+
}), mergo.WithOverride)
91+
if err != nil {
92+
log.Errorln("Error when applying override #%v: %v", id, err)
93+
}
94+
}
95+
return nil
96+
}

config/override_test.go

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
"github.com/metacubex/mihomo/constant"
6+
"github.com/metacubex/mihomo/log"
7+
"github.com/stretchr/testify/assert"
8+
"os"
9+
"os/user"
10+
"runtime"
11+
"testing"
12+
)
13+
14+
func TestMihomo_Config_Override(t *testing.T) {
15+
t.Run("override_existing", func(t *testing.T) {
16+
config_file := `
17+
mixed-port: 7890
18+
ipv6: true
19+
log-level: debug
20+
allow-lan: false
21+
unified-delay: false
22+
tcp-concurrent: true
23+
external-controller: 127.0.0.1:9090
24+
default-nameserver:
25+
- "223.5.5.5"
26+
override:
27+
- content:
28+
external-controller: 0.0.0.0:9090
29+
allow-lan: true`
30+
rawCfg, err := UnmarshalRawConfig([]byte(config_file))
31+
assert.NoError(t, err)
32+
cfg, err := ParseRawConfig(rawCfg)
33+
assert.NoError(t, err)
34+
assert.Equal(t, log.DEBUG, cfg.General.LogLevel)
35+
assert.Equal(t, true, cfg.General.AllowLan)
36+
assert.Equal(t, "0.0.0.0:9090", cfg.Controller.ExternalController)
37+
})
38+
39+
t.Run("add_new", func(t *testing.T) {
40+
config_file := `
41+
mixed-port: 7890
42+
ipv6: true
43+
log-level: debug
44+
unified-delay: false
45+
tcp-concurrent: true
46+
override:
47+
- content:
48+
external-controller: 0.0.0.0:9090
49+
- content:
50+
allow-lan: true`
51+
rawCfg, err := UnmarshalRawConfig([]byte(config_file))
52+
assert.NoError(t, err)
53+
cfg, err := ParseRawConfig(rawCfg)
54+
assert.NoError(t, err)
55+
assert.Equal(t, log.DEBUG, cfg.General.LogLevel)
56+
assert.Equal(t, true, cfg.General.AllowLan)
57+
assert.Equal(t, "0.0.0.0:9090", cfg.Controller.ExternalController)
58+
})
59+
60+
t.Run("conditions", func(t *testing.T) {
61+
hName, err := os.Hostname()
62+
assert.NoError(t, err)
63+
u, err := user.Current()
64+
assert.NoError(t, err)
65+
66+
config_file := fmt.Sprintf(`
67+
mixed-port: 7890
68+
ipv6: true
69+
log-level: debug
70+
allow-lan: false
71+
unified-delay: false
72+
tcp-concurrent: true
73+
external-controller: 127.0.0.1:9090
74+
default-nameserver:
75+
- "223.5.5.5"
76+
override:
77+
- os: %v
78+
arch: %v
79+
hostname: %v
80+
username: %v
81+
content:
82+
external-controller: 0.0.0.0:9090
83+
allow-lan: true`, runtime.GOOS, runtime.GOARCH, hName, u.Username)
84+
rawCfg, err := UnmarshalRawConfig([]byte(config_file))
85+
assert.NoError(t, err)
86+
cfg, err := ParseRawConfig(rawCfg)
87+
assert.NoError(t, err)
88+
assert.Equal(t, log.DEBUG, cfg.General.LogLevel)
89+
assert.Equal(t, true, cfg.General.AllowLan)
90+
assert.Equal(t, "0.0.0.0:9090", cfg.Controller.ExternalController)
91+
})
92+
93+
t.Run("invalid_condition", func(t *testing.T) {
94+
config_file := `
95+
mixed-port: 7890
96+
log-level: debug
97+
ipv6: true
98+
allow-lan: false
99+
unified-delay: false
100+
tcp-concurrent: true
101+
external-controller: 127.0.0.1:9090
102+
override:
103+
- os: lw2eiru20f923j
104+
content:
105+
external-controller: 0.0.0.0:9090
106+
- arch: 32of9u8p3jrp
107+
content:
108+
allow-lan: true`
109+
rawCfg, err := UnmarshalRawConfig([]byte(config_file))
110+
assert.NoError(t, err)
111+
cfg, err := ParseRawConfig(rawCfg)
112+
assert.NoError(t, err)
113+
assert.Equal(t, log.DEBUG, cfg.General.LogLevel)
114+
assert.Equal(t, false, cfg.General.AllowLan)
115+
assert.Equal(t, "127.0.0.1:9090", cfg.Controller.ExternalController)
116+
})
117+
118+
t.Run("list_insert_front", func(t *testing.T) {
119+
config_file := `
120+
log-level: debug
121+
rules:
122+
- DOMAIN-SUFFIX,foo.com,DIRECT
123+
- DOMAIN-SUFFIX,bar.org,DIRECT
124+
- DOMAIN-SUFFIX,bazz.io,DIRECT
125+
override:
126+
- list-strategy: insert-front
127+
content:
128+
rules:
129+
- GEOIP,lan,DIRECT,no-resolve`
130+
rawCfg, err := UnmarshalRawConfig([]byte(config_file))
131+
assert.NoError(t, err)
132+
cfg, err := ParseRawConfig(rawCfg)
133+
assert.NoError(t, err)
134+
assert.Equal(t, log.DEBUG, cfg.General.LogLevel)
135+
assert.Equal(t, 4, len(cfg.Rules))
136+
assert.Equal(t, constant.GEOIP, cfg.Rules[0].RuleType())
137+
assert.Equal(t, false, cfg.Rules[0].ShouldResolveIP())
138+
})
139+
140+
t.Run("list_append", func(t *testing.T) {
141+
config_file := `
142+
log-level: debug
143+
rules:
144+
- DOMAIN-SUFFIX,foo.com,DIRECT
145+
- DOMAIN-SUFFIX,bar.org,DIRECT
146+
- DOMAIN-SUFFIX,bazz.io,DIRECT
147+
override:
148+
- list-strategy: append
149+
content:
150+
rules:
151+
- GEOIP,lan,DIRECT,no-resolve`
152+
rawCfg, err := UnmarshalRawConfig([]byte(config_file))
153+
assert.NoError(t, err)
154+
cfg, err := ParseRawConfig(rawCfg)
155+
assert.NoError(t, err)
156+
assert.Equal(t, log.DEBUG, cfg.General.LogLevel)
157+
assert.Equal(t, 4, len(cfg.Rules))
158+
assert.Equal(t, constant.GEOIP, cfg.Rules[3].RuleType())
159+
assert.Equal(t, false, cfg.Rules[3].ShouldResolveIP())
160+
})
161+
162+
t.Run("list_override", func(t *testing.T) {
163+
config_file := `
164+
log-level: debug
165+
proxies:
166+
- name: "DIRECT-PROXY"
167+
type: direct
168+
udp: true
169+
- name: "SOCKS-PROXY"
170+
type: socks5
171+
server: foo.com
172+
port: 443
173+
override:
174+
- list-strategy: override
175+
content:
176+
proxies:
177+
- name: "HTTP-PROXY"
178+
type: http
179+
server: bar.org
180+
port: 443`
181+
rawCfg, err := UnmarshalRawConfig([]byte(config_file))
182+
assert.NoError(t, err)
183+
cfg, err := ParseRawConfig(rawCfg)
184+
assert.NoError(t, err)
185+
assert.Equal(t, log.DEBUG, cfg.General.LogLevel)
186+
assert.NotContains(t, cfg.Proxies, "DIRECT-PROXY")
187+
assert.NotContains(t, cfg.Proxies, "SOCKS-PROXY")
188+
assert.Contains(t, cfg.Proxies, "HTTP-PROXY")
189+
assert.Equal(t, constant.Http, cfg.Proxies["HTTP-PROXY"].Type())
190+
})
191+
192+
t.Run("map_merge", func(t *testing.T) {
193+
config_file := `
194+
log-level: debug
195+
proxy-providers:
196+
provider1:
197+
url: "foo.com"
198+
type: http
199+
interval: 86400
200+
health-check: {enable: true,url: "https://www.gstatic.com/generate_204", interval: 300}
201+
provider2:
202+
url: "bar.com"
203+
type: http
204+
interval: 86400
205+
health-check: {enable: true,url: "https://www.gstatic.com/generate_204", interval: 300}
206+
override:
207+
- content:
208+
proxy-providers:
209+
provider3:
210+
url: "buzz.com"
211+
type: http
212+
interval: 86400
213+
health-check: {enable: true,url: "https://www.google.com", interval: 300}`
214+
rawCfg, err := UnmarshalRawConfig([]byte(config_file))
215+
assert.NoError(t, err)
216+
cfg, err := ParseRawConfig(rawCfg)
217+
assert.NoError(t, err)
218+
assert.Equal(t, log.DEBUG, cfg.General.LogLevel)
219+
assert.Contains(t, cfg.Providers, "provider1")
220+
assert.Contains(t, cfg.Providers, "provider2")
221+
assert.Contains(t, cfg.Providers, "provider3")
222+
assert.Equal(t, "https://www.google.com", cfg.Providers["provider3"].HealthCheckURL())
223+
})
224+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/metacubex/mihomo
33
go 1.20
44

55
require (
6+
dario.cat/mergo v1.0.1
67
github.com/3andne/restls-client-go v0.1.6
78
github.com/bahlo/generic-list-go v0.2.0
89
github.com/coreos/go-iptables v0.7.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
2+
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
13
github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08=
24
github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY=
35
github.com/RyuaNerin/elliptic2 v1.0.0/go.mod h1:wWB8fWrJI/6EPJkyV/r1Rj0hxUgrusmqSj8JN6yNf/A=

0 commit comments

Comments
 (0)