From 71c7cd2823e14f6a775eba0986ab33da22bee91d Mon Sep 17 00:00:00 2001 From: nadoo <287492+nadoo@users.noreply.github.com> Date: Wed, 22 Dec 2021 21:20:29 +0800 Subject: [PATCH] proxy: support https health checking --- README.md | 3 ++- config.go | 2 +- config/glider.conf.example | 1 + main.go | 2 +- rule/check.go | 45 ++++++++++++++++++++++++++++++-------- rule/config.go | 4 ++-- rule/group.go | 38 ++++++++++++++++---------------- rule/proxy.go | 2 +- 8 files changed, 63 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index a6c3c36..2a8c7d2 100644 --- a/README.md +++ b/README.md @@ -100,10 +100,11 @@ glider -h click to see details ```bash -glider 0.15.1 usage: +glider 0.15.2 usage: -check string check=tcp[://HOST:PORT]: tcp port connect check check=http://HOST[:PORT][/URI][#expect=STRING_IN_RESP_LINE] + check=https://HOST[:PORT][/URI][#expect=STRING_IN_RESP_LINE] check=file://SCRIPT_PATH: run a check script, healthy when exitcode=0, environment variables: FORWARDER_ADDR check=disable: disable health check (default "http://www.msftconnecttest.com/connecttest.txt#expect=200") -checkdisabledonly diff --git a/config.go b/config.go index 0691a31..9be2fd4 100644 --- a/config.go +++ b/config.go @@ -51,7 +51,7 @@ func parseConfig() *Config { flag.StringSliceVar(&conf.Forwards, "forward", nil, "forward url, format: SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS[,SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS]") 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=tcp[://HOST:PORT]: tcp port connect check\ncheck=http://HOST[:PORT][/URI][#expect=STRING_IN_RESP_LINE]\ncheck=file://SCRIPT_PATH: run a check script, healthy when exitcode=0, environment variables: FORWARDER_ADDR\ncheck=disable: disable health check") + flag.StringVar(&conf.Strategy.Check, "check", "http://www.msftconnecttest.com/connecttest.txt#expect=200", "check=tcp[://HOST:PORT]: tcp port connect check\ncheck=http://HOST[:PORT][/URI][#expect=STRING_IN_RESP_LINE]\ncheck=https://HOST[:PORT][/URI][#expect=STRING_IN_RESP_LINE]\ncheck=file://SCRIPT_PATH: run a check script, healthy when exitcode=0, environment variables: FORWARDER_ADDR\ncheck=disable: disable health check") 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") diff --git a/config/glider.conf.example b/config/glider.conf.example index 2549c77..557fefb 100644 --- a/config/glider.conf.example +++ b/config/glider.conf.example @@ -193,6 +193,7 @@ maxfailures=3 # Forwarder health check: # check=tcp[://HOST:PORT]: tcp port connect check # check=http://HOST[:PORT][/URI][#expect=STRING_IN_RESP_LINE] +# check=https://HOST[:PORT][/URI][#expect=STRING_IN_RESP_LINE] # check=file://SCRIPT_PATH: run a check script, healthy when exitcode=0, environment variables: FORWARDER_ADDR # check=disable: disable health check check=http://www.msftconnecttest.com/connecttest.txt#expect=200 diff --git a/main.go b/main.go index 5c88e11..e25693d 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ import ( ) var ( - version = "0.15.1" + version = "0.15.2" config = parseConfig() ) diff --git a/rule/check.go b/rule/check.go index c4ba392..37b4a05 100644 --- a/rule/check.go +++ b/rule/check.go @@ -1,9 +1,11 @@ package rule import ( + "crypto/tls" "errors" "fmt" "io" + "net" "os" "os/exec" "strings" @@ -24,6 +26,9 @@ type tcpChecker struct { } func newTcpChecker(addr string, timeout time.Duration) *tcpChecker { + if _, port, _ := net.SplitHostPort(addr); port == "" { + addr = net.JoinHostPort(addr, "80") + } return &tcpChecker{addr, timeout} } @@ -34,20 +39,33 @@ func (c *tcpChecker) Check(dialer proxy.Dialer) (time.Duration, error) { if err != nil { return 0, err } - defer rc.Close() - + rc.Close() return time.Since(startTime), nil } type httpChecker struct { - addr string - uri string - expect string - timeout time.Duration + addr string + uri string + expect string + timeout time.Duration + tlsConfig *tls.Config + serverName string } -func newHttpChecker(addr, uri, expect string, timeout time.Duration) *httpChecker { - return &httpChecker{addr, uri, expect, timeout} +func newHttpChecker(addr, uri, expect string, timeout time.Duration, withTLS bool) *httpChecker { + c := &httpChecker{addr: addr, uri: uri, expect: expect, timeout: timeout} + if _, p, _ := net.SplitHostPort(addr); p == "" { + if withTLS { + c.addr = net.JoinHostPort(addr, "443") + } else { + c.addr = net.JoinHostPort(addr, "80") + } + } + c.serverName = c.addr[:strings.LastIndex(c.addr, ":")] + if withTLS { + c.tlsConfig = &tls.Config{ServerName: c.serverName} + } + return c } // Check implements the Checker interface. @@ -57,6 +75,15 @@ func (c *httpChecker) Check(dialer proxy.Dialer) (time.Duration, error) { if err != nil { return 0, err } + + if c.tlsConfig != nil { + tlsConn := tls.Client(rc, c.tlsConfig) + if err := tlsConn.Handshake(); err != nil { + tlsConn.Close() + return 0, err + } + rc = tlsConn + } defer rc.Close() if c.timeout > 0 { @@ -64,7 +91,7 @@ func (c *httpChecker) Check(dialer proxy.Dialer) (time.Duration, error) { } if _, err = io.WriteString(rc, - "GET "+c.uri+" HTTP/1.1\r\nHost:"+c.addr+"\r\nConnection: close"+"\r\n\r\n"); err != nil { + "GET "+c.uri+" HTTP/1.1\r\nHost:"+c.serverName+"\r\n\r\n"); err != nil { return 0, err } diff --git a/rule/config.go b/rule/config.go index ff67cc3..bc7c1c4 100644 --- a/rule/config.go +++ b/rule/config.go @@ -9,7 +9,7 @@ import ( // Config is config of rule. type Config struct { - Name string + RulePath string Forward []string Strategy Strategy @@ -38,7 +38,7 @@ type Strategy struct { // NewConfFromFile returns a new config from file. func NewConfFromFile(ruleFile string) (*Config, error) { - p := &Config{Name: ruleFile} + p := &Config{RulePath: ruleFile} 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]") diff --git a/rule/group.go b/rule/group.go index ad1cb83..d6e9f7b 100644 --- a/rule/group.go +++ b/rule/group.go @@ -5,6 +5,7 @@ import ( "hash/fnv" "net" "net/url" + "path/filepath" "sort" "strings" "sync" @@ -24,6 +25,7 @@ func (p priSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } // FwdrGroup is a forwarder group. type FwdrGroup struct { + name string config *Strategy fwdrs priSlice avail []*Forwarder // available forwarders @@ -34,7 +36,7 @@ type FwdrGroup struct { } // NewFwdrGroup returns a new forward group. -func NewFwdrGroup(name string, s []string, c *Strategy) *FwdrGroup { +func NewFwdrGroup(rulePath string, s []string, c *Strategy) *FwdrGroup { var fwdrs []*Forwarder for _, chain := range s { fwdr, err := ForwarderFromURL(chain, c.IntFace, @@ -57,12 +59,13 @@ func NewFwdrGroup(name string, s []string, c *Strategy) *FwdrGroup { c.Strategy = "rr" } + name := strings.TrimSuffix(filepath.Base(rulePath), filepath.Ext(rulePath)) return newFwdrGroup(name, fwdrs, c) } // newFwdrGroup returns a new FwdrGroup. func newFwdrGroup(name string, fwdrs []*Forwarder, c *Strategy) *FwdrGroup { - p := &FwdrGroup{fwdrs: fwdrs, config: c} + p := &FwdrGroup{name: name, fwdrs: fwdrs, config: c} sort.Sort(p.fwdrs) p.init() @@ -164,8 +167,8 @@ func (p *FwdrGroup) onStatusChanged(fwdr *Forwarder) { } else if fwdr.Priority() > p.Priority() { p.init() } - log.F("[group] %s(%d) changed status from DISABLED to ENABLED (%d of %d currently enabled)", - fwdr.Addr(), fwdr.Priority(), len(p.avail), len(p.fwdrs)) + log.F("[group] %s: %s(%d) changed status from DISABLED to ENABLED (%d of %d currently enabled)", + p.name, fwdr.Addr(), fwdr.Priority(), len(p.avail), len(p.fwdrs)) } else { for i, f := range p.avail { if f == fwdr { @@ -173,8 +176,8 @@ func (p *FwdrGroup) onStatusChanged(fwdr *Forwarder) { break } } - log.F("[group] %s(%d) changed status from ENABLED to DISABLED (%d of %d currently enabled)", - fwdr.Addr(), fwdr.Priority(), len(p.avail), len(p.fwdrs)) + log.F("[group] %s: %s(%d) changed status from ENABLED to DISABLED (%d of %d currently enabled)", + p.name, fwdr.Addr(), fwdr.Priority(), len(p.avail), len(p.fwdrs)) } if len(p.avail) == 0 { @@ -185,7 +188,7 @@ func (p *FwdrGroup) onStatusChanged(fwdr *Forwarder) { // Check runs the forwarder checks. func (p *FwdrGroup) Check() { if len(p.fwdrs) == 1 { - log.F("[group] only 1 forwarder found, disable health checking") + log.F("[group] %s: only 1 forwarder found, disable health checking", p.name) return } @@ -195,36 +198,32 @@ func (p *FwdrGroup) Check() { u, err := url.Parse(p.config.Check) if err != nil { - log.F("[group] parse check config error: %s, disable health checking", err) + log.F("[group] %s: parse check config error: %s, disable health checking", p.name, err) return } addr := u.Host - if _, port, _ := net.SplitHostPort(addr); port == "" { - addr = net.JoinHostPort(addr, "80") - } - timeout := time.Duration(p.config.CheckTimeout) * time.Second var checker Checker switch u.Scheme { case "tcp": checker = newTcpChecker(addr, timeout) - case "http": + case "http", "https": 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) + checker = newHttpChecker(addr, u.RequestURI(), expect, timeout, u.Scheme == "https") case "file": checker = newFileChecker(u.Host + u.Path) default: - log.F("[group] check config `%s`, disable health checking", p.config.Check) + log.F("[group] %s: unknown scheme in check config `%s`, disable health checking", p.name, p.config.Check) return } - log.F("[group] using check config: %s", p.config.Check) + log.F("[group] %s: using check config: %s", p.name, p.config.Check) for i := 0; i < len(p.fwdrs); i++ { go p.check(p.fwdrs[i], checker) @@ -251,7 +250,7 @@ func (p *FwdrGroup) check(fwdr *Forwarder, checker Checker) { if err != nil { if errors.Is(err, proxy.ErrNotSupported) { fwdr.SetMaxFailures(0) - log.F("[check] %s(%d), %s, stop checking", fwdr.Addr(), fwdr.Priority(), err) + log.F("[check] %s: %s(%d), %s, stop checking", p.name, fwdr.Addr(), fwdr.Priority(), err) fwdr.Enable() break } @@ -261,14 +260,15 @@ func (p *FwdrGroup) check(fwdr *Forwarder, checker Checker) { wait = 16 } - log.F("[check] %s(%d), FAILED. error: %s", fwdr.Addr(), fwdr.Priority(), err) + log.F("[check] %s: %s(%d), FAILED. error: %s", p.name, fwdr.Addr(), fwdr.Priority(), err) fwdr.Disable() continue } wait = 1 fwdr.SetLatency(int64(elapsed)) - log.F("[check] %s(%d), SUCCESS. elapsed: %s", fwdr.Addr(), fwdr.Priority(), elapsed) + log.F("[check] %s: %s(%d), SUCCESS. elapsed: %s", + p.name, fwdr.Addr(), fwdr.Priority(), elapsed) fwdr.Enable() } } diff --git a/rule/proxy.go b/rule/proxy.go index 586de45..6e6b100 100644 --- a/rule/proxy.go +++ b/rule/proxy.go @@ -22,7 +22,7 @@ func NewProxy(mainForwarders []string, mainStrategy *Strategy, rules []*Config) rd := &Proxy{main: NewFwdrGroup("main", mainForwarders, mainStrategy)} for _, r := range rules { - group := NewFwdrGroup(r.Name, r.Forward, &r.Strategy) + group := NewFwdrGroup(r.RulePath, r.Forward, &r.Strategy) rd.all = append(rd.all, group) for _, domain := range r.Domain {