diff --git a/config.go b/config.go index 85901b1..10e50a6 100644 --- a/config.go +++ b/config.go @@ -20,8 +20,8 @@ type Config struct { Listens []string - Forwards []string - StrategyConfig rule.StrategyConfig + Forwards []string + Strategy rule.Strategy RuleFiles []string RulesDir string @@ -43,17 +43,16 @@ func parseConfig() *Config { flag.StringSliceUniqVar(&conf.Listens, "listen", nil, "listen url, format: SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS") flag.StringSliceUniqVar(&conf.Forwards, "forward", nil, "forward url, format: SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS[,SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS]") - flag.StringVar(&conf.StrategyConfig.Strategy, "strategy", "rr", "forward strategy, default: rr") - flag.StringVar(&conf.StrategyConfig.CheckType, "checktype", "http", "fowarder check type, http/tcp") - flag.StringVar(&conf.StrategyConfig.CheckAddr, "checkaddr", "www.apple.com:80", "fowarder check addr, format: HOST[:PORT], default port: 80,") - flag.IntVar(&conf.StrategyConfig.CheckInterval, "checkinterval", 30, "fowarder check interval(seconds)") - flag.IntVar(&conf.StrategyConfig.CheckTimeout, "checktimeout", 10, "fowarder check timeout(seconds)") - flag.IntVar(&conf.StrategyConfig.CheckTolerance, "checktolerance", 0, "fowarder check tolerance(ms), switch only when new_latency < old_latency - tolerance, only used in lha mode") - flag.BoolVar(&conf.StrategyConfig.CheckDisabledOnly, "checkdisabledonly", false, "check disabled fowarders only") - flag.IntVar(&conf.StrategyConfig.MaxFailures, "maxfailures", 3, "max failures to change forwarder status to disabled") - flag.IntVar(&conf.StrategyConfig.DialTimeout, "dialtimeout", 3, "dial timeout(seconds)") - flag.IntVar(&conf.StrategyConfig.RelayTimeout, "relaytimeout", 0, "relay timeout(seconds)") - flag.StringVar(&conf.StrategyConfig.IntFace, "interface", "", "source ip or source interface") + flag.StringVar(&conf.Strategy.Strategy, "strategy", "rr", "forward strategy, default: rr") + flag.StringVar(&conf.Strategy.Check, "check", "http://www.msftconnecttest.com/connecttest.txt#expect=200", "check=disable: disable health check\ncheck=tcp[://HOST:PORT]: tcp port connect check\ncheck=http://HOST[:PORT][/URI][#expect=STRING_IN_RESP_LINE]") + flag.IntVar(&conf.Strategy.CheckInterval, "checkinterval", 30, "fowarder check interval(seconds)") + flag.IntVar(&conf.Strategy.CheckTimeout, "checktimeout", 10, "fowarder check timeout(seconds)") + flag.IntVar(&conf.Strategy.CheckTolerance, "checktolerance", 0, "fowarder check tolerance(ms), switch only when new_latency < old_latency - tolerance, only used in lha mode") + flag.BoolVar(&conf.Strategy.CheckDisabledOnly, "checkdisabledonly", false, "check disabled fowarders only") + flag.IntVar(&conf.Strategy.MaxFailures, "maxfailures", 3, "max failures to change forwarder status to disabled") + flag.IntVar(&conf.Strategy.DialTimeout, "dialtimeout", 3, "dial timeout(seconds)") + flag.IntVar(&conf.Strategy.RelayTimeout, "relaytimeout", 0, "relay timeout(seconds)") + flag.StringVar(&conf.Strategy.IntFace, "interface", "", "source ip or source interface") flag.StringSliceUniqVar(&conf.RuleFiles, "rulefile", nil, "rule file path") flag.StringVar(&conf.RulesDir, "rules-dir", "", "rule file folder") diff --git a/go.mod b/go.mod index 951f785..ef9f245 100644 --- a/go.mod +++ b/go.mod @@ -15,8 +15,8 @@ require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/xtaci/kcp-go/v5 v5.6.1 golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 - golang.org/x/sys v0.0.0-20201116194326-cc9327a14d48 // indirect - golang.org/x/tools v0.0.0-20201117152513-9036a0f9af11 // indirect + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect + golang.org/x/tools v0.0.0-20201120032337-6d151481565c // indirect gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect ) diff --git a/go.sum b/go.sum index 8299fcd..bda791f 100644 --- a/go.sum +++ b/go.sum @@ -128,8 +128,8 @@ golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201112073958-5cba982894dd h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY= golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201116194326-cc9327a14d48 h1:AYCWBZhgIw6XobZ5CibNJr0Rc4ZofGGKvWa1vcx2IGk= -golang.org/x/sys v0.0.0-20201116194326-cc9327a14d48/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -141,8 +141,8 @@ golang.org/x/tools v0.0.0-20200425043458-8463f397d07c/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200808161706-5bf02b21f123/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201105001634-bc3cf281b174 h1:0rx0F4EjJNbxTuzWe0KjKcIzs+3VEb/Mrs/d1ciNz1c= golang.org/x/tools v0.0.0-20201105001634-bc3cf281b174/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201117152513-9036a0f9af11 h1:gqcmLJzeDSNhSzkyhJ4kxP6CtTimi/5hWFDGp0lFd1w= -golang.org/x/tools v0.0.0-20201117152513-9036a0f9af11/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201120032337-6d151481565c h1:IXtuZap6vTKIQ3jemmcwf2gY4BT+lwfZHBYwxMGe5/k= +golang.org/x/tools v0.0.0-20201120032337-6d151481565c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index 71035f3..6efee32 100644 --- a/main.go +++ b/main.go @@ -24,7 +24,7 @@ var ( func main() { // global rule proxy - pxy := rule.NewProxy(config.Forwards, &config.StrategyConfig, config.rules) + pxy := rule.NewProxy(config.Forwards, &config.Strategy, config.rules) // ipset manager ipsetM, _ := ipset.NewManager(config.rules) diff --git a/rule/check.go b/rule/check.go new file mode 100644 index 0000000..bc9737d --- /dev/null +++ b/rule/check.go @@ -0,0 +1,117 @@ +package rule + +import ( + "bytes" + "io" + "time" + + "github.com/nadoo/glider/log" + "github.com/nadoo/glider/pool" +) + +// Checker is a forwarder health checker. +type Checker interface { + Check(fwdr *Forwarder) (healthy bool) +} + +type tcpChecker struct { + addr string + timeout time.Duration +} + +func newTcpChecker(addr string, timeout time.Duration) *tcpChecker { + return &tcpChecker{addr, timeout} +} + +func (c *tcpChecker) Check(fwdr *Forwarder) bool { + startTime := time.Now() + + rc, err := fwdr.Dial("tcp", c.addr) + if err != nil { + log.F("[check] tcp:%s(%d), FAILED. error in dial: %s", fwdr.Addr(), fwdr.Priority(), err) + fwdr.Disable() + return false + } + defer rc.Close() + + if c.timeout > 0 { + rc.SetDeadline(time.Now().Add(c.timeout)) + } + + elapsed := time.Since(startTime) + fwdr.SetLatency(int64(elapsed)) + + if elapsed > c.timeout { + log.F("[check] tcp:%s(%d), FAILED. check timeout: %s", fwdr.Addr(), fwdr.Priority(), elapsed) + fwdr.Disable() + return false + } + + log.F("[check] tcp:%s(%d), SUCCESS. elapsed: %s", fwdr.Addr(), fwdr.Priority(), elapsed) + fwdr.Enable() + + return true +} + +type httpChecker struct { + addr string + uri string + expect string + timeout time.Duration +} + +func newHttpChecker(addr, uri, expect string, timeout time.Duration) *httpChecker { + return &httpChecker{addr, uri, expect, timeout} +} + +func (c *httpChecker) Check(fwdr *Forwarder) bool { + startTime := time.Now() + rc, err := fwdr.Dial("tcp", c.addr) + if err != nil { + log.F("[check] %s(%d) -> http://%s, FAILED. error in dial: %s", fwdr.Addr(), fwdr.Priority(), c.addr, err) + fwdr.Disable() + return false + } + defer rc.Close() + + if c.timeout > 0 { + rc.SetDeadline(time.Now().Add(c.timeout)) + } + + _, err = io.WriteString(rc, "GET "+c.uri+" HTTP/1.1\r\nHost:"+c.addr+"\r\nConnection: close"+"\r\n\r\n") + if err != nil { + log.F("[check] %s(%d) -> http://%s, FAILED. error in write: %s", fwdr.Addr(), fwdr.Priority(), c.addr, err) + fwdr.Disable() + return false + } + + r := pool.GetBufReader(rc) + defer pool.PutBufReader(r) + + line, _, err := r.ReadLine() + if err != nil { + log.F("[check] %s(%d) -> http://%s, FAILED. error in read: %s", fwdr.Addr(), fwdr.Priority(), c.addr, err) + fwdr.Disable() + return false + } + + if !bytes.Contains(line, []byte(c.expect)) { + log.F("[check] %s(%d) -> http://%s, FAILED. expect: %s, server response: %s", fwdr.Addr(), fwdr.Priority(), c.addr, c.expect, line) + fwdr.Disable() + return false + } + + elapsed := time.Since(startTime) + fwdr.SetLatency(int64(elapsed)) + + if elapsed > c.timeout { + log.F("[check] %s(%d) -> http://%s, FAILED. check timeout: %s", fwdr.Addr(), fwdr.Priority(), c.addr, elapsed) + fwdr.Disable() + return false + } + + log.F("[check] %s(%d) -> http://%s, SUCCESS. elapsed: %s", fwdr.Addr(), fwdr.Priority(), c.addr, elapsed) + fwdr.Enable() + + return true +} diff --git a/rule/config.go b/rule/config.go index 015566c..ded066a 100644 --- a/rule/config.go +++ b/rule/config.go @@ -12,8 +12,8 @@ import ( type Config struct { Name string - Forward []string - StrategyConfig StrategyConfig + Forward []string + Strategy Strategy DNSServers []string IPSet string @@ -23,11 +23,10 @@ type Config struct { CIDR []string } -// StrategyConfig is config of strategy. -type StrategyConfig struct { +// Strategy configurations. +type Strategy struct { Strategy string - CheckType string - CheckAddr string + Check string CheckInterval int CheckTimeout int CheckTolerance int @@ -44,17 +43,16 @@ func NewConfFromFile(ruleFile string) (*Config, error) { f := conflag.NewFromFile("rule", ruleFile) f.StringSliceUniqVar(&p.Forward, "forward", nil, "forward url, format: SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS[,SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS]") - f.StringVar(&p.StrategyConfig.Strategy, "strategy", "rr", "forward strategy, default: rr") - f.StringVar(&p.StrategyConfig.CheckType, "checktype", "http", "fowarder check type, http/tcp") - f.StringVar(&p.StrategyConfig.CheckAddr, "checkaddr", "www.apple.com:80", "fowarder check addr, format: HOST[:PORT], default port: 80,") - f.IntVar(&p.StrategyConfig.CheckInterval, "checkinterval", 30, "fowarder check interval(seconds)") - f.IntVar(&p.StrategyConfig.CheckTimeout, "checktimeout", 10, "fowarder check timeout(seconds)") - f.IntVar(&p.StrategyConfig.CheckTolerance, "checktolerance", 0, "fowarder check tolerance(ms), switch only when new_latency < old_latency - tolerance, only used in lha mode") - f.BoolVar(&p.StrategyConfig.CheckDisabledOnly, "checkdisabledonly", false, "check disabled fowarders only") - f.IntVar(&p.StrategyConfig.MaxFailures, "maxfailures", 3, "max failures to change forwarder status to disabled") - f.IntVar(&p.StrategyConfig.DialTimeout, "dialtimeout", 3, "dial timeout(seconds)") - f.IntVar(&p.StrategyConfig.RelayTimeout, "relaytimeout", 0, "relay timeout(seconds)") - f.StringVar(&p.StrategyConfig.IntFace, "interface", "", "source ip or source interface") + f.StringVar(&p.Strategy.Strategy, "strategy", "rr", "forward strategy, default: rr") + f.StringVar(&p.Strategy.Check, "check", "http://www.msftconnecttest.com/connecttest.txt#expect=200", "check=disable: disable health check\ncheck=tcp[://HOST:PORT]: tcp port connect check\ncheck=http://HOST[:PORT][/URI][#expect=STRING_IN_RESP_LINE]") + f.IntVar(&p.Strategy.CheckInterval, "checkinterval", 30, "fowarder check interval(seconds)") + f.IntVar(&p.Strategy.CheckTimeout, "checktimeout", 10, "fowarder check timeout(seconds)") + f.IntVar(&p.Strategy.CheckTolerance, "checktolerance", 0, "fowarder check tolerance(ms), switch only when new_latency < old_latency - tolerance, only used in lha mode") + f.BoolVar(&p.Strategy.CheckDisabledOnly, "checkdisabledonly", false, "check disabled fowarders only") + f.IntVar(&p.Strategy.MaxFailures, "maxfailures", 3, "max failures to change forwarder status to disabled") + f.IntVar(&p.Strategy.DialTimeout, "dialtimeout", 3, "dial timeout(seconds)") + f.IntVar(&p.Strategy.RelayTimeout, "relaytimeout", 0, "relay timeout(seconds)") + f.StringVar(&p.Strategy.IntFace, "interface", "", "source ip or source interface") f.StringSliceUniqVar(&p.DNSServers, "dnsserver", nil, "remote dns server") f.StringVar(&p.IPSet, "ipset", "", "ipset name") diff --git a/rule/group.go b/rule/group.go index 25db39c..b72163c 100644 --- a/rule/group.go +++ b/rule/group.go @@ -1,10 +1,9 @@ package rule import ( - "bytes" "hash/fnv" - "io" "net" + "net/url" "sort" "strings" "sync" @@ -24,7 +23,7 @@ func (p priSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } // FwdrGroup is a forwarder group. type FwdrGroup struct { - config *StrategyConfig + config *Strategy fwdrs priSlice avail []*Forwarder // available forwarders mu sync.RWMutex @@ -34,7 +33,7 @@ type FwdrGroup struct { } // NewFwdrGroup returns a new forward group. -func NewFwdrGroup(name string, s []string, c *StrategyConfig) *FwdrGroup { +func NewFwdrGroup(name string, s []string, c *Strategy) *FwdrGroup { var fwdrs []*Forwarder for _, chain := range s { fwdr, err := ForwarderFromURL(chain, c.IntFace, @@ -57,16 +56,12 @@ func NewFwdrGroup(name string, s []string, c *StrategyConfig) *FwdrGroup { } // newFwdrGroup returns a new FwdrGroup. -func newFwdrGroup(name string, fwdrs []*Forwarder, c *StrategyConfig) *FwdrGroup { +func newFwdrGroup(name string, fwdrs []*Forwarder, c *Strategy) *FwdrGroup { p := &FwdrGroup{fwdrs: fwdrs, config: c} sort.Sort(p.fwdrs) p.init() - if strings.IndexByte(p.config.CheckAddr, ':') == -1 { - p.config.CheckAddr += ":80" - } - // default scheduler p.next = p.scheduleRR @@ -178,24 +173,55 @@ func (p *FwdrGroup) onStatusChanged(fwdr *Forwarder) { } } -// Check implements the Checker interface. +// Check runs the forwarder checks. func (p *FwdrGroup) Check() { - if p.config.CheckType != "http" && p.config.CheckType != "tcp" { + if len(p.fwdrs) == 1 { p.config.MaxFailures = 0 + log.F("[group] only 1 forwarder found, disable health checking") return } - // no need to check when there's only 1 forwarder - if len(p.fwdrs) > 1 { - for i := 0; i < len(p.fwdrs); i++ { - go p.check(p.fwdrs[i], p.config.CheckType == "http") + if !strings.Contains(p.config.Check, "://") { + p.config.Check += "://" + } + + u, err := url.Parse(p.config.Check) + if err != nil { + log.F("[group] parse check config error: %s", err) + return + } + + addr := u.Host + if strings.IndexByte(addr, ':') == -1 { + addr += ":80" + } + + timeout := time.Duration(p.config.CheckTimeout) * time.Second + + var checker Checker + switch u.Scheme { + case "tcp": + checker = newTcpChecker(addr, timeout) + case "http": + expect := "HTTP" // default: check the first 4 chars in response + params, _ := url.ParseQuery(u.Fragment) + if ex := params.Get("expect"); ex != "" { + expect = ex } + checker = newHttpChecker(addr, u.RequestURI(), expect, timeout) + default: + p.config.MaxFailures = 0 + log.F("[group] invalid check config `%s`, disable health checking", p.config.Check) + return + } + + for i := 0; i < len(p.fwdrs); i++ { + go p.check(p.fwdrs[i], checker) } } -func (p *FwdrGroup) check(f *Forwarder, http bool) { +func (p *FwdrGroup) check(f *Forwarder, checker Checker) { wait := uint8(0) - buf := make([]byte, 4) intval := time.Duration(p.config.CheckInterval) * time.Second for { @@ -210,16 +236,9 @@ func (p *FwdrGroup) check(f *Forwarder, http bool) { continue } - if http { - if checkHttp(f, p.config.CheckAddr, time.Duration(p.config.CheckTimeout)*time.Second, buf) { - wait = 1 - continue - } - } else { - if checkTcp(f, p.config.CheckAddr, time.Duration(p.config.CheckTimeout)*time.Second) { - wait = 1 - continue - } + if checker.Check(f) { + wait = 1 + continue } if wait == 0 { @@ -233,86 +252,6 @@ func (p *FwdrGroup) check(f *Forwarder, http bool) { } } -func checkTcp(fwdr *Forwarder, addr string, timeout time.Duration) bool { - startTime := time.Now() - - rc, err := fwdr.Dial("tcp", addr) - if err != nil { - log.F("[check] tcp://%s(%d), FAILED. error in dial: %s", fwdr.Addr(), fwdr.Priority(), err) - fwdr.Disable() - return false - } - defer rc.Close() - - if timeout > 0 { - rc.SetDeadline(time.Now().Add(timeout)) - } - - elapsed := time.Since(startTime) - fwdr.SetLatency(int64(elapsed)) - - if elapsed > timeout { - log.F("[check] tcp://%s(%d), FAILED. check timeout: %s", fwdr.Addr(), fwdr.Priority(), elapsed) - fwdr.Disable() - return false - } - - log.F("[check] tcp://%s(%d), SUCCESS. elapsed: %s", fwdr.Addr(), fwdr.Priority(), elapsed) - fwdr.Enable() - - return true -} - -func checkHttp(fwdr *Forwarder, addr string, timeout time.Duration, buf []byte) bool { - startTime := time.Now() - - rc, err := fwdr.Dial("tcp", addr) - if err != nil { - log.F("[check] %s(%d) -> http://%s, FAILED. error in dial: %s", fwdr.Addr(), fwdr.Priority(), addr, err) - fwdr.Disable() - return false - } - defer rc.Close() - - if timeout > 0 { - rc.SetDeadline(time.Now().Add(timeout)) - } - - _, err = io.WriteString(rc, "GET / HTTP/1.1\r\nHost:"+addr+"\r\nConnection: close"+"\r\n\r\n") - if err != nil { - log.F("[check] %s(%d) -> http://%s, FAILED. error in write: %s", fwdr.Addr(), fwdr.Priority(), addr, err) - fwdr.Disable() - return false - } - - _, err = io.ReadFull(rc, buf) - if err != nil { - log.F("[check] %s(%d) -> http://%s, FAILED. error in read: %s", fwdr.Addr(), fwdr.Priority(), addr, err) - fwdr.Disable() - return false - } - - if !bytes.Equal([]byte("HTTP"), buf) { - log.F("[check] %s(%d) -> http://%s, FAILED. server response: %s", fwdr.Addr(), fwdr.Priority(), addr, buf) - fwdr.Disable() - return false - } - - elapsed := time.Since(startTime) - fwdr.SetLatency(int64(elapsed)) - - if elapsed > timeout { - log.F("[check] %s(%d) -> http://%s, FAILED. check timeout: %s", fwdr.Addr(), fwdr.Priority(), addr, elapsed) - fwdr.Disable() - return false - } - - log.F("[check] %s(%d) -> http://%s, SUCCESS. elapsed: %s", fwdr.Addr(), fwdr.Priority(), addr, elapsed) - fwdr.Enable() - - return true -} - // Round Robin. func (p *FwdrGroup) scheduleRR(dstAddr string) *Forwarder { return p.avail[atomic.AddUint32(&p.index, 1)%uint32(len(p.avail))] diff --git a/rule/proxy.go b/rule/proxy.go index a70e19b..50699a1 100644 --- a/rule/proxy.go +++ b/rule/proxy.go @@ -19,11 +19,11 @@ type Proxy struct { } // NewProxy returns a new rule proxy. -func NewProxy(mainForwarders []string, mainStrategy *StrategyConfig, rules []*Config) *Proxy { +func NewProxy(mainForwarders []string, mainStrategy *Strategy, rules []*Config) *Proxy { rd := &Proxy{main: NewFwdrGroup("main", mainForwarders, mainStrategy)} for _, r := range rules { - group := NewFwdrGroup(r.Name, r.Forward, &r.StrategyConfig) + group := NewFwdrGroup(r.Name, r.Forward, &r.Strategy) rd.all = append(rd.all, group) for _, domain := range r.Domain {