diff --git a/bfe_balance/backend/health_check.go b/bfe_balance/backend/health_check.go index c13b88863..68310e6da 100644 --- a/bfe_balance/backend/health_check.go +++ b/bfe_balance/backend/health_check.go @@ -17,25 +17,36 @@ package backend import ( + "crypto/x509" + "encoding/json" "fmt" "net" "net/http" + "net/url" + "regexp" + "strconv" "strings" "time" -) -import ( "github.com/baidu/go-lib/log" -) -import ( "github.com/bfenetworks/bfe/bfe_config/bfe_cluster_conf/cluster_conf" "github.com/bfenetworks/bfe/bfe_debug" + "github.com/bfenetworks/bfe/bfe_tls" ) +type checkRtn struct { + ok bool + err error +} + func UpdateStatus(backend *BfeBackend, cluster string) bool { + var ( + checkConf *cluster_conf.BackendCheck + httpsConf *cluster_conf.BackendHTTPS + ) // get conf of health check, which is separately stored for each cluster - checkConf := getCheckConf(cluster) + checkConf, httpsConf = getCheckConf(cluster) if checkConf == nil { // just ignore if not found health check conf return false @@ -45,14 +56,15 @@ func UpdateStatus(backend *BfeBackend, cluster string) bool { // if backend's status become fail, start healthcheck. // at most start 1 check goroutine for each backend. if backend.UpdateStatus(*checkConf.FailNum) { - go check(backend, cluster) + go check(backend, cluster, httpsConf) return true } return false } -func check(backend *BfeBackend, cluster string) { +func check(backend *BfeBackend, cluster string, httpsConf *cluster_conf.BackendHTTPS) { + log.Logger.Info("start healthcheck for %s", backend.Name) // backend close chan @@ -67,7 +79,7 @@ loop: } // get the latest conf to do health check - checkConf := getCheckConf(cluster) + checkConf, _ := getCheckConf(cluster) if checkConf == nil { // never come here time.Sleep(time.Second) @@ -76,7 +88,7 @@ loop: checkInterval := time.Duration(*checkConf.CheckInterval) * time.Millisecond // health check - if ok, err := CheckConnect(backend, checkConf); !ok { + if ok, err := CheckConnect(backend, checkConf, httpsConf); !ok { backend.ResetSuccNum() if bfe_debug.DebugHealthCheck { log.Logger.Debug("backend %s still not avail (check failure: %s)", backend.Name, err) @@ -150,6 +162,237 @@ func doHTTPHealthCheck(request *http.Request, timeout time.Duration) (int, error return response.StatusCode, nil } +// extractIP extract ip address +func extractIP(rsAddr string) string { + if strings.HasPrefix(rsAddr, "[") { + // IPv6 + endIndex := strings.LastIndex(rsAddr, "]") + if endIndex == -1 { + return "" + } + ip := rsAddr[:endIndex+1] + if net.ParseIP(ip[1:endIndex]) == nil { + return "" + } + return ip + } else { + // IPv4 + ip := strings.Split(rsAddr, ":")[0] + if net.ParseIP(ip) == nil { + return "" + } + return ip + } +} + +func getHostByType(host, rsAddr, hostType *string, def string) string { + if hostType == nil { + ht := cluster_conf.HostType_HOST + hostType = &ht + } + switch *hostType { + case cluster_conf.HostType_Instance_IP: + if rsAddr != nil { + return extractIP(*rsAddr) + } + default: + if host != nil { + return *host + } + } + return def +} + +func checkHTTPSConnect(backend *BfeBackend, checkConf *cluster_conf.BackendCheck, httpsConf *cluster_conf.BackendHTTPS) (bool, error) { + var ( + err error + conn net.Conn + addrInfo = getHealthCheckAddrInfo(backend, checkConf) + checkTimeout = 30 * time.Second + statusCode = 0 + host string + rootCAs *x509.CertPool = nil + certs []bfe_tls.Certificate = nil + cert bfe_tls.Certificate + insecure = false + uri = "/" + checkRtnCh = make(chan checkRtn, 1) + rtn checkRtn + ) + + var ( + getStatusCodeFn = func(statusLine string) (int, error) { + // "HTTP/1.1 200 OK" + re, err := regexp.Compile(`\s(\d{3})\s`) + if err != nil { + return 0, err + } + matches := re.FindStringSubmatch(statusLine) + if len(matches) == 2 { + statusCode := matches[1] + log.Logger.Debug("StatusCode = %s, raw = %s", statusCode, statusLine) + return strconv.Atoi(statusCode) + } else { + return 0, fmt.Errorf("Status code not found: %s", statusLine) + } + } + + doCheckFn = func(conn net.Conn) checkRtn { + // Set timeout + timeout := 3 * time.Second + err = conn.SetDeadline(time.Now().Add(timeout)) + if err != nil { + return checkRtn{false, err} + } + + // TLS Check + if err = conn.(*bfe_tls.Conn).Handshake(); err != nil { + log.Logger.Debug("debug_https err=%s", err.Error()) + return checkRtn{false, err} + } + if *checkConf.Schem == "tls" { + return checkRtn{true, nil} + } + + // HTTPS Check + if checkConf.Uri != nil && *checkConf.Uri != "" { + uri = *checkConf.Uri + } + request := fmt.Sprintf("GET %s HTTP/1.1\r\n"+ + "Host: %s\r\n"+ + "User-Agent: BFE-Health-Check\r\n"+ + "\r\n", uri, host) + _, err = conn.Write([]byte(request)) + if err != nil { + log.Logger.Debug("debug_https err=%s", err.Error()) + return checkRtn{false, err} + } + var ( + response = "" + ok bool + err error + data = make([]byte, 0) + bufSz = 128 + buf = make([]byte, bufSz) + total = 0 + ) + + for { + total, err = conn.Read(buf) + if err != nil { + break + } + data = append(data, buf[:total]...) + if total < bufSz { + break + } + } + + if err != nil { + log.Logger.Debug("debug_https err=%s", err.Error()) + return checkRtn{false, err} + } + response = string(data) + log.Logger.Debug("<- Request:\n%s", request) + log.Logger.Debug("-> Response:\n%s", response) + if checkConf.StatusCode != nil { // check status code + var ( + s string + arr = strings.Split(response, "\n") + ) + if len(arr) > 0 { + s = strings.ToUpper(arr[0]) + statusCode, err = getStatusCodeFn(s) + if err != nil { + return checkRtn{false, err} + } + if checkConf.StatusCodeRange != nil && *checkConf.StatusCodeRange != "" { + log.Logger.Debug("statusCode=%d, statusCodeRange=%s", statusCode, *checkConf.StatusCodeRange) + ok, err := cluster_conf.MatchStatusCodeRange(fmt.Sprintf("%d", statusCode), *checkConf.StatusCodeRange) + return checkRtn{ok, err} + } + } + ok, err = cluster_conf.MatchStatusCode(statusCode, *checkConf.StatusCode) + } + return checkRtn{ok, err} + } + + toStringFn = func(o interface{}) string { + b, err := json.Marshal(o) + if err != nil { + return err.Error() + } + return string(b) + } + ) + + if checkConf.CheckTimeout != nil { + checkTimeout = time.Duration(*checkConf.CheckTimeout) * time.Millisecond + } + conn, err = net.DialTimeout("tcp", addrInfo, checkTimeout) + + if err != nil { + log.Logger.Debug("debug_https err=%v", err) + return false, err + } + + defer func() { + if r := recover(); r != nil { + log.Logger.Debug("recover_panic = %v", r) + } + _ = conn.Close() + }() + + _, err = url.Parse(fmt.Sprintf("%s://%s%s", "https", addrInfo, *checkConf.Uri)) + if err != nil { + log.Logger.Debug("debug_https err=%v", err) + return false, err + } + + serverName := "" + if httpsConf.RSHost != nil { + serverName = *httpsConf.RSHost + } else if checkConf.Host != nil { + serverName = *checkConf.Host + } + host = getHostByType(checkConf.Host, &addrInfo, checkConf.HostType, serverName) + + rootCAs, err = httpsConf.GetRSCAList() + + if cert, err = httpsConf.GetBFECert(); err == nil { + certs = []bfe_tls.Certificate{cert} + } + + if httpsConf.RSInsecureSkipVerify != nil { + insecure = *httpsConf.RSInsecureSkipVerify + } + + conn = bfe_tls.Client(conn, &bfe_tls.Config{ + Certificates: certs, + InsecureSkipVerify: true, + ServerName: host, + RootCAs: rootCAs, + VerifyPeerCertificate: bfe_tls.NewVerifyPeerCertHooks(insecure, host, rootCAs).Ready(), + }) + + log.Logger.Debug("httpsCheck conf=%s", toStringFn(checkConf)) + go func(conn net.Conn, rtnCh chan checkRtn) { + rtnCh <- doCheckFn(conn) + }(conn, checkRtnCh) + + if checkTimeout > 0 { + select { + case rtn = <-checkRtnCh: + return rtn.ok, rtn.err + case <-time.Tick(checkTimeout): + return false, fmt.Errorf("https checkTimeout %dms", checkTimeout/time.Millisecond) + } + } else { + rtn = <-checkRtnCh + } + return rtn.ok, rtn.err +} + func checkHTTPConnect(backend *BfeBackend, checkConf *cluster_conf.BackendCheck) (bool, error) { // prepare health check request addrInfo := getHealthCheckAddrInfo(backend, checkConf) @@ -182,12 +425,14 @@ func checkHTTPConnect(backend *BfeBackend, checkConf *cluster_conf.BackendCheck) } // CheckConnect checks whether backend server become available. -func CheckConnect(backend *BfeBackend, checkConf *cluster_conf.BackendCheck) (bool, error) { +func CheckConnect(backend *BfeBackend, checkConf *cluster_conf.BackendCheck, httpsConf *cluster_conf.BackendHTTPS) (bool, error) { switch *checkConf.Schem { case "http": return checkHTTPConnect(backend, checkConf) case "tcp": return checkTCPConnect(backend, checkConf) + case "https", "tls": + return checkHTTPSConnect(backend, checkConf, httpsConf) default: // never come here return checkHTTPConnect(backend, checkConf) @@ -195,13 +440,13 @@ func CheckConnect(backend *BfeBackend, checkConf *cluster_conf.BackendCheck) (bo } // CheckConfFetcher returns current health check conf for cluster. -type CheckConfFetcher func(cluster string) *cluster_conf.BackendCheck +type CheckConfFetcher func(name string) (*cluster_conf.BackendCheck, *cluster_conf.BackendHTTPS) var checkConfFetcher CheckConfFetcher -func getCheckConf(cluster string) *cluster_conf.BackendCheck { +func getCheckConf(cluster string) (*cluster_conf.BackendCheck, *cluster_conf.BackendHTTPS) { if checkConfFetcher == nil { - return nil + return nil, nil } return checkConfFetcher(cluster) } diff --git a/bfe_balance/backend/health_check_test.go b/bfe_balance/backend/health_check_test.go index 05a832634..174d84762 100644 --- a/bfe_balance/backend/health_check_test.go +++ b/bfe_balance/backend/health_check_test.go @@ -15,15 +15,25 @@ package backend import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/hex" "fmt" + "math/big" + "math/rand" + "net" "net/http" "net/http/httptest" "strings" + "sync" "testing" -) + "time" + + "github.com/baidu/go-lib/log" -import ( "github.com/bfenetworks/bfe/bfe_config/bfe_cluster_conf/cluster_conf" + "github.com/bfenetworks/bfe/bfe_tls" ) // test CheckConnect, AnyStatusCode case @@ -50,7 +60,7 @@ func TestCheckConnect_1(t *testing.T) { } // CheckConnect - isHealthy, err := CheckConnect(&backend, &checkConf) + isHealthy, err := CheckConnect(&backend, &checkConf, nil) if err != nil { t.Errorf("should have no err: %v", err) } @@ -85,7 +95,7 @@ func TestCheckConnect_2(t *testing.T) { } // CheckConnect - isHealthy, err := CheckConnect(&backend, &checkConf) + isHealthy, err := CheckConnect(&backend, &checkConf, nil) if err != nil { t.Errorf("should have no err: %v", err) } @@ -120,7 +130,7 @@ func TestCheckConnect_3(t *testing.T) { } // CheckConnect - _, err := CheckConnect(&backend, &checkConf) + _, err := CheckConnect(&backend, &checkConf, nil) if err == nil { t.Errorf("should have err") } @@ -146,7 +156,7 @@ func TestCheckConnect_4(t *testing.T) { } // CheckConnect - _, err := CheckConnect(&backend, &checkConf) + _, err := CheckConnect(&backend, &checkConf, nil) if err != nil { t.Errorf("should have no err: %v", err) } @@ -176,7 +186,7 @@ func TestCheckConnect_5(t *testing.T) { } // CheckConnect - _, err := CheckConnect(&backend, &checkConf) + _, err := CheckConnect(&backend, &checkConf, nil) if err != nil { t.Errorf("should have no err: %v", err) } @@ -213,7 +223,7 @@ func TestCheckConnect_6(t *testing.T) { } // CheckConnect - isHealthy, err := CheckConnect(&backend, &checkConf) + isHealthy, err := CheckConnect(&backend, &checkConf, nil) if err != nil { t.Errorf("should have no err: %v", err) } @@ -251,7 +261,7 @@ func TestCheckConnect_7(t *testing.T) { } // CheckConnect - isHealthy, err := CheckConnect(&backend, &checkConf) + isHealthy, err := CheckConnect(&backend, &checkConf, nil) if err != nil { t.Errorf("should have no err: %v", err) } @@ -285,7 +295,7 @@ func TestCheckConnect_8(t *testing.T) { } // CheckConnect - _, err := CheckConnect(&backend, &checkConf) + _, err := CheckConnect(&backend, &checkConf, nil) if err != nil { t.Errorf("should have no err: %v", err) } @@ -315,7 +325,7 @@ func TestCheckConnect_9(t *testing.T) { } // CheckConnect - isHealthy, err := CheckConnect(&backend, &checkConf) + isHealthy, err := CheckConnect(&backend, &checkConf, nil) if err != nil { t.Errorf("should have no err: %v", err) } @@ -350,7 +360,7 @@ func TestCheckConnect_10(t *testing.T) { } // CheckConnect - isHealthy, err := CheckConnect(&backend, &checkConf) + isHealthy, err := CheckConnect(&backend, &checkConf, nil) if err != nil { t.Errorf("should have no err: %v", err) } @@ -387,7 +397,7 @@ func TestCheckConnect_11(t *testing.T) { } // CheckConnect - isHealthy, err := CheckConnect(&backend, &checkConf) + isHealthy, err := CheckConnect(&backend, &checkConf, nil) if err != nil { t.Errorf("should have no err: %v", err) } @@ -424,15 +434,343 @@ func TestCheck_1(t *testing.T) { CheckInterval: &checkInterval, } - mockCheckConfFetcher := func(cluster string) *cluster_conf.BackendCheck { - return &checkConf + mockCheckConfFetcher := func(cluster string) (*cluster_conf.BackendCheck, *cluster_conf.BackendHTTPS) { + return &checkConf, nil } checkConfFetcher = mockCheckConfFetcher // check func - check(&backend, "") + check(&backend, "", nil) if backend.SuccNum() != 0 { t.Errorf("recover num should be 0") } } + +// test CheckConnect->checkHTTPSConnect >>>>>>>>>>>>>>>>>>>> +// test CheckConnect->checkHTTPSConnect >>>>>>>>>>>>>>>>>>>> +func TestCheckConnect_checkHTTPSConnect(t *testing.T) { + _ = log.Init(fmt.Sprintf("test_%s", time.Now().String()), "DEBUG", "/tmp", true, "M", 5) + type confArg struct { + Schem string // protocol for health check (HTTP/TCP) + Uri string // uri used in health check + Host string // if check request use special host header + StatusCode int // default value is 200 + FailNum int // unhealthy threshold (consecutive failures of check request) + SuccNum int // healthy threshold (consecutive successes of normal request) + CheckTimeout int // timeout for health check, in ms + CheckInterval int // interval of health check, in ms + StatusCodeRange string // #issue-14 + + //----------------------- + addrInfo string + requireAndVerifyClientCert bool + clientInsecure bool + assertOk, assertNoErr bool + + __id string // uuid for mock cluster name + } + type mockConf struct { + check *cluster_conf.BackendCheck + https *cluster_conf.BackendHTTPS + } + var ( + testCase = map[string]confArg{ + "https_xxx_connect_fail": { + assertOk: false, + assertNoErr: false, + Schem: "https", + StatusCodeRange: "1xx|20x|301|302|4xx", + Uri: "/foo/bar", + SuccNum: 1, + clientInsecure: false, + requireAndVerifyClientCert: false, + addrInfo: "1.1.1.1:1111", + }, + + "https_xxx": { + assertOk: true, + assertNoErr: true, + Schem: "https", + StatusCodeRange: "1xx|20x|301|302|4xx", + Uri: "/foo/bar", + SuccNum: 1, + clientInsecure: false, + requireAndVerifyClientCert: false, + }, + "https_xxx_two_way_authc": { + assertOk: false, + assertNoErr: false, + Schem: "https", + StatusCodeRange: "1xx|301|4xx", + Uri: "/", + SuccNum: 1, + clientInsecure: false, + requireAndVerifyClientCert: true, + }, + "https_xxx_wrong_host": { + assertOk: false, + assertNoErr: false, + Schem: "https", + StatusCodeRange: "1xx|20x|301|302|4xx", + Uri: "/foo/bar", + SuccNum: 1, + clientInsecure: false, + requireAndVerifyClientCert: false, + Host: "www.foobar.org", + }, + "https_404": { + assertOk: true, + assertNoErr: true, + Schem: "https", + StatusCode: 404, + Uri: "/foobar", + SuccNum: 1, + clientInsecure: false, + requireAndVerifyClientCert: false, + }, + + "https_404_two_way_authc": { + assertOk: true, + assertNoErr: true, + Schem: "https", + StatusCode: 404, + Uri: "/foobar", + SuccNum: 1, + clientInsecure: false, + requireAndVerifyClientCert: true, + }, + + "https_200": { + assertOk: true, + assertNoErr: true, + Schem: "https", + StatusCode: 200, + Uri: "/", + SuccNum: 1, + clientInsecure: false, + requireAndVerifyClientCert: false, + }, + "httos_200_two_way_authc": { + assertOk: true, + assertNoErr: true, + Schem: "https", + StatusCode: 200, + Uri: "/", + SuccNum: 1, + clientInsecure: false, + requireAndVerifyClientCert: true, + }, + + "tls_connect_fail": { + assertOk: false, + assertNoErr: false, + Schem: "tls", + clientInsecure: false, + requireAndVerifyClientCert: true, + addrInfo: "1.1.1.1:1111", + }, + + "tls": { + assertOk: true, + assertNoErr: true, + Schem: "tls", + clientInsecure: true, + requireAndVerifyClientCert: false, + }, + + "tls_two_way_authc": { + assertOk: true, + assertNoErr: true, + Schem: "tls", + clientInsecure: false, + requireAndVerifyClientCert: true, + }, + } + ) + + var ( + confMap = make(map[string]mockConf) + confMapLock = new(sync.RWMutex) + serverHost = "foobar.org" + //serverTimeout = 3 * time.Second + cert, _ = tls.X509KeyPair(bfe_tls.I_CN_FOOBAR_ORG_CRT.Bytes(), bfe_tls.I_CN_FOOBAR_ORG_PRV.Bytes()) + handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.String() != "/" { + w.WriteHeader(404) + } else { + w.WriteHeader(200) + w.Header().Set("Content-Type", "text/plain") + w.Header().Set("Host", serverHost) + _, _ = w.Write([]byte("Hello BFE https")) + } + }) + + clusterNameFn = func() string { + rand.Seed(time.Now().UnixNano()) + return hex.EncodeToString(big.NewInt(time.Now().UnixNano() + rand.Int63()).Bytes()) + } + casFn = func() *x509.CertPool { + cas := x509.NewCertPool() + cas.AppendCertsFromPEM(bfe_tls.BFE_R_CA_CRT.Bytes()) + cas.AppendCertsFromPEM(bfe_tls.BFE_I_CA_CRT.Bytes()) + return cas + } + cliCertFn = func() *bfe_tls.Certificate { + ccert, _ := bfe_tls.X509KeyPair(bfe_tls.I_BFE_DEV_CRT.Bytes(), bfe_tls.I_BFE_DEV_PRV.Bytes()) + return &ccert + } + checkConfFn = func(c confArg) (*cluster_conf.BackendCheck, *cluster_conf.BackendHTTPS) { + var ( + cas = casFn() + httpsConf = new(cluster_conf.BackendHTTPS) + checkConf = &cluster_conf.BackendCheck{ + Schem: &c.Schem, + StatusCode: &c.StatusCode, + Uri: &c.Uri, + SuccNum: &c.SuccNum, + StatusCodeRange: &c.StatusCodeRange, + CheckTimeout: &c.CheckTimeout, + Host: &c.Host, + } + // for httpsConf >>>> + rscalist = []string{} + insecure = c.clientInsecure + ) + if c.requireAndVerifyClientCert { + // client >>>> + httpsConf.SetBFECert(cliCertFn()) + insecure = false + // client <<<< + } + httpsConf.SetRSCAList(cas) + httpsConf.RSHost = &c.Host + httpsConf.RSCAList = &rscalist + httpsConf.RSInsecureSkipVerify = &insecure + confMapLock.Lock() + confMap[c.__id] = mockConf{checkConf, httpsConf} + confMapLock.Unlock() + return checkConf, httpsConf + } + + buildConfAndStartServerFn = func(ctx context.Context, c confArg) *BfeBackend { + var ( + cas = casFn() + backend = &BfeBackend{} + // 创建TLS配置 + tlsConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientAuth: tls.NoClientCert, // default + } + ) + // Two-way authc + if c.requireAndVerifyClientCert { + // server + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + tlsConfig.ClientCAs = cas + tlsConfig.RootCAs = cas + } + + listener, err := net.Listen("tcp", ":0") + if err != nil { + t.Errorf("failed to listen: %v", err) + } + + server := &http.Server{ + TLSConfig: tlsConfig, + Handler: handler, + } + go func() { + if err := server.ServeTLS(listener, "", ""); err != nil && err != http.ErrServerClosed { + t.Errorf("HTTPS test server fail : %v", err) + } + }() + time.Sleep(100 * time.Millisecond) + port := listener.Addr().(*net.TCPAddr).Port + backend.AddrInfo = fmt.Sprintf("127.0.0.1:%d", port) + if c.addrInfo != "" { + backend.AddrInfo = c.addrInfo + } + // 等待ctx取消信号 + go func() { + <-ctx.Done() + if err := server.Close(); err != nil { + t.Errorf("HTTPS test server Close failed: %v", err) + } + }() + time.Sleep(100 * time.Millisecond) + return backend + } + + startlinkAndWaitRtnFn = func(args confArg) func(t *testing.T) { + args.__id = clusterNameFn() + if args.Host == "" { + args.Host = serverHost + } + if args.CheckTimeout <= 0 { + args.CheckTimeout = 2000 // ms + } + log.Logger.Debug("cid=%s, test-host=%s", args.__id, args.Host) + return func(t *testing.T) { + defer func() { + }() + var rtnCh = make(chan checkRtn, 1) + conf, httpsConf := checkConfFn(args) + ctx, cancelFn := context.WithCancel(context.Background()) + go func(ctx context.Context, conf *cluster_conf.BackendCheck, httpsConf *cluster_conf.BackendHTTPS) { + backend := buildConfAndStartServerFn(ctx, args) + + isHealthy, err := CheckConnect(backend, conf, httpsConf) + rtnCh <- checkRtn{ok: isHealthy, err: err} + }(ctx, conf, httpsConf) + select { + case rtn := <-rtnCh: + defer cancelFn() + if rtn.err != nil { + if args.assertNoErr { + t.Errorf("assertNoErr=%v, err=%v", args.assertNoErr, rtn.err) + } else { + t.Logf("assertNoErr=%v, err=%v", args.assertNoErr, rtn.err) + } + } + if !rtn.ok { + if args.assertOk { + t.Errorf("backend assertOk=%v, rtnOk=%v", args.assertOk, rtn.ok) + } else { + t.Logf("backend assertOk=%v, rtnOk=%v", args.assertOk, rtn.ok) + } + } + } + } + } + ) + + SetCheckConfFetcher(func(name string) (*cluster_conf.BackendCheck, *cluster_conf.BackendHTTPS) { + confMapLock.Lock() + defer confMapLock.Unlock() + cnf := confMap[name] + return cnf.check, cnf.https + }) + for k, v := range testCase { + t.Run(k, startlinkAndWaitRtnFn(v)) + } + time.Sleep(1 * time.Second) +} + +// test CheckConnect->checkHTTPSConnect <<<<<<<<<<<<<<<<<<<< +// test CheckConnect->checkHTTPSConnect <<<<<<<<<<<<<<<<<<<< + +func TestExtractIP(t *testing.T) { + rsAddr1 := "192.168.1.1:8888" + rsAddr2 := "[fe80::d450:2dc5:d]:8888" + + ip1 := extractIP(rsAddr1) + if ip1 != "192.168.1.1" { + t.Error("expect: 192.168.1.1, got :", ip1) + } + + ip2 := extractIP(rsAddr2) + if ip2 != "[fe80::d450:2dc5:d]" { + t.Error("expect: [fe80::d450:2dc5:d], got :", ip2) + } +} diff --git a/bfe_config/bfe_cluster_conf/cluster_conf/cluster_conf_load.go b/bfe_config/bfe_cluster_conf/cluster_conf/cluster_conf_load.go index f7c727cc6..11ae55194 100644 --- a/bfe_config/bfe_cluster_conf/cluster_conf/cluster_conf_load.go +++ b/bfe_config/bfe_cluster_conf/cluster_conf/cluster_conf_load.go @@ -17,13 +17,17 @@ package cluster_conf import ( + "crypto/x509" + "encoding/pem" "errors" "fmt" "os" + "regexp" "strings" -) -import ( + "github.com/baidu/go-lib/log" + + "github.com/bfenetworks/bfe/bfe_tls" "github.com/bfenetworks/bfe/bfe_util/json" ) @@ -42,8 +46,8 @@ const ( // HashStrategy for subcluster-level load balance (GSLB). // Note: -// - CLIENTID is a special request header which represents a unique client, -// eg. baidu id, passport id, device id etc. +// - CLIENTID is a special request header which represents a unique client, +// eg. baidu id, passport id, device id etc. const ( ClientIdOnly = iota // use CLIENTID to hash ClientIpOnly // use CLIENTIP to hash @@ -63,16 +67,27 @@ const ( AnyStatusCode = 0 ) +const ( + HostType_HOST = "HOST" + HostType_Instance_IP = "Instance_IP" +) + // BackendCheck is conf of backend check type BackendCheck struct { - Schem *string // protocol for health check (HTTP/TCP) - Uri *string // uri used in health check - Host *string // if check request use special host header - StatusCode *int // default value is 200 - FailNum *int // unhealthy threshold (consecutive failures of check request) - SuccNum *int // healthy threshold (consecutive successes of normal request) - CheckTimeout *int // timeout for health check, in ms - CheckInterval *int // interval of health check, in ms + Schem *string // protocol for health check (HTTP/HTTPS/TCP) + Uri *string // uri used in health check + Host *string // if check request use special host header + HostType *string // extending the type of Host. + StatusCode *int // default value is 200 + // StatusCodeRange Legal configuration items: + // (1) One of "3xx", "4xx", "5xx" + // (2) Specific HTTP status codes + // (3) Combinations of the above (1) or (2) connected by the "|" symbol, for example: "3xx", "4xx", "5xx", "503" | "4xx", "501" | "409" + StatusCodeRange *string + FailNum *int // unhealthy threshold (consecutive failures of check request) + SuccNum *int // healthy threshold (consecutive successes of normal request) + CheckTimeout *int // timeout for health check, in ms + CheckInterval *int // interval of health check, in ms } // FCGIConf are FastCGI related configurations @@ -95,6 +110,75 @@ type BackendBasic struct { FCGIConf *FCGIConf } +// BackendHTTPS is conf of backend https +type BackendHTTPS struct { + // confs + RSHost *string // real server hostname + RSInsecureSkipVerify *bool // whether to skip verify cert of real server + RSCAList *[]string // real server CA Cert list. nil/empty - use system ca list; not empty - completely use RSCAList and do not use system list + BFECertFile *string // BFE Cert file + BFEKeyFile *string // Privatekey file of BFECert + + // caches + rsCAList *x509.CertPool // cache RSCAList + bfeCert *bfe_tls.Certificate // cache BFECertFile & BFEKeyFile + protocol string // protocol of backend https +} + +func (conf *BackendHTTPS) GetProtocol() string { + return conf.protocol +} + +// GetRSCAList : cache the RSCAList in memory, +func (conf *BackendHTTPS) GetRSCAList() (*x509.CertPool, error) { + return conf.rsCAList, nil +} + +// SetRSCAList : just for unit test +func (conf *BackendHTTPS) SetRSCAList(cal *x509.CertPool) { + conf.rsCAList = cal +} + +// GetBFECert : cache the cert in memory +func (conf *BackendHTTPS) GetBFECert() (bfe_tls.Certificate, error) { + if conf.bfeCert == nil { + return bfe_tls.Certificate{}, errors.New("BFECert not found.") + } + return *conf.bfeCert, nil +} + +// SetBFECert : just for unit test +func (conf *BackendHTTPS) SetBFECert(cert *bfe_tls.Certificate) { + conf.bfeCert = cert +} + +// CheckBFECertAndKey : check the BFECertFile and BFEKeyFile +func (conf *BackendHTTPS) CheckBFECertAndKey() error { + var ( + certPem, keyPem []byte + cert bfe_tls.Certificate + err error = nil + ) + conf.bfeCert = nil + if conf.BFECertFile != nil && *conf.BFECertFile != "" { + if certPem, err = os.ReadFile(*conf.BFECertFile); err != nil { + return err + } + } + if conf.BFEKeyFile != nil && *conf.BFEKeyFile != "" { + if keyPem, err = os.ReadFile(*conf.BFEKeyFile); err != nil { + return err + } + } + if certPem == nil || keyPem == nil { + return nil + } else if cert, err = bfe_tls.X509KeyPair(certPem, keyPem); err != nil { + return err + } + conf.bfeCert = &cert + return nil +} + type HashConf struct { // HashStrategy is hash strategy for subcluster-level load balance. // ClientIdOnly, ClientIpOnly, ClientIdPreferred, RequestURI. @@ -137,6 +221,7 @@ type ClusterConf struct { CheckConf *BackendCheck // how to check backend GslbBasic *GslbBasicConf // gslb basic conf for cluster ClusterBasic *ClusterBasicConf // basic conf for cluster + HTTPSConf *BackendHTTPS // backend's https conf } type ClusterToConf map[string]ClusterConf @@ -147,6 +232,47 @@ type BfeClusterConf struct { Config *ClusterToConf } +// BackendHTTPS is https conf of backend. +func BackendHTTPSCheck(protocol *string, conf *BackendHTTPS) error { + if protocol == nil || *protocol != "https" { + return nil + } + conf.protocol = *protocol + if conf.RSInsecureSkipVerify == nil || !*conf.RSInsecureSkipVerify { + if conf.RSCAList != nil && len(*conf.RSCAList) > 0 { + rootCAs := x509.NewCertPool() + for _, caFp := range *conf.RSCAList { + chain, err := os.ReadFile(caFp) + log.Logger.Debug("load: ca_fp=%s, err=%v", caFp, err) + if err != nil { + return err + } + var certs []*x509.Certificate + block, rest := pem.Decode(chain) + for block != nil { + if block.Type == "CERTIFICATE" { + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return err + } + certs = append(certs, cert) + } + if len(rest) > 0 { + block, rest = pem.Decode(rest) + } else { + break + } + } + for _, crt := range certs { + rootCAs.AddCert(crt) + } + } + conf.rsCAList = rootCAs + } + } + return conf.CheckBFECertAndKey() +} + // BackendBasicCheck check BackendBasic config. func BackendBasicCheck(conf *BackendBasic) error { if conf.Protocol == nil { @@ -155,9 +281,9 @@ func BackendBasicCheck(conf *BackendBasic) error { } *conf.Protocol = strings.ToLower(*conf.Protocol) switch *conf.Protocol { - case "http", "tcp", "ws", "fcgi", "h2c": + case "http", "tcp", "ws", "fcgi", "h2c", "https": default: - return fmt.Errorf("protocol only support http/tcp/ws/fcgi/h2c, but is:%s", *conf.Protocol) + return fmt.Errorf("protocol only support http/tcp/ws/fcgi/h2c/https, but is:%s", *conf.Protocol) } if conf.TimeoutConnSrv == nil { @@ -256,6 +382,51 @@ func convertStatusCode(statusCode int) string { return fmt.Sprintf("INVALID %d", statusCode) } +func checkStatusCodeRange(sp *string) error { + if sp == nil || *sp == "" { + return nil + } + s := *sp + s = strings.ReplaceAll(s, " ", "") + validPattern := "^[0-9xX|]+$" + matched, err := regexp.MatchString(validPattern, s) + if err != nil { + return err + } + rtnErr := fmt.Errorf("StatusCodeRange format error : %s", s) + if !matched { + return rtnErr + } + parts := strings.Split(s, "|") + for _, part := range parts { + if len(part) != 3 { // xxx|xxx|xxx + return rtnErr + } + } + return nil + +} + +func MatchStatusCodeRange(statusCode string, statusCodeRange string) (bool, error) { + if statusCodeRange == "" { + return true, nil + } + statusCodeRange = strings.ReplaceAll(statusCodeRange, " ", "") + ranges := strings.Split(statusCodeRange, "|") + for _, rangeStr := range ranges { + pattern := strings.ReplaceAll(rangeStr, "x", `\d`) + pattern = fmt.Sprintf("^%s$", pattern) + match, err := regexp.MatchString(pattern, statusCode) + if err != nil { + return false, err + } + if match { + return true, nil + } + } + return false, fmt.Errorf("not match: statusCode=%s, statusCodeRange=%s", statusCode, statusCodeRange) +} + func MatchStatusCode(statusCodeGet int, statusCodeExpect int) (bool, error) { // for normal status code if statusCodeExpect >= 100 && statusCodeExpect <= 599 { @@ -287,6 +458,8 @@ func BackendCheckCheck(conf *BackendCheck) error { // set default schem to http schem := "http" conf.Schem = &schem + } else if *conf.Schem != "http" && *conf.Schem != "https" && *conf.Schem != "tls" && *conf.Schem != "tcp" { + return errors.New("Schem for BackendCheck should be http/https/tls/tcp") } if conf.Uri == nil { @@ -299,6 +472,11 @@ func BackendCheckCheck(conf *BackendCheck) error { conf.Host = &host } + if conf.HostType == nil { + hostType := HostType_HOST + conf.HostType = &hostType + } + if conf.StatusCode == nil { statusCode := 0 conf.StatusCode = &statusCode @@ -319,11 +497,7 @@ func BackendCheckCheck(conf *BackendCheck) error { conf.SuccNum = &succNum } - if *conf.Schem != "http" && *conf.Schem != "tcp" { - return errors.New("schem for BackendCheck should be http/tcp") - } - - if *conf.Schem == "http" { + if *conf.Schem == "http" || *conf.Schem == "https" { if !strings.HasPrefix(*conf.Uri, "/") { return errors.New("Uri should be start with '/'") } @@ -331,6 +505,10 @@ func BackendCheckCheck(conf *BackendCheck) error { if err := checkStatusCode(*conf.StatusCode); err != nil { return err } + + if err := checkStatusCodeRange(conf.StatusCodeRange); err != nil { + return err + } } if *conf.SuccNum < 1 { @@ -503,8 +681,7 @@ func ClusterToConfCheck(conf ClusterToConf) error { return nil } -// BfeClusterConfCheck check integrity of config -func BfeClusterConfCheck(conf *BfeClusterConf) error { +func prevCheckBfeClusterConf(conf *BfeClusterConf, fn func() error) error { if conf == nil { return errors.New("nil BfeClusterConf") } @@ -515,13 +692,35 @@ func BfeClusterConfCheck(conf *BfeClusterConf) error { if conf.Config == nil { return errors.New("no Config") } + return fn() +} - err := ClusterToConfCheck(*conf.Config) - if err != nil { - return fmt.Errorf("BfeClusterConf.Config:%s", err.Error()) - } +// ClusterToConfBackendHTTPSCheck check ClusterToConf.HTTPSConf +func ClusterToConfBackendHTTPSCheck(conf *BfeClusterConf) error { + return prevCheckBfeClusterConf(conf, func() error { + for clusterName, clusterConf := range *conf.Config { + // "HTTPSConf" does not have strict required fields, so it should be allowed to be empty. + if clusterConf.HTTPSConf == nil { + clusterConf.HTTPSConf = new(BackendHTTPS) + } + err := BackendHTTPSCheck(clusterConf.BackendConf.Protocol, clusterConf.HTTPSConf) + if err != nil { + return fmt.Errorf("BackendHTTPS: clusterName=%s, err=%s", clusterName, err.Error()) + } + } + return nil + }) +} - return nil +// BfeClusterConfCheck check integrity of config +func BfeClusterConfCheck(conf *BfeClusterConf) error { + return prevCheckBfeClusterConf(conf, func() error { + err := ClusterToConfCheck(*conf.Config) + if err != nil { + return fmt.Errorf("BfeClusterConf.Config:%s", err.Error()) + } + return nil + }) } func GetCookieKey(header string) (string, bool) { @@ -548,11 +747,15 @@ func (conf *BfeClusterConf) LoadAndCheck(filename string) (string, error) { return "", err } - /* check conf */ + /* check conf */ if err := BfeClusterConfCheck(conf); err != nil { return "", err } + if err := ClusterToConfBackendHTTPSCheck(conf); err != nil { + return "", fmt.Errorf("BfeClusterConf.Config.HTTPSConf:%s", err.Error()) + } + return *(conf.Version), nil } diff --git a/bfe_config/bfe_cluster_conf/cluster_conf/cluster_conf_load_test.go b/bfe_config/bfe_cluster_conf/cluster_conf/cluster_conf_load_test.go index ec610472a..8ebc54dc8 100644 --- a/bfe_config/bfe_cluster_conf/cluster_conf/cluster_conf_load_test.go +++ b/bfe_config/bfe_cluster_conf/cluster_conf/cluster_conf_load_test.go @@ -65,3 +65,55 @@ func TestClusterConfLoad_6(t *testing.T) { return } } + +func TestStatusCodeRange(t *testing.T) { + var ( + statusCode = "400" + statusCodeRanges = map[string]bool{ + "200": false, + "2xx": false, + "4x0": true, + "43x": false, + "400": true, + "40x": true, + "4xx": true, + "x00": true, + "x0x": true, + "404|30x|2xx": false, + "200|40x|3xx": true, + "2xx|4xx|3xx": true, + } + worngRange = []string{ + "4000", + "x4x&5xx|6xx|111", + "[200-300]", + "|400", + } + ) + t.Run("checkStatusCodeRange", func(t *testing.T) { + var err error + for statusCodeRange, _ := range statusCodeRanges { + if err = checkStatusCodeRange(&statusCodeRange); err != nil { + t.Error(err) + } + } + for _, w := range worngRange { + if err = checkStatusCodeRange(&w); err == nil { + t.Errorf("assertOk=false, ok=true, statusCodeRange=%s", w) + } else { + t.Log(err) + } + } + }) + t.Run("MatchStatusCodeRange", func(t *testing.T) { + for statusCodeRange, assert := range statusCodeRanges { + ok, err := MatchStatusCodeRange(statusCode, statusCodeRange) + if ok != assert { + t.Errorf("statusCode=%s, statusCodeRange=%s, assertOk=%v, ok=%v", statusCode, statusCodeRange, assert, ok) + } + if err != nil { + t.Logf("assertOk=%v, ok=%v, err_msg=%s", assert, ok, err.Error()) + } + } + }) +} diff --git a/bfe_http/transport.go b/bfe_http/transport.go index 90bd9b197..a25f2d4ac 100644 --- a/bfe_http/transport.go +++ b/bfe_http/transport.go @@ -25,6 +25,7 @@ package bfe_http import ( "compress/gzip" + "crypto/x509" "errors" "fmt" "io" @@ -34,15 +35,11 @@ import ( "strings" "sync" "time" -) -import ( "github.com/baidu/go-lib/gotrack" "github.com/baidu/go-lib/log" -) - -import ( "github.com/bfenetworks/bfe/bfe_bufio" + "github.com/bfenetworks/bfe/bfe_config/bfe_cluster_conf/cluster_conf" "github.com/bfenetworks/bfe/bfe_tls" ) @@ -128,10 +125,16 @@ type Transport struct { // If zero, no periodic flushing is done. ReqFlushInterval time.Duration + HttpsConf *cluster_conf.BackendHTTPS + // TODO: tunable on global max cached connections // TODO: tunable on timeout on cached connections } +func (t *Transport) SetHttpsConf(c *cluster_conf.BackendHTTPS) { + t.HttpsConf = c +} + // ProxyFromEnvironment returns the URL of the proxy to use for a // given request, as indicated by the environment variables // $HTTP_PROXY and $NO_PROXY (or $http_proxy and $no_proxy). @@ -188,6 +191,9 @@ func (tr *transportRequest) extraHeaders() Header { // For higher-level HTTP client support (such as handling of cookies // and redirects), see Get, Post, and the Client type. func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) { + if t.HttpsConf != nil && t.HttpsConf.GetProtocol() == "https" { + req.URL.Scheme = "https" + } state.HttpBackendReqAll.Inc(1) if req.URL == nil { return nil, errors.New("http: nil Request.URL") @@ -306,6 +312,7 @@ func (t *Transport) connectMethodForRequest(treq *transportRequest) (*connectMet cm := &connectMethod{ targetScheme: treq.URL.Scheme, targetAddr: canonicalAddr(treq.URL), + targetHost: treq.Host, } if t.Proxy != nil { var err error @@ -591,13 +598,70 @@ func (t *Transport) dialConn(cm *connectMethod) (*persistConn, error) { } if cm.targetScheme == "https" { + if t.HttpsConf == nil { + return nil, errors.New("https: error, httpsConf is nil") + } // Initiate TLS and check remote host name against certificate. cfg := t.TLSClientConfig if cfg == nil || cfg.ServerName == "" { - host := cm.tlsHost() + //host := cm.tlsHost() + var ( + host string + rootCAs *x509.CertPool = nil + certs []bfe_tls.Certificate = nil + cert bfe_tls.Certificate + httpsConf = t.HttpsConf + ) + if httpsConf.RSInsecureSkipVerify == nil || !*httpsConf.RSInsecureSkipVerify { + + // if the host has port, only use the hostname + host = cm.targetHost + if hasPort(host) { + host = host[:strings.LastIndex(host, ":")] + } + + if httpsConf.RSHost != nil && *httpsConf.RSHost != "" { + host = *httpsConf.RSHost + } + rootCAs, err = httpsConf.GetRSCAList() + if err != nil { + log.Logger.Debug("debug_https get_cas err=%s", err.Error()) + return nil, err + } + } + if cert, err = httpsConf.GetBFECert(); err == nil { + certs = []bfe_tls.Certificate{cert} + } if cfg == nil { - cfg = &bfe_tls.Config{ServerName: host} + if httpsConf.RSInsecureSkipVerify != nil && *httpsConf.RSInsecureSkipVerify { + // should skip Insecure Verify + cfg = &bfe_tls.Config{ + Certificates: certs, + InsecureSkipVerify: true, + ServerName: host, + } + } else { + // do Insecure Verify + if rootCAs == nil { + // use system cas + cfg = &bfe_tls.Config{ + Certificates: certs, + InsecureSkipVerify: false, + ServerName: host, + } + } else { + // use custom cas only, keep unchanged. + cfg = &bfe_tls.Config{ + Certificates: certs, + InsecureSkipVerify: true, + ServerName: host, + RootCAs: rootCAs, + VerifyPeerCertificate: bfe_tls.NewVerifyPeerCertHooks(*httpsConf.RSInsecureSkipVerify, host, rootCAs).Ready(), + } + } + } } else { + // should not arrive here, since t.TLSClientConfig is never set clone := cfg.Clone() // shallow clone clone.ServerName = host cfg = clone @@ -698,11 +762,11 @@ func useProxy(addr string) bool { // http://proxy.com|http http to proxy, http to anywhere after that // // Note: no support to https to the proxy yet. -// type connectMethod struct { proxyURL *url.URL // nil for no proxy, else full proxy URL targetScheme string // "http" or "https" targetAddr string // Not used if proxy + http targetScheme (4th example in table) + targetHost string // used in https protocol for default servername } func (ck *connectMethod) key() string { diff --git a/bfe_route/bfe_cluster/bfe_cluster.go b/bfe_route/bfe_cluster/bfe_cluster.go index c0db12c8d..5be369b3d 100644 --- a/bfe_route/bfe_cluster/bfe_cluster.go +++ b/bfe_route/bfe_cluster/bfe_cluster.go @@ -19,13 +19,8 @@ package bfe_cluster import ( "sync" "time" -) -import ( "github.com/baidu/go-lib/log" -) - -import ( "github.com/bfenetworks/bfe/bfe_config/bfe_cluster_conf/cluster_conf" ) @@ -36,6 +31,7 @@ type BfeCluster struct { backendConf *cluster_conf.BackendBasic // backend's basic conf CheckConf *cluster_conf.BackendCheck // how to check backend GslbBasic *cluster_conf.GslbBasicConf // gslb basic + httpsConf *cluster_conf.BackendHTTPS // https basic timeoutReadClient time.Duration // timeout for read client body timeoutReadClientAgain time.Duration // timeout for read client again @@ -57,6 +53,7 @@ func (cluster *BfeCluster) BasicInit(clusterConf cluster_conf.ClusterConf) { // set backendConf and checkConf cluster.backendConf = clusterConf.BackendConf cluster.CheckConf = clusterConf.CheckConf + cluster.httpsConf = clusterConf.HTTPSConf // set gslb retry conf cluster.GslbBasic = clusterConf.GslbBasic @@ -102,6 +99,14 @@ func (cluster *BfeCluster) BackendConf() *cluster_conf.BackendBasic { return res } +func (cluster *BfeCluster) BackendHTTPSConf() *cluster_conf.BackendHTTPS { + cluster.RLock() + res := cluster.httpsConf + cluster.RUnlock() + + return res +} + func (cluster *BfeCluster) RetryLevel() int { cluster.RLock() retryLevel := cluster.backendConf.RetryLevel diff --git a/bfe_server/bfe_server.go b/bfe_server/bfe_server.go index cfd5fa3e6..a11b45ef1 100644 --- a/bfe_server/bfe_server.go +++ b/bfe_server/bfe_server.go @@ -24,13 +24,8 @@ import ( "sync" "syscall" "time" -) -import ( "github.com/baidu/go-lib/log" -) - -import ( "github.com/bfenetworks/bfe/bfe_balance" "github.com/bfenetworks/bfe/bfe_config/bfe_cluster_conf/cluster_conf" "github.com/bfenetworks/bfe/bfe_config/bfe_conf" @@ -396,13 +391,13 @@ func (srv *BfeServer) GetServerConf() *bfe_route.ServerDataConf { // GetCheckConf implements CheckConfFetcher and return current // health check configuration. -func (srv *BfeServer) GetCheckConf(clusterName string) *cluster_conf.BackendCheck { - sf := srv.GetServerConf() +func (s *BfeServer) GetCheckConf(clusterName string) (*cluster_conf.BackendCheck, *cluster_conf.BackendHTTPS) { + sf := s.GetServerConf() cluster, err := sf.ClusterTable.Lookup(clusterName) if err != nil { - return nil + return nil, nil } - return cluster.BackendCheckConf() + return cluster.BackendCheckConf(), cluster.BackendHTTPSConf() } func (srv *BfeServer) InitListeners(config bfe_conf.BfeConfig) error { diff --git a/bfe_server/reverseproxy.go b/bfe_server/reverseproxy.go index a14e69424..12685ed3a 100644 --- a/bfe_server/reverseproxy.go +++ b/bfe_server/reverseproxy.go @@ -29,15 +29,11 @@ import ( "strings" "sync" "time" -) -import ( "golang.org/x/net/http2" "github.com/baidu/go-lib/log" -) -import ( bfe_cluster_backend "github.com/bfenetworks/bfe/bfe_balance/backend" "github.com/bfenetworks/bfe/bfe_balance/bal_gslb" "github.com/bfenetworks/bfe/bfe_basic" @@ -63,8 +59,9 @@ import ( // prior to the headers being written. If the set of trailers is fixed // or known before the header is written, the normal Go trailers mechanism // is preferred: -// https://golang.org/pkg/net/http/#ResponseWriter -// https://golang.org/pkg/net/http/#example_ResponseWriter_trailers +// +// https://golang.org/pkg/net/http/#ResponseWriter +// https://golang.org/pkg/net/http/#example_ResponseWriter_trailers const TrailerPrefix = "Trailer:" // RoundTripperMap holds mappings from cluster-name to RoundTripper. @@ -140,6 +137,53 @@ func setBackendAddr(req *bfe_http.Request, backend *bfe_cluster_backend.BfeBacke req.URL.Host = backend.GetAddrInfo() } +// compareHttpsConf compares two BackendHTTPS configurations and determines whether they are identical. +// Return: +// - bool: Returns true if all fields in src and dst are identical, otherwise false. +func compareHttpsConf(src, dst *cluster_conf.BackendHTTPS) bool { + // Check if either src or dst is nil + if src == nil || dst == nil { + return src == dst // Both must be nil to be equal + } + + // Compare RSHost + if (src.RSHost == nil) != (dst.RSHost == nil) || (src.RSHost != nil && *src.RSHost != *dst.RSHost) { + return false + } + + // Compare RSInsecureSkipVerify + if (src.RSInsecureSkipVerify == nil) != (dst.RSInsecureSkipVerify == nil) || (src.RSInsecureSkipVerify != nil && *src.RSInsecureSkipVerify != *dst.RSInsecureSkipVerify) { + return false + } + + // Compare RSCAList + if (src.RSCAList == nil) != (dst.RSCAList == nil) { + return false + } + if src.RSCAList != nil && dst.RSCAList != nil { + if len(*src.RSCAList) != len(*dst.RSCAList) { + return false + } + for i := range *src.RSCAList { + if (*src.RSCAList)[i] != (*dst.RSCAList)[i] { + return false + } + } + } + + // Compare BFECertFile + if (src.BFECertFile == nil) != (dst.BFECertFile == nil) || (src.BFECertFile != nil && *src.BFECertFile != *dst.BFECertFile) { + return false + } + + // Compare BFEKeyFile + if (src.BFEKeyFile == nil) != (dst.BFEKeyFile == nil) || (src.BFEKeyFile != nil && *src.BFEKeyFile != *dst.BFEKeyFile) { + return false + } + + return true +} + func (p *ReverseProxy) setTransports(clusterMap bfe_route.ClusterMap) { p.tsMu.Lock() defer p.tsMu.Unlock() @@ -157,7 +201,15 @@ func (p *ReverseProxy) setTransports(clusterMap bfe_route.ClusterMap) { case *bfe_http.Transport: // get transport, check if transport needs update backendConf := conf.BackendConf() - if (t.MaxIdleConnsPerHost != *backendConf.MaxIdleConnsPerHost) || + + proto := "http" + if t.HttpsConf != nil { + proto = "https" + } + + if (proto != *backendConf.Protocol) || + !compareHttpsConf(t.HttpsConf, conf.BackendHTTPSConf()) || + (t.MaxIdleConnsPerHost != *backendConf.MaxIdleConnsPerHost) || (t.MaxConnsPerHost != *backendConf.MaxConnsPerHost) || (t.ResponseHeaderTimeout != time.Millisecond*time.Duration(*backendConf.TimeoutResponseHeader)) || (t.ReqWriteBufferSize != conf.ReqWriteBufferSize()) || @@ -168,7 +220,6 @@ func (p *ReverseProxy) setTransports(clusterMap bfe_route.ClusterMap) { newTransports[cluster] = transport continue } - newTransports[cluster] = transport default: transport = createTransport(conf) @@ -202,7 +253,7 @@ func createTransport(cluster *bfe_cluster.BfeCluster) bfe_http.RoundTripper { log.Logger.Debug("create a new transport for %s, timeout %d", cluster.Name, *backendConf.TimeoutResponseHeader) switch protocol { - case "http": + case "http", "https": // cluster has its own Connect Server Timeout. // so each cluster has a different transport // once cluster's timeout updated, dailer use new value @@ -211,7 +262,7 @@ func createTransport(cluster *bfe_cluster.BfeCluster) bfe_http.RoundTripper { return net.DialTimeout(network, add, timeout) } - return &bfe_http.Transport{ + transport := &bfe_http.Transport{ Dial: dailer, DisableKeepAlives: (*backendConf.MaxIdleConnsPerHost) == 0, MaxIdleConnsPerHost: *backendConf.MaxIdleConnsPerHost, @@ -221,6 +272,10 @@ func createTransport(cluster *bfe_cluster.BfeCluster) bfe_http.RoundTripper { DisableCompression: true, MaxConnsPerHost: *backendConf.MaxConnsPerHost, } + if protocol == "https" { + transport.SetHttpsConf(cluster.BackendHTTPSConf()) + } + return transport case "fcgi": return &bfe_fcgi.Transport{ Root: backendConf.FCGIConf.Root, @@ -561,11 +616,11 @@ func (p *ReverseProxy) setReadClientAgainTimeout(cluster *bfe_cluster.BfeCluster // ServeHTTP processes http request and send http response. // // Params: -// - rw : context for sending response -// - request: context for request +// - rw : context for sending response +// - request: context for request // // Return: -// - action: action to do after ServeHTTP +// - action: action to do after ServeHTTP func (p *ReverseProxy) ServeHTTP(rw bfe_http.ResponseWriter, basicReq *bfe_basic.Request) (action int) { var err error var res *bfe_http.Response diff --git a/bfe_tls/bfe_testkeys.go b/bfe_tls/bfe_testkeys.go new file mode 100644 index 000000000..837447d6d --- /dev/null +++ b/bfe_tls/bfe_testkeys.go @@ -0,0 +1,300 @@ +// Copyright (c) 2025 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package bfe_tls + +type TestPemNameEnmu string + +/* +* +Prefix Explanation + - I: Intermediate certificate issued + - R: Root certificate issued + +Suffix Explanation + - CRT: x509 certificate + - PRV: Private key +*/ +const ( + BFE_R_CA_CRT TestPemNameEnmu = "bfe_r_ca.crt" + BFE_I_CA_CRT TestPemNameEnmu = "bfe_i_ca.crt" + I_BFE_DEV_PRV TestPemNameEnmu = "i_bfe_dev_prv.pem" + I_BFE_DEV_CRT TestPemNameEnmu = "i_bfe_dev.crt" + I_CN_FOOBAR_ORG_PRV TestPemNameEnmu = "i_cn_foobar.org_prv.pem" + I_CN_FOOBAR_ORG_CRT TestPemNameEnmu = "i_cn_foobar.org.crt" + R_BFE_DEV_PRV TestPemNameEnmu = "r_bfe_dev_prv.pem" + R_BFE_DEV_CRT TestPemNameEnmu = "r_bfe_dev.crt" + R_SAN_EXAMPLE_ORG_PRV TestPemNameEnmu = "r_san_example.org_prv.pem" + R_SAN_EXAMPLE_ORG_CRT TestPemNameEnmu = "r_san_example.org.crt" +) + +var testkeys = map[TestPemNameEnmu]string{ + BFE_R_CA_CRT: `-----BEGIN CERTIFICATE----- +MIIDkzCCAnugAwIBAgIBCjANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJjbjEQ +MA4GA1UECAwHYmVpamluZzEUMBIGA1UECgwLeWluZ2ZlaS1kZXYxFDASBgNVBAsM +C3lpbmdmZWktZGV2MRcwFQYDVQQDDA55aW5nZmVpLWRldi1jYTAeFw0yMzExMDMx +MzQ5NTVaFw0zNzA3MTIxMzQ5NTVaMGQxCzAJBgNVBAYTAmNuMRAwDgYDVQQIDAdi +ZWlqaW5nMRQwEgYDVQQKDAt5aW5nZmVpLWRldjEUMBIGA1UECwwLeWluZ2ZlaS1k +ZXYxFzAVBgNVBAMMDnlpbmdmZWktZGV2LWNhMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAvNA3HrsMjBcXrMIIhGVWsurIA1F9jxKeA7dh06H00Vt4inVV +SUvNFrTqgPRhLkAhGRMPxrjVRgJ5bbFqqXIuPIpUFBhUsWXIDH+oVXQl9jsxAXaG +gZ0lTO/uYR9qyrS1rj9nyNPwRf59Al/VlsQL71cNQ/T/agJ4PfvPfULTPLOsqclJ +hj0IgXmDj464dqcdG3ZdfXpfhNF6ab+8YjpwafTRmY+LoV8qjUwsYeJMcW4N8pxJ +8F2ktZj9J6uWepNGj+87ZeXg9XquzC62ASIFzPjoE1WN//Q518EqizxhuLGBDpK3 +6sEYUK4kHYUL4gZKFPlKTPXIl0ZsJIM5PsBMKwIDAQABo1AwTjAdBgNVHQ4EFgQU +DZloT8VhVbysD819oG/oqHdW1D0wHwYDVR0jBBgwFoAUDZloT8VhVbysD819oG/o +qHdW1D0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEALgB+IcN3UD4T ++4g8jaOyOiSUpUVdYqW+3go5DppqnvlGNq99N2cXKqssVPa/T9TikEcBEicFa8zU +bwlx6TEte+MkWfWdQxFR1EuI1FgKr3ps6ZBRr7MPpnYmI7K9aK372K9n7WrQhbmP +s7ult8bWB1/t6o3R7B9ChNkWT+7DPD4+FvB1GMJSGPno7cdnvDkevBOuC2DnQl3M ++ADFAge1Lo8wKBy6gYkNFd2BfarHGvRC5Qmmrme+RIpWZnvux1+lfXIInnfSTJRM +uAo/ePkoNsM3qQll6uEdhDxOMx8Pq94bCkM3DtI3ObuxWXjKCgUm/n9yetUvPj4U +iYhRUqHpUg== +-----END CERTIFICATE----- +`, + BFE_I_CA_CRT: `-----BEGIN CERTIFICATE----- +MIIDwDCCAqigAwIBAgIBCzANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJjbjEQ +MA4GA1UECAwHYmVpamluZzEUMBIGA1UECgwLeWluZ2ZlaS1kZXYxFDASBgNVBAsM +C3lpbmdmZWktZGV2MRcwFQYDVQQDDA55aW5nZmVpLWRldi1jYTAeFw0yMzExMDMx +NDAxNDZaFw0zNzA3MTIxNDAxNDZaMIGQMQswCQYDVQQGEwJjbjEQMA4GA1UECAwH +YmVpamluZzEUMBIGA1UECgwLeWluZ2ZlaS1kZXYxFDASBgNVBAsMC3lpbmdmZWkt +ZGV2MRgwFgYDVQQDDA95aW5nZmVpLWRldi1pY2ExKTAnBgkqhkiG9w0BCQEWGmxp +YW5nY2h1YW5AeWYtbmV0d29ya3MuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA6eFuWgoknixrRO9NCX4jAKyLtcAOWVJqVN2yX7CxjZjLyPurjvTZ +W73NYUbrPAN4AB5gY6UAPzuiEOSopVVIZ0OschK0cJldu9vZ0mZObBOsovuFcLQq +dgNSJ5slJSk7tCgD2EnCB3GYPG4D+uIKYd0c49wzTWWv4bjDwpgnf0LQbFpy7GhN +7D59zFH4qgOK/IQ5vaTMGyvIvtWR5/1Gvc9MLpGopTgi0DiNLed4UwDYrod5kysl +q3UcB5puONHQISOVoD3uRxo7wdsmVsHUfW7YfAWkhi6ec8mx9fy8IyE6f7GlXnuV +ysNccwqyEotL5bOXPJCqwUCL1v+iKTMPWwIDAQABo1AwTjAdBgNVHQ4EFgQUCjvC +vDVr8QXh9/hMI5xBgNvoSFgwHwYDVR0jBBgwFoAUDZloT8VhVbysD819oG/oqHdW +1D0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAhIggK/6JK4N7+HZW +VoTemwRpilugZZyKrcVAHbiiwUfXVQVuI64vc+yHWMSFRD1mykHkKBFzxEoDabMl +ASBtUJNt4b4zEL9V7k295vmAOp2IdLUQxlgeKWqABm4DGX96tGR9nKQTcn1ZeAxx +NyQFV1aj+dnNcF1iFFNF6t0bRrOEZ/aRSiu1bWp3Dj1JYTjXbyyplh3Ktb8lv4lt +EmvCXjo/l4TgQC9233kcTkXcq1swppzkkXfhB0NVuf9DE3C2xAWX0b2FiIEqnAhl +Bx0Cn3RrX7PZ6qrdRL6oBKvGJV6DP8BlF4LXiuD1VN/waQFJha57KQ78ZYYIXxQg +WMjWgA== +-----END CERTIFICATE-----`, + I_BFE_DEV_PRV: `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAxTz6v6i/s/egWxWvXrjwmGp5c4QZD4ybcCezJHOQvmGpjwaf +yVqgvEXprxOZ9DUeE3frCFb2j6wo0KeofvZcerUaiE/Lde0/olhUC2N5KZKcZI6T +z6IKXqO/IYUH5QLZoOt/p+R61vfDmm/RPedScuPe7vi7IjHZqjG+zevRpngWt9vc +CMMe15NBmkyUkfDcD2L+TKibpEFa+NMMUuM4ZdwrWY4ctbgvPGNWDF97wa4riKGG +sM3V6iayHqgKsdstpk9tYY/+RKqGQSaMYgbRZA+4jUysuE3oOsKz0QOhn5XAAe98 +rhJbCa5Fi5avnTVQOypDk5aAxsdHybip8lvP/wIDAQABAoIBAEJNA0UV5osKfF7h +5TeEF2xErlrEVuBBNab30WI5brhwf9zSLzgyPMHNBoaRojjS+i81Kk59XRhimL26 +/grfqaqd4jNcD1quy6s44deKMppk7ClpPAqZv76ccI1F+Kdk098iCqFXTmugkaIC +YGXcsnxoWPIfrlgKRc7ONgNmd+zq7SAuVxC7gFrDSkjNX0rtMF32y2ZaSjPnwwu+ +Eego/wk35IhMYM7xx2t39CQPwXxvTeGmc2Ri8Q4fWtiVnPZAIURDghouWEwoU/T+ +70cByaZpoxPJLfgSC5XHAGOm1C+XzKw8H0SKZsPVLnQrhx8ouNhMGJ16e8MhHWmN +r3V2OkECgYEA/U2KB5S+rWfnKwkI9hucTA1XBFprEVVSAjRLKpwrR5E6SZbotiAu +mMi5BPQA0VD94M66JQE9p0q6VmfJYZGWq8ALvpA+bh+u914L66HU5eRCtfXA8rCb +GyOv27GHyam9guzisJJUCNpgvM+IBHhAHT3tX0+E9sj+oduYyZbkto8CgYEAx1ae +Eboy3baPvsMBAlNS38f0NUpfvp8j4jiPcGk48BWafn7jTU5GC4aRSmzCWhIKvgmA +If7hiEBFC+HQWXUVrq4svV7CIxd460E+OuLPz9cOUse+ppAToJXqlr0FRQyBPovH +eZ5E2SSK+ubv7cqrD2soMlBxdq67F6QK5rZFh5ECgYEAixtFHUqzuJliG4E3uaiK +Gj4NNqfNOtSnV+yOBxWCsyfvYbCNlk9wJ5m7+htiQ5F+CzKciWv4BuKEGKWgs4N3 +wlSSXpHqpyGhPdoZI8tZFvNXK4SN9PnGBI6Bql4Bm18rYzZie+OwYLhE/gvev82m +MCjtLjWGaN0S8aKecr76VcMCgYAM+C3AqYS1uDMSDk36gMFbnf7dmMEx/div1049 +2hrCRCWRJWBUn0sfZNn/JaRfh9z7EFMt4w95dfUIGOEdcOjAPMTcbVXkQpqzc8NA +wZETzMI50JUu8SDVyetBc3rsSyv9jcqktw9zsVT5jhz+M7l9f1NWMrWvKx8xIpMy +/5j2gQKBgGQ5mRLkmTl8cruj+pa9VBZUNBGW0JIEY6U44iX4l1TrPd34UjANb9Cu +pcoy47AvYKGcZUvS2Z1KKscqClzue0glgtw06zVIK9z1non/3suiGENSREu534ey +ilHWKsWtn+943EndCZMVdECe60AbUMxNWECos3KdEOKo262hG+kU +-----END RSA PRIVATE KEY----- +`, + I_BFE_DEV_CRT: `-----BEGIN CERTIFICATE----- +MIID4zCCAsugAwIBAgIBCzANBgkqhkiG9w0BAQsFADCBkDELMAkGA1UEBhMCY24x +EDAOBgNVBAgMB2JlaWppbmcxFDASBgNVBAoMC3lpbmdmZWktZGV2MRQwEgYDVQQL +DAt5aW5nZmVpLWRldjEYMBYGA1UEAwwPeWluZ2ZlaS1kZXYtaWNhMSkwJwYJKoZI +hvcNAQkBFhpsaWFuZ2NodWFuQHlmLW5ldHdvcmtzLmNvbTAeFw0yMzExMDMxNTUz +NDhaFw0zMzEwMzExNTUzNDhaMFwxCzAJBgNVBAYTAmNuMRAwDgYDVQQIDAdiZWlq +aW5nMRQwEgYDVQQKDAt5aW5nZmVpLWRldjEUMBIGA1UECwwLeWluZ2ZlaS1kZXYx +DzANBgNVBAMMBmktdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AMU8+r+ov7P3oFsVr1648JhqeXOEGQ+Mm3AnsyRzkL5hqY8Gn8laoLxF6a8TmfQ1 +HhN36whW9o+sKNCnqH72XHq1GohPy3XtP6JYVAtjeSmSnGSOk8+iCl6jvyGFB+UC +2aDrf6fketb3w5pv0T3nUnLj3u74uyIx2aoxvs3r0aZ4Frfb3AjDHteTQZpMlJHw +3A9i/kyom6RBWvjTDFLjOGXcK1mOHLW4LzxjVgxfe8GuK4ihhrDN1eomsh6oCrHb +LaZPbWGP/kSqhkEmjGIG0WQPuI1MrLhN6DrCs9EDoZ+VwAHvfK4SWwmuRYuWr501 +UDsqQ5OWgMbHR8m4qfJbz/8CAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhC +AQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFL/f +4zPE61A0bZ6854Yg0q4zbZ9GMB8GA1UdIwQYMBaAFAo7wrw1a/EF4ff4TCOcQYDb +6EhYMA0GCSqGSIb3DQEBCwUAA4IBAQAIUv8sRqJ1NfrbURoQdS2N17P66q0t717e +dpHSBfCpqeq4aVgDwKT1k6XRr/LdYgSHuatctProtcHT3BOSqrT2iqK4YvEdlht8 +obOoL7aQFn5hFh64n5ziHHZt1XrZjNobT7jilxHgekdsJlpfT7tIwlMS2vTOy3qJ +7yj2uHr0VCxDLFaW0fdsiAyLY+K7GertqU2xs0eJNnbrx5lA/7QqpQvhRQh97Rn6 +pldqXdsQ/wGOueVyzyRg08HAmLR/BkB4tg6ZpeY7pR2YWr2p6C6v+pg5p3RqXp8f +2T0fpCROTtl47Rzl8Az3D0NItImVLuyp1FMxqBXLIUObk/aM3z+d +-----END CERTIFICATE----- +`, + I_CN_FOOBAR_ORG_PRV: `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAreq0i1hG2iGhDNzsA7jYRPqK9WlU8ZJGcrfKgtx5PUEkvkPi +Kqg7epkHzSSftOA2Vis1L612SXzi75zGFP9XyBmmu5P1Cjt7AMy+pfsetlwEyHdz +CV9OoFj0UmeFhPgK44LqejlOq7dnvYTDTMLW4w/e4Cv6LdNNc9UYHOWtzwdgWtP+ +KODxpAJMTFUNqy/+al7GXodIdZLwK9hZ0pahjOck9h5Z6FyZ5dyBWWFCOuzydWvb +k6KSmYUbnW2EGTIxu83X/gUlRK5iQiDs90eOORoz1BtdF8lJp2fnCM7NExNvzc9h +6GRjY4P0HXaeO6q3VD2JG//TdmM1JjXTDJRbmQIDAQABAoIBAFDBXeoiKGwbF2wg +nRqxVwLYj3Oa4E13jWyxOMCA3W7687PBU6BZE50+t6ei7OGRBsoGMCEeW4GOEtYO +JtATLNCGUKxi7HI+4kOhzpjFvSISIkpvZKQidCDyjShCV3ZBstSnsnbka0pC1FHk +9saK5jry4JuY8AcdSaaSeSrZ31X0o+igelUe1NFCGQYdNrctrK8WmpXIE6B17xw9 +toorD/bvBLpKJ7857s6AdWI0/jH3MjUq43DwTg/4MD4ppmgRRDmtdghdC4wyKse+ +BNsbRIfASApGefQZloP3VjztzJ+MzF2ATjdWGPipcKjHDOteLDGW1YWiOIjFxndA +KKKidl0CgYEA1f7Vt6XCCVk8BTuBAf9uVAkDvgCglumtJ16DmgtT3DVNyDddboqg +aoJK0rONMbVRUhiPOeJdhZyQOh6qTx6XVxUq0IdpZ9dgPy7t6mYaBv+Je8pIbUMq +qVC89IEg9oF5YfeDyd8Pt+glU3NKjpOGpC2aIwcx8+BeljHw1fa9mPsCgYEA0A3w +Z1MAAB+2Nz+d0fmRkG4H4My4tSAr0Dy7A1TfEKrh+SOLX6pXfvV5CN2vHRLbxSOW +wsng27BO84/sxKhvQhl+twZTj8mKv8Q5ggGxks8F3UaVOrr5Tr/tBpD3Uyvf5Sbf +a5aDwHtBvq/g0pH/nmvjuDd7igGG4K49Id/yoXsCgYASdIvR7sWxMLTwbpVNqs3F +CZH9DOjMmxKH1qra2ic9UouGvN+d7O6wwNPbIAkJRG3i+qM/hroyD7KQkJx3flfm +9rhei48XmYd/a3ixQmT0PY2J04QziBthxsjE/W3uVHQ3crU277nXTnoJnGPNsANo +nVYxtykjszH1GhF/ImxviQKBgGjuLsodvUlStRbGOfV1limupMCG371R/WbnyjYS +7vG8DX4WpCtagQhiC2oiTDgwk7Cok1eoc4S5Ngh3FSXWAU7oBtWOFkTVk+nFsG9W +JVXynXWNLKRTOnuyJnwCTwqefSxYX6QmZEqGn5DpqUzqH26p4U6+hMsEnB7jIafd +B8TTAoGBALJyVJ7SULTr2Q03s/GDaug9KaIVexfieIRqbY2fsZPhlUQXt0dERznd +CS/qnU2OH69ddQagJ63T8SK54PSLhQ7XsjxdZ92GxMdid+KzTH277f2lklnneE8q +I5glmikP62ljseHxls2rr0nMod7yMwnn3gbQX7TMFdd2oSCiVEMs +-----END RSA PRIVATE KEY----- +`, + I_CN_FOOBAR_ORG_CRT: `-----BEGIN CERTIFICATE----- +MIIEDTCCAvWgAwIBAgIBDDANBgkqhkiG9w0BAQsFADCBkDELMAkGA1UEBhMCY24x +EDAOBgNVBAgMB2JlaWppbmcxFDASBgNVBAoMC3lpbmdmZWktZGV2MRQwEgYDVQQL +DAt5aW5nZmVpLWRldjEYMBYGA1UEAwwPeWluZ2ZlaS1kZXYtaWNhMSkwJwYJKoZI +hvcNAQkBFhpsaWFuZ2NodWFuQHlmLW5ldHdvcmtzLmNvbTAgFw0yNDExMDYwMjUw +NDdaGA8yMTI0MTAxMzAyNTA0N1owgYMxCzAJBgNVBAYTAmNuMRAwDgYDVQQIDAdi +ZWlqaW5nMRQwEgYDVQQKDAt5aW5nZmVpLWRldjEUMBIGA1UECwwLeWluZ2ZlaS1k +ZXYxEzARBgNVBAMMCmZvb2Jhci5vcmcxITAfBgkqhkiG9w0BCQEWEmNjMTQ1MTRA +aWNsb3VkLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3qtItY +RtohoQzc7AO42ET6ivVpVPGSRnK3yoLceT1BJL5D4iqoO3qZB80kn7TgNlYrNS+t +dkl84u+cxhT/V8gZpruT9Qo7ewDMvqX7HrZcBMh3cwlfTqBY9FJnhYT4CuOC6no5 +Tqu3Z72Ew0zC1uMP3uAr+i3TTXPVGBzlrc8HYFrT/ijg8aQCTExVDasv/mpexl6H +SHWS8CvYWdKWoYznJPYeWehcmeXcgVlhQjrs8nVr25OikpmFG51thBkyMbvN1/4F +JUSuYkIg7PdHjjkaM9QbXRfJSadn5wjOzRMTb83PYehkY2OD9B12njuqt1Q9iRv/ +03ZjNSY10wyUW5kCAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYd +T3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFK0/W1YLE7CK +2QpgpSR40IgEbVp3MB8GA1UdIwQYMBaAFAo7wrw1a/EF4ff4TCOcQYDb6EhYMA0G +CSqGSIb3DQEBCwUAA4IBAQBoyqiqQFHmH3DqWX9BJywWe2b6CdmJo+8eRhLbrHpv +JxPZ7ThhwIZS8Wku8D/z20g6McpurNy/fihqLCfkSXmoXCM0pl6ZGorVJCb4/2H4 +VwmZXdSN4YREjF2Mz1tud2t0/BXKToKRT8tw/yamO0Zv53uf6R/SOBgLwFyMHNLa +oU0Mq8adjPdkHVs8nkI09WKPd300xExgksEtwM8afXF8zSuWMPyxnTULwEPpj/mP +BFXhHx0Xy6XnMXgHfeJS/kTvNOQ+7TGAhcTYEdHEioOnMVu0whvy7ewAf8mfR7IZ +8uaFZ81R6wW0nJRqoHDHj5h4fyErZ+PaCKlZxESfmNuC +-----END CERTIFICATE----- +`, + R_BFE_DEV_PRV: `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAsn/BDM1JQ9mZeBVMWlKpp7/V65JxQ+E35SkbaPRvTOr6pIp8 +KQEq+nyBW8jroUCUtyuC4gAINoTwt9JaHlaXqjb/TQdJLf4lOvnp8a1ObiGXqfmi +payCIwrS4JfNLxSx8Iqo5sWXRZT4jj+WZlsLjHwHYRhEkvcjWsJLiFhZXcpcDW7d +/xhZZd+VmeM6NkgfPzrmzoXzCwReku1vjnSS5DdG2l8XYpyCQAb7KfhV8rojdcpk +wEUDEr31FxV+R9W9MPKZymvjB7CuRIkeECbqdd9vB7ZHdlRHT2z2aP6ozyIgc+gZ +VYr+9XjoUYhSgB951MWuj9Ir9kEBQgHPmMLJJQIDAQABAoIBAFAKHzupVb/58+o3 +yqv5wx94Uuk2GlnwxIqaezL94GaiO0/K1U/huS7m426P0rDU75qPBTpn/0bLJ9GV +nllaRNnLnYEh0juwaWtfovp+1ttlbseGK9uUVip2cQbKqvQAmKWe14vbcDCAU1Ad +zUgKbUxKVVjBdAZekVjiJNJ3o2L9WPhf/uQo7A1XAJh2DajlTbgvrDM73W+47QuU +X4OHU0FMio6bxupu3OWl1bMrnKhuC4qczZWf2nOpcVQa89rtopuP4ENLJuWkbeGk +YQpNilEclnAa/Noumt/j/6GKC1EEHFsH2CNRRIazcZrsFhkSKc4pn1Y/WI3vj8kZ ++RYnJsUCgYEA6gr0x8suwRTAmVpoPk4XyP1x+eInG7onV5RjgsUtgruYd9naxRHg +2p8PHcv32pDs51Fa+4RldyMd/jec1SscRF5/+VOP9qeoRnaDqUu+uASgEO3OBxbP +JcWovyxRHIQxbYCQtqIr9bdzXw55MBLZou/sBUVTAkrIyPVyjWqZlV8CgYEAwz7L +YyYN615TsrzZKURxMjj94Nmob/NldSLRXaR3Ax7/ABtEOA685cwQxq7ONdkJTMIA +uR8u2GHZSzGiWnehuF6Zp7Xs71a57eFbs3ueZvvEba4Dff7hl7Y4tTlwrKndKjvP +J/5a2Ol8siQcRWAXHOdzggEHMSZ/sB4hWswly/sCgYEAhLRBpyemEwTZUBrbELjm +86gBgFajJi2fMSGKaxOygnYsNYjpauSAQnX99D87Aks6iM6wb/zaK3tV/lc6LgSL +uph6p7yh3JGj8JAyh0PTmDPHLtIoCAz+18QDsqJGO40ZGaXUaDn8Aw9J85QZUxDd +Jm4zvalZL+uHfarukRDolLECgYBUiupS4nWAh3XCnZeDEQna72avaFBROZmjIRJ7 +c+28wj009JmTlH4jGzvgbG0KUBKA1Div8Fq+g5AtyS498jNqvDvYrSQNdwZHhR/K +Fis++KHTxFfqxOU2Zkcj4d1yRpNn6EIJVVBNQL0n/g7n03XupCIWFw/gLoV343QZ +9vAe5QKBgA8ml59z1w3eUooc0yGfLhihXqCmM3IU006bFbODA30fBU4QKrHO8+Yx +Xbz9bi/1QLagLG6FzYQAkOjBlEBt5XLayYvwSb8xvWsm5A3vzTAbMFDOsDPEmRoH +dWtQccJcygOuK+PZtoZnNoJciNO5c9dZWD3xtIiURmX/kVtWrf6N +-----END RSA PRIVATE KEY----- +`, + R_BFE_DEV_CRT: `-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIBDTANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJjbjEQ +MA4GA1UECAwHYmVpamluZzEUMBIGA1UECgwLeWluZ2ZlaS1kZXYxFDASBgNVBAsM +C3lpbmdmZWktZGV2MRcwFQYDVQQDDA55aW5nZmVpLWRldi1jYTAeFw0yMzExMDYx +MzExMjlaFw0zMzExMDMxMzExMjlaMF0xCzAJBgNVBAYTAmNuMRAwDgYDVQQIDAdi +ZWlqaW5nMRQwEgYDVQQKDAt5aW5nZmVpLWRldjEUMBIGA1UECwwLeWluZ2ZlaS1k +ZXYxEDAOBgNVBAMMB2JmZV9kZXYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCyf8EMzUlD2Zl4FUxaUqmnv9XrknFD4TflKRto9G9M6vqkinwpASr6fIFb +yOuhQJS3K4LiAAg2hPC30loeVpeqNv9NB0kt/iU6+enxrU5uIZep+aKlrIIjCtLg +l80vFLHwiqjmxZdFlPiOP5ZmWwuMfAdhGESS9yNawkuIWFldylwNbt3/GFll35WZ +4zo2SB8/OubOhfMLBF6S7W+OdJLkN0baXxdinIJABvsp+FXyuiN1ymTARQMSvfUX +FX5H1b0w8pnKa+MHsK5EiR4QJup1328Htkd2VEdPbPZo/qjPIiBz6BlViv71eOhR +iFKAH3nUxa6P0iv2QQFCAc+YwsklAgMBAAGjezB5MAkGA1UdEwQCMAAwLAYJYIZI +AYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQW +BBTu6GhC37Hw729HUb3UlGAfBYWhAzAfBgNVHSMEGDAWgBQNmWhPxWFVvKwPzX2g +b+iod1bUPTANBgkqhkiG9w0BAQsFAAOCAQEANZjZV/+uNUpr/FRUI9go6I3nJ95g +ElJxzz7QIgg4/UuZoD9bEA9d1qwIs5lxLqMGAHFwyqh5U8/6lHsFG9iZsbpWU+B0 +Dm5lyyNIediedSviri8mArQYZEckGrERTMFZ08hj3SrGyS5q1Z/IjJf5Tc9edh2X +D6pnaVcgf83M36yjHJybXopbKgJbHJTupSBp6DTUuSNe3Me7gf1ZrMdC7LhOs8ty +wuLVkRitAfb1SPhHVeivY75V+85fX5UsDJbFHGnUrTHSqqoCspqTvl+TdxH5T5gO +cyRHBgurceRZqAdCanjjgKG4yUFpWeOSDBb4EAbfC2Wvi3oqqyV0QN9wMQ== +-----END CERTIFICATE----- +`, + R_SAN_EXAMPLE_ORG_PRV: `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA1kewF2mRTNBmySUuOPKEJ+J8OMwEuQyOPczvS8U1K8GC2EH8 +Jcgk8uAlqvl2POKyUPst7E4W1MHaHuNRn53JcvnLzBScwYLJdh+Y3Q65iiDWq/Km ++S8jgfOv5EcMVZWU3u32pSTuMudreeFCb6QHspUd9amOYEJwQr3iMBhodFIymKmB +2tjGb14dznm28+xP7X0iV9IU0PvyUNSAO4ntd/1FbOZSawpScaxZyNUl9UAD+1Gw +EacAednYTwBDlmhEKUHc0syRyGGVQU0OZlq1FWc+im8p3xyKb+6el5yeaXHTNFJ1 +6ernUXcjmEbKR6LT05cDQUvjMxFyLa+/Kz6zUQIDAQABAoIBAC4sYGuLGf49Ygix +9FXdHFEj4rSyccoWRIhYoq/nHOAC4NkMzvKtQBj95+ABxVK1XstIdMrYwN6zrva8 +8Re9/mzCGwIs5uJj9ll30Y7A34Y+MUP4E7baS4JzKlG8ZZIDm4K2MFHBtXpOl8A5 +pAE+jVIUA9Kt6LohVuNq21SVzdxSfNYC/+SLqSftkWa/ZsqdkiHM5Hl+fVedh516 +IaLNW5hSthGh5n8dHY5h/AKPjfoq77aYp5/CUtJTC9mYdZu1j/W/pBVTRfOnwLQd +SQ1Xmr7f6q9Vmz+HnajIbFg9hQ54blvtUJ7DnugWxfUcoxf7ue79fnYjOIUOkRWw +8Iid/mECgYEA+5s0p0j+gkNZN5QtVNStoT04+1DqA1O381gczeaiz6Njt/MT0y5W +OpCsILQ70CpEjWAV+f6PDJiesDMxdGV+v2TCqK8ml8GahEczBLnHoAkPWCVf2XOX +oNj/CkZ2kmWufHFR+kcQbeDt1vFFcYUa61hKDFyNjinMW79Qy+PQID0CgYEA2gWd +7thE05sqU7/1MntmVRONKoAgnJmHcfSpWwLyh6E4YX3iKDSgI/9RnAMF39KUUY/O +XFWyIwAM9soeXknVsV/SmCaPeaEDiLHqz98aUEfvdLYMnuR883GgoXc4JrLsLw4z +oSi9lbAZFn0ekJ5L+rSFrY4rz9QZgYZxsLjUXKUCgYAbkaUSU2g3w8Np2J2i9u7T +hQ7SUsphdPHqAxSc5xGd6MxLYqIgeKpQHnwN1VHcfFUonIer7d2kxrBUpDdeBqT9 +ub+ulgqHhFo29ko70VNzUKrSwL2g6Q6LPFutt4zUe7nDvvL5loHRWF0XOTafurL5 +aKIsepO0KRZQU0U6IgszDQKBgFs6ZHaP2mTtFY4L0a75Ab3xu20gRgUhHRLq/H6P +wipMpMnuodaPBr9pU53Dig65D8T9Nq1eUnbgy4vs0T5FCPz6iqWN5RVQ8aieQhIP +WfRj1Wfx0WAfXcWEM2G9ACr5TWj3OVVjNclP8X9+hW6gPky+gv03c0+4gZ+4QRRg +ksPdAoGBAKXVv8L5Qt8rmi+QpF/6DVSeMB6lEevf319UEtyZPiYSBn1JJ9H+3Ddc +TMriXgZrw+PoXNJTAguDeGgzmtye1vraP0cjt7aG6sQ4YtMTnnuja880vK8z4i3Q +HJAi5fI8NvlW92dQBAccgrzFIKpRx+6CHwtCkNrxL2vJmGGzp8BR +-----END RSA PRIVATE KEY----- +`, + R_SAN_EXAMPLE_ORG_CRT: `-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIBDjANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJjbjEQ +MA4GA1UECAwHYmVpamluZzEUMBIGA1UECgwLeWluZ2ZlaS1kZXYxFDASBgNVBAsM +C3lpbmdmZWktZGV2MRcwFQYDVQQDDA55aW5nZmVpLWRldi1jYTAeFw0yMzExMDYx +MzEyMThaFw0zMzExMDMxMzEyMThaMGExCzAJBgNVBAYTAmNuMRAwDgYDVQQIDAdi +ZWlqaW5nMRQwEgYDVQQKDAt5aW5nZmVpLWRldjEUMBIGA1UECwwLeWluZ2ZlaS1k +ZXYxFDASBgNVBAMMC2V4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA1kewF2mRTNBmySUuOPKEJ+J8OMwEuQyOPczvS8U1K8GC2EH8Jcgk +8uAlqvl2POKyUPst7E4W1MHaHuNRn53JcvnLzBScwYLJdh+Y3Q65iiDWq/Km+S8j +gfOv5EcMVZWU3u32pSTuMudreeFCb6QHspUd9amOYEJwQr3iMBhodFIymKmB2tjG +b14dznm28+xP7X0iV9IU0PvyUNSAO4ntd/1FbOZSawpScaxZyNUl9UAD+1GwEacA +ednYTwBDlmhEKUHc0syRyGGVQU0OZlq1FWc+im8p3xyKb+6el5yeaXHTNFJ16ern +UXcjmEbKR6LT05cDQUvjMxFyLa+/Kz6zUQIDAQABo4GlMIGiMAkGA1UdEwQCMAAw +LAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0G +A1UdDgQWBBRaJzKd5zYko8HcL5WAxc8MhejmrzAfBgNVHSMEGDAWgBQNmWhPxWFV +vKwPzX2gb+iod1bUPTAnBgNVHREEIDAeggtleGFtcGxlLm9yZ4IPd3d3LmV4YW1w +bGUub3JnMA0GCSqGSIb3DQEBCwUAA4IBAQB3woFp8Bc64AkU1vo5SJ98y7zc7Nyg +xBD+g8r4DrCqWArRsCtLcvAreVSNXhhSygnqydT1qpnZCjq0GOtFWi5pb2cXwGLc +0/7jRXVM2bxg/cRQo5V+DsA+IWcmgxojoVT7wkNN5qUXjIABboD+8bv9a/hqavOq +ijd5dBEqTriaF33IJbF7gk35Va//j/glBMI6uPS+Ft5XPu8liQ3heJ/ulC4HXFaz +Elf9q6C2KE8Mx5YqIO6gAapF52Derkvj6ksJl6P6ToJODiYm//+xW+67f68a8nhQ +NX+/kQz6aJXT7/9MqDD7K/MqTKbVKsfbjQxAbSCet0jMiO1AFyBHoI8Y +-----END CERTIFICATE----- +`, +} + +func (t TestPemNameEnmu) Bytes() []byte { + return []byte(testkeys[t]) +} diff --git a/bfe_tls/bfe_testkeys_test.go b/bfe_tls/bfe_testkeys_test.go new file mode 100644 index 000000000..d8ddeb3a3 --- /dev/null +++ b/bfe_tls/bfe_testkeys_test.go @@ -0,0 +1,46 @@ +// Copyright (c) 2025 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package bfe_tls + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "testing" +) + +func TestKeysDecode(t *testing.T) { + chain := append(BFE_R_CA_CRT.Bytes(), BFE_I_CA_CRT.Bytes()...) + + // Decode pem raw data + var certs []*x509.Certificate + block, rest := pem.Decode(chain) + for block != nil { + if block.Type == "CERTIFICATE" { + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + fmt.Println("cannot parse certificate", err) + return + } + certs = append(certs, cert) + t.Log(cert.Subject) + } + if len(rest) > 0 { + block, rest = pem.Decode(rest) + } else { + break + } + } + +} diff --git a/bfe_tls/bfe_verify_hooks.go b/bfe_tls/bfe_verify_hooks.go new file mode 100644 index 000000000..909c9c8d5 --- /dev/null +++ b/bfe_tls/bfe_verify_hooks.go @@ -0,0 +1,275 @@ +// Copyright (c) 2025 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package bfe_tls + +import ( + "crypto/x509" + "fmt" + "net" + "strings" + "unicode/utf8" + + "github.com/baidu/go-lib/log" +) + +type VerifyCertHook func(insecureSkipVerify bool, host string, certificates []*x509.Certificate, cas *x509.CertPool) error + +type VerifyPeerCertHooks struct { + InsecureSkipVerify bool + Host string + certificates []*x509.Certificate + CAs *x509.CertPool + verifyFn []VerifyCertHook + disableDef bool +} + +func toLowerCaseASCII(in string) string { + // If the string is already lower-case then there's nothing to do. + isAlreadyLowerCase := true + for _, c := range in { + if c == utf8.RuneError { + // If we get a UTF-8 error then there might be + // upper-case ASCII bytes in the invalid sequence. + isAlreadyLowerCase = false + break + } + if 'A' <= c && c <= 'Z' { + isAlreadyLowerCase = false + break + } + } + + if isAlreadyLowerCase { + return in + } + + out := []byte(in) + for i, c := range out { + if 'A' <= c && c <= 'Z' { + out[i] += 'a' - 'A' + } + } + return string(out) +} +func matchExactly(hostA, hostB string) bool { + if hostA == "" || hostA == "." || hostB == "" || hostB == "." { + return false + } + return toLowerCaseASCII(hostA) == toLowerCaseASCII(hostB) +} + +func matchHostnames(pattern, host string) bool { + pattern = toLowerCaseASCII(pattern) + host = toLowerCaseASCII(strings.TrimSuffix(host, ".")) + + if len(pattern) == 0 || len(host) == 0 { + return false + } + + patternParts := strings.Split(pattern, ".") + hostParts := strings.Split(host, ".") + + if len(patternParts) != len(hostParts) { + return false + } + + for i, patternPart := range patternParts { + if i == 0 && patternPart == "*" { + continue + } + if patternPart != hostParts[i] { + return false + } + } + return true +} + +func validHostname(host string, isPattern bool) bool { + if !isPattern { + host = strings.TrimSuffix(host, ".") + } + if len(host) == 0 { + return false + } + for i, part := range strings.Split(host, ".") { + if part == "" { + // Empty label. + return false + } + if isPattern && i == 0 && part == "*" { + // Only allow full left-most wildcards, as those are the only ones + // we match, and matching literal '*' characters is probably never + // the expected behavior. + continue + } + for j, c := range part { + if 'a' <= c && c <= 'z' { + continue + } + if '0' <= c && c <= '9' { + continue + } + if 'A' <= c && c <= 'Z' { + continue + } + if c == '-' && j != 0 { + continue + } + if c == '_' { + // Not a valid character in hostnames, but commonly + // found in deployments outside the WebPKI. + continue + } + return false + } + } + return true +} +func verifyIpFn(c *x509.Certificate, h string) error { + candidateIP := h + if len(h) >= 3 && h[0] == '[' && h[len(h)-1] == ']' { + candidateIP = h[1 : len(h)-1] + } + if ip := net.ParseIP(candidateIP); ip != nil { + // We only match IP addresses against IP SANs. + // See RFC 6125, Appendix B.2. + for _, candidate := range c.IPAddresses { + if ip.Equal(candidate) { + return nil + } + } + return x509.HostnameError{Certificate: c, Host: candidateIP} + } + return nil +} + +// Only allow full left-most wildcards +func verifyHost(c *x509.Certificate, h string) error { + // Verify : SAN + names := c.DNSNames + if validHostname(c.Subject.CommonName, true) { + // Verify : CN + names = append(names, c.Subject.CommonName) + } + candidateName := toLowerCaseASCII(h) // Save allocations inside the loop. + validCandidateName := validHostname(candidateName, false) + for _, match := range names { + if validCandidateName && validHostname(match, true) { + if matchHostnames(match, candidateName) { + return nil + } + } else { + if matchExactly(match, candidateName) { + return nil + } + } + } + return x509.HostnameError{Certificate: c, Host: h} +} + +// NewVerifyPeerCertHooks : build hooks for bef_tls.Config.VerifyPeerCertificate and wait callback +func NewVerifyPeerCertHooks(insecureSkipVerify bool, host string, cas *x509.CertPool) *VerifyPeerCertHooks { + return &VerifyPeerCertHooks{ + InsecureSkipVerify: insecureSkipVerify, + Host: host, + CAs: cas, + verifyFn: make([]VerifyCertHook, 0), + } +} + +func (p *VerifyPeerCertHooks) DisableDefaultHooks() *VerifyPeerCertHooks { + p.disableDef = true + return p +} + +// Ready : ready to wait tls callback +func (p *VerifyPeerCertHooks) Ready() func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + if !p.disableDef { + p.verifyFn = append([]VerifyCertHook{defVerifyCA, defVerifyHost}, p.verifyFn...) + } + return p.verifyPeerCertificate +} + +func (p *VerifyPeerCertHooks) verifyPeerCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error { + + var certificates []*x509.Certificate + for _, rawCert := range rawCerts { + cert, err := x509.ParseCertificate(rawCert) + if err != nil { + return err + } + certificates = append(certificates, cert) + } + p.certificates = certificates + return p.apply() +} + +func (p *VerifyPeerCertHooks) AppendHook(fn VerifyCertHook) *VerifyPeerCertHooks { + p.verifyFn = append(p.verifyFn, fn) + return p +} + +func (p *VerifyPeerCertHooks) apply() error { + if !p.InsecureSkipVerify { + for _, fn := range p.verifyFn { + if err := fn(p.InsecureSkipVerify, p.Host, p.certificates, p.CAs); err != nil { + return err + } + } + } + return nil +} + +// defVerifyCA : verify cert by cas +func defVerifyCA(insecureSkipVerify bool, _ string, certificates []*x509.Certificate, cas *x509.CertPool) (err error) { + if !insecureSkipVerify { + for _, cert := range certificates { + if _, err = cert.Verify(x509.VerifyOptions{ + Roots: cas, + }); err != nil { + log.Logger.Debug("err=%s", err.Error()) + return err + } + log.Logger.Debug("HTTPS-Verify-CA-Success: %s", cert.Subject.String()) + } + } + return nil +} + +// defVerifyHost : verify host over CN and SAN +func defVerifyHost(insecureSkipVerify bool, host string, certificates []*x509.Certificate, _ *x509.CertPool) (err error) { + if !insecureSkipVerify { + for _, cert := range certificates { + var ( + err error + candidateIP = host + ) + if len(host) >= 3 && host[0] == '[' && host[len(host)-1] == ']' { + candidateIP = host[1 : len(host)-1] + } + if ip := net.ParseIP(candidateIP); ip != nil { + err = verifyIpFn(cert, host) + } else { + err = verifyHost(cert, host) + } + if err == nil { + return nil + } + log.Logger.Debug("debug_https not_match host=%s, sn=%s, cn=%s, san=%v, err=%v", host, cert.SerialNumber.String(), cert.Subject.CommonName, cert.DNSNames, err) + } + err = fmt.Errorf("host=%s not match CN/SAN", host) + return err + } + return nil +} diff --git a/bfe_tls/bfe_verify_hooks_test.go b/bfe_tls/bfe_verify_hooks_test.go new file mode 100644 index 000000000..302e638f5 --- /dev/null +++ b/bfe_tls/bfe_verify_hooks_test.go @@ -0,0 +1,113 @@ +// Copyright (c) 2025 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package bfe_tls + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "net" + "testing" + "time" +) + +// createCert create a cert with common name +func createCert(sn int64, cn string) *x509.Certificate { + template := &x509.Certificate{ + SerialNumber: big.NewInt(sn), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + BasicConstraintsValid: true, + } + priv, _ := rsa.GenerateKey(rand.Reader, 2048) + pub := &priv.PublicKey + certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, pub, priv) + cert, _ := x509.ParseCertificate(certBytes) + return cert +} + +// createCertWithSAN create a cert with SAN +func createCertWithSAN(sn int64, dnsNames []string, ips []net.IP) *x509.Certificate { + template := &x509.Certificate{ + SerialNumber: big.NewInt(sn), + Subject: pkix.Name{}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + BasicConstraintsValid: true, + DNSNames: dnsNames, + IPAddresses: ips, + } + priv, _ := rsa.GenerateKey(rand.Reader, 2048) + pub := &priv.PublicKey + certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, pub, priv) + cert, _ := x509.ParseCertificate(certBytes) + return cert +} + +func TestDefVerifyHost(t *testing.T) { + // _ = log.Init(fmt.Sprintf("test_%s", time.Now().String()), "DEBUG", "/tmp", true, "M", 5) + type testcase struct { + host string + cert *x509.Certificate + assertOk bool + } + + var ( + cnCert0 = createCert(1, "example.com") + cnCert1 = createCert(2, "*.example.com") + sanCert = createCertWithSAN(3, + []string{"www.example.com", "*.example.org"}, + []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("192.168.0.1")}, + ) + _testcase = map[string]testcase{ + "cn0_assert_ok": {"example.com", cnCert0, true}, + "cn0_assert_1_fail": {"login.example.com", cnCert0, false}, + "cn0_assert_2_fail": {"www.example.com", cnCert0, false}, + "cn1_assert_wildcard1_ok": {"login.example.com", cnCert1, true}, + "cn1_assert_wildcard2_ok": {"www.example.com", cnCert1, true}, + "cn1_assert_wildcard4_fail": {"example.com", cnCert1, false}, + "cn1_assert_wildcard5_fail": {"example.org", cnCert1, false}, + "cn1_assert_wildcard6_fail": {"www.login.example.org", cnCert1, false}, + + "san_assert_wildcard1_fail": {"example.com", sanCert, false}, + "san_assert_wildcard2_fail": {"login.example.com", sanCert, false}, + "san_assert_wildcard3_ok": {"www.example.com", sanCert, true}, + "san_assert_wildcard4_ok": {"login.example.org", sanCert, true}, + "san_assert_wildcard5_ok": {"www.example.org", sanCert, true}, + "san_assert_wildcard6_fail": {"example.org", sanCert, false}, + + "san_assert_wildcard7_ok": {"127.0.0.1", sanCert, true}, + "san_assert_wildcard8_ok": {"192.168.0.1", sanCert, true}, + "san_assert_wildcard9_fail": {"192.168.0.2", sanCert, false}, + } + fn = func(tc testcase) func(t *testing.T) { + return func(t *testing.T) { + err := defVerifyHost(false, tc.host, []*x509.Certificate{tc.cert}, nil) + if tc.assertOk && err != nil { + t.Error(err) + } else if !tc.assertOk && err == nil { + t.Error("assertOk=false, err==nil") + } + } + } + ) + // execute test cases + for name, val := range _testcase { + t.Run(name, fn(val)) + } + time.Sleep(time.Second) +} diff --git a/bfe_tls/common.go b/bfe_tls/common.go index 46e84aa0a..69c9c87fe 100644 --- a/bfe_tls/common.go +++ b/bfe_tls/common.go @@ -29,9 +29,7 @@ import ( "strings" "sync" "time" -) -import ( "golang.org/x/crypto/ocsp" ) @@ -60,7 +58,9 @@ const ( // Grade A+: no ssl3, tls1.0, tls1.1 && no RC4 ciphers // Grade A: no ssl3 && no RC4 ciphers // Grade B: ssl3 is ok only with RC4 cipher, or -// modern version(>=tls10) with no RC4 cipher +// +// modern version(>=tls10) with no RC4 cipher +// // Grade C: ssl3 is ok only with RC4 cipher const ( GradeAPlus = "A+" @@ -79,8 +79,8 @@ const ( * http://chimera.labs.oreilly.com/books/1230000000545/ch04.html#TLS_RECORD_SIZE */ var ( - initPlaintext int = minPlaintext // initial length of plaintext payload - bytesThreshold int = 1024 * 1024 // 1 MB + initPlaintext int = minPlaintext // initial length of plaintext payload + bytesThreshold int = 1024 * 1024 // 1 MB inactiveSeconds time.Duration = time.Duration(1 * time.Second) // 1 second ) @@ -349,6 +349,19 @@ type Config struct { // for all connections. NameToCertificate map[string]*Certificate + // VerifyPeerCertificate, if not nil, is called after normal + // certificate verification by either a TLS client or server. It + // receives the raw ASN.1 certificates provided by the peer and also + // any verified chains that normal processing found. If it returns a + // non-nil error, the handshake is aborted and that error results. + // + // If normal verification fails then the handshake will abort before + // considering this callback. If normal verification is disabled by + // setting InsecureSkipVerify, or (for a server) when ClientAuth is + // RequestClientCert or RequireAnyClientCert, then this callback will + // be considered but the verifiedChains argument will always be nil. + VerifyPeerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error + // default multiply certificates policy for tls server MultiCert MultiCertificate @@ -466,6 +479,7 @@ func (c *Config) Clone() *Config { Time: c.Time, Certificates: c.Certificates, NameToCertificate: c.NameToCertificate, + VerifyPeerCertificate: c.VerifyPeerCertificate, MultiCert: c.MultiCert, RootCAs: c.RootCAs, NextProtos: c.NextProtos, diff --git a/bfe_tls/handshake_client.go b/bfe_tls/handshake_client.go index 383cb2846..dfe6eaee9 100644 --- a/bfe_tls/handshake_client.go +++ b/bfe_tls/handshake_client.go @@ -268,6 +268,13 @@ func (hs *clientHandshakeState) doFullHandshake() error { } } + if c.config.VerifyPeerCertificate != nil { + if err := c.config.VerifyPeerCertificate(certMsg.certificates, c.verifiedChains); err != nil { + c.sendAlert(alertBadCertificate) + return err + } + } + switch certs[0].PublicKey.(type) { case *rsa.PublicKey, *ecdsa.PublicKey: break diff --git a/bfe_tls/handshake_server.go b/bfe_tls/handshake_server.go index c9eb3cd74..5d9fed2f3 100644 --- a/bfe_tls/handshake_server.go +++ b/bfe_tls/handshake_server.go @@ -932,8 +932,8 @@ func (hs *serverHandshakeState) processCertsFromClient(certificates [][]byte) (c c.verifiedChains = chains } + var pub crypto.PublicKey if len(certs) > 0 { - var pub crypto.PublicKey switch key := certs[0].PublicKey.(type) { case *ecdsa.PublicKey, *rsa.PublicKey: pub = key @@ -945,7 +945,14 @@ func (hs *serverHandshakeState) processCertsFromClient(certificates [][]byte) (c return pub, nil } - return nil, nil + if c.config.VerifyPeerCertificate != nil { + if err := c.config.VerifyPeerCertificate(certificates, c.verifiedChains); err != nil { + c.sendAlert(alertBadCertificate) + return nil, err + } + } + + return pub, nil } // tryCipherSuite returns a cipherSuite with the given id if that cipher suite diff --git a/conf/cluster_conf/cluster_table.data b/conf/cluster_conf/cluster_table.data index 4006823dc..f162dca2a 100644 --- a/conf/cluster_conf/cluster_table.data +++ b/conf/cluster_conf/cluster_table.data @@ -10,6 +10,16 @@ } ] }, + "https_cluster_example": { + "example.bfe.bj": [ + { + "Addr": "127.0.0.1", + "Name": "https_example_hostname", + "Port": 9443, + "Weight": 10 + } + ] + }, "fcgi_cluster_example": { "fcgi.example.bfe.bj": [ { diff --git a/conf/cluster_conf/gslb.data b/conf/cluster_conf/gslb.data index 67ba6f419..8b4f7c281 100644 --- a/conf/cluster_conf/gslb.data +++ b/conf/cluster_conf/gslb.data @@ -4,6 +4,10 @@ "GSLB_BLACKHOLE": 0, "example.bfe.bj": 100 }, + "https_cluster_example": { + "GSLB_BLACKHOLE": 0, + "example.bfe.bj": 100 + }, "fcgi_cluster_example": { "GSLB_BLACKHOLE": 0, "fcgi.example.bfe.bj": 100 diff --git a/conf/server_data_conf/cluster_conf.data b/conf/server_data_conf/cluster_conf.data index a23dfe602..0c03f3c67 100644 --- a/conf/server_data_conf/cluster_conf.data +++ b/conf/server_data_conf/cluster_conf.data @@ -35,6 +35,51 @@ "CancelOnClientClose": false } }, + "https_cluster_example": { + "BackendConf": { + "Protocol": "https", + "TimeoutConnSrv": 2000, + "TimeoutResponseHeader": 50000, + "MaxIdleConnsPerHost": 0, + "RetryLevel": 0 + }, + "CheckConf": { + "Schem": "https", + "Uri": "/", + "Host": "example.org", + "StatusCode": 200, + "FailNum": 10, + "CheckInterval": 1000 + }, + "GslbBasic": { + "CrossRetry": 0, + "RetryMax": 2, + "HashConf": { + "HashStrategy": 0, + "HashHeader": "Cookie:UID", + "SessionSticky": false + } + }, + "ClusterBasic": { + "TimeoutReadClient": 30000, + "TimeoutWriteClient": 60000, + "TimeoutReadClientAgain": 30000, + "ReqWriteBufferSize": 512, + "ReqFlushInterval": 0, + "ResFlushInterval": -1, + "CancelOnClientClose": false + }, + "HTTPSConf":{ + "RSHost": "www.example.org", + "BFEKeyFile": "../conf/tls_conf/backend_rs/r_bfe_dev_prv.pem", + "BFECertFile": "../conf/tls_conf/backend_rs/r_bfe_dev.crt", + "RSCAList": [ + "../conf/tls_conf/backend_rs/bfe_r_ca.crt", + "../conf/tls_conf/backend_rs/bfe_i_ca.crt" + ], + "RSInsecureSkipVerify": false + } + }, "h2c_cluster_example": { "BackendConf": { "Protocol": "h2c", diff --git a/conf/server_data_conf/host_rule.data b/conf/server_data_conf/host_rule.data index 734d675f7..28c485f8d 100644 --- a/conf/server_data_conf/host_rule.data +++ b/conf/server_data_conf/host_rule.data @@ -4,6 +4,7 @@ "Hosts": { "exampleTag":[ "example.org", + "www.example.org", "fcgi.example.org", "h2c.example.org" ], diff --git a/conf/server_data_conf/route_rule.data b/conf/server_data_conf/route_rule.data index 40e9710a2..987124747 100644 --- a/conf/server_data_conf/route_rule.data +++ b/conf/server_data_conf/route_rule.data @@ -6,6 +6,10 @@ "Cond": "req_host_in(\"example.org\")", "ClusterName": "cluster_example" }, + { + "Cond": "req_host_in(\"www.example.org\")", + "ClusterName": "https_cluster_example" + }, { "Cond": "req_host_in(\"fcgi.example.org\")", "ClusterName": "fcgi_cluster_example" diff --git a/conf/tls_conf/backend_rs/bfe_i_ca.crt b/conf/tls_conf/backend_rs/bfe_i_ca.crt new file mode 100644 index 000000000..8b5eeee95 --- /dev/null +++ b/conf/tls_conf/backend_rs/bfe_i_ca.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDwDCCAqigAwIBAgIBCzANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJjbjEQ +MA4GA1UECAwHYmVpamluZzEUMBIGA1UECgwLeWluZ2ZlaS1kZXYxFDASBgNVBAsM +C3lpbmdmZWktZGV2MRcwFQYDVQQDDA55aW5nZmVpLWRldi1jYTAeFw0yMzExMDMx +NDAxNDZaFw0zNzA3MTIxNDAxNDZaMIGQMQswCQYDVQQGEwJjbjEQMA4GA1UECAwH +YmVpamluZzEUMBIGA1UECgwLeWluZ2ZlaS1kZXYxFDASBgNVBAsMC3lpbmdmZWkt +ZGV2MRgwFgYDVQQDDA95aW5nZmVpLWRldi1pY2ExKTAnBgkqhkiG9w0BCQEWGmxp +YW5nY2h1YW5AeWYtbmV0d29ya3MuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA6eFuWgoknixrRO9NCX4jAKyLtcAOWVJqVN2yX7CxjZjLyPurjvTZ +W73NYUbrPAN4AB5gY6UAPzuiEOSopVVIZ0OschK0cJldu9vZ0mZObBOsovuFcLQq +dgNSJ5slJSk7tCgD2EnCB3GYPG4D+uIKYd0c49wzTWWv4bjDwpgnf0LQbFpy7GhN +7D59zFH4qgOK/IQ5vaTMGyvIvtWR5/1Gvc9MLpGopTgi0DiNLed4UwDYrod5kysl +q3UcB5puONHQISOVoD3uRxo7wdsmVsHUfW7YfAWkhi6ec8mx9fy8IyE6f7GlXnuV +ysNccwqyEotL5bOXPJCqwUCL1v+iKTMPWwIDAQABo1AwTjAdBgNVHQ4EFgQUCjvC +vDVr8QXh9/hMI5xBgNvoSFgwHwYDVR0jBBgwFoAUDZloT8VhVbysD819oG/oqHdW +1D0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAhIggK/6JK4N7+HZW +VoTemwRpilugZZyKrcVAHbiiwUfXVQVuI64vc+yHWMSFRD1mykHkKBFzxEoDabMl +ASBtUJNt4b4zEL9V7k295vmAOp2IdLUQxlgeKWqABm4DGX96tGR9nKQTcn1ZeAxx +NyQFV1aj+dnNcF1iFFNF6t0bRrOEZ/aRSiu1bWp3Dj1JYTjXbyyplh3Ktb8lv4lt +EmvCXjo/l4TgQC9233kcTkXcq1swppzkkXfhB0NVuf9DE3C2xAWX0b2FiIEqnAhl +Bx0Cn3RrX7PZ6qrdRL6oBKvGJV6DP8BlF4LXiuD1VN/waQFJha57KQ78ZYYIXxQg +WMjWgA== +-----END CERTIFICATE----- diff --git a/conf/tls_conf/backend_rs/bfe_r_ca.crt b/conf/tls_conf/backend_rs/bfe_r_ca.crt new file mode 100644 index 000000000..62065c47b --- /dev/null +++ b/conf/tls_conf/backend_rs/bfe_r_ca.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDkzCCAnugAwIBAgIBCjANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJjbjEQ +MA4GA1UECAwHYmVpamluZzEUMBIGA1UECgwLeWluZ2ZlaS1kZXYxFDASBgNVBAsM +C3lpbmdmZWktZGV2MRcwFQYDVQQDDA55aW5nZmVpLWRldi1jYTAeFw0yMzExMDMx +MzQ5NTVaFw0zNzA3MTIxMzQ5NTVaMGQxCzAJBgNVBAYTAmNuMRAwDgYDVQQIDAdi +ZWlqaW5nMRQwEgYDVQQKDAt5aW5nZmVpLWRldjEUMBIGA1UECwwLeWluZ2ZlaS1k +ZXYxFzAVBgNVBAMMDnlpbmdmZWktZGV2LWNhMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAvNA3HrsMjBcXrMIIhGVWsurIA1F9jxKeA7dh06H00Vt4inVV +SUvNFrTqgPRhLkAhGRMPxrjVRgJ5bbFqqXIuPIpUFBhUsWXIDH+oVXQl9jsxAXaG +gZ0lTO/uYR9qyrS1rj9nyNPwRf59Al/VlsQL71cNQ/T/agJ4PfvPfULTPLOsqclJ +hj0IgXmDj464dqcdG3ZdfXpfhNF6ab+8YjpwafTRmY+LoV8qjUwsYeJMcW4N8pxJ +8F2ktZj9J6uWepNGj+87ZeXg9XquzC62ASIFzPjoE1WN//Q518EqizxhuLGBDpK3 +6sEYUK4kHYUL4gZKFPlKTPXIl0ZsJIM5PsBMKwIDAQABo1AwTjAdBgNVHQ4EFgQU +DZloT8VhVbysD819oG/oqHdW1D0wHwYDVR0jBBgwFoAUDZloT8VhVbysD819oG/o +qHdW1D0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEALgB+IcN3UD4T ++4g8jaOyOiSUpUVdYqW+3go5DppqnvlGNq99N2cXKqssVPa/T9TikEcBEicFa8zU +bwlx6TEte+MkWfWdQxFR1EuI1FgKr3ps6ZBRr7MPpnYmI7K9aK372K9n7WrQhbmP +s7ult8bWB1/t6o3R7B9ChNkWT+7DPD4+FvB1GMJSGPno7cdnvDkevBOuC2DnQl3M ++ADFAge1Lo8wKBy6gYkNFd2BfarHGvRC5Qmmrme+RIpWZnvux1+lfXIInnfSTJRM +uAo/ePkoNsM3qQll6uEdhDxOMx8Pq94bCkM3DtI3ObuxWXjKCgUm/n9yetUvPj4U +iYhRUqHpUg== +-----END CERTIFICATE----- diff --git a/conf/tls_conf/backend_rs/r_bfe_dev.crt b/conf/tls_conf/backend_rs/r_bfe_dev.crt new file mode 100644 index 000000000..dbbb7e51a --- /dev/null +++ b/conf/tls_conf/backend_rs/r_bfe_dev.crt @@ -0,0 +1,85 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 18 (0x12) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=cn, ST=beijing, O=yingfei-dev, OU=yingfei-dev, CN=yingfei-dev-ca + Validity + Not Before: Jan 31 02:55:32 2024 GMT + Not After : Jan 28 02:55:32 2034 GMT + Subject: C=cn, ST=beijing, O=yingfei-dev, OU=yingfei-dev, CN=bfe-dev/emailAddress=dev@example.org + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:b2:7f:c1:0c:cd:49:43:d9:99:78:15:4c:5a:52: + a9:a7:bf:d5:eb:92:71:43:e1:37:e5:29:1b:68:f4: + 6f:4c:ea:fa:a4:8a:7c:29:01:2a:fa:7c:81:5b:c8: + eb:a1:40:94:b7:2b:82:e2:00:08:36:84:f0:b7:d2: + 5a:1e:56:97:aa:36:ff:4d:07:49:2d:fe:25:3a:f9: + e9:f1:ad:4e:6e:21:97:a9:f9:a2:a5:ac:82:23:0a: + d2:e0:97:cd:2f:14:b1:f0:8a:a8:e6:c5:97:45:94: + f8:8e:3f:96:66:5b:0b:8c:7c:07:61:18:44:92:f7: + 23:5a:c2:4b:88:58:59:5d:ca:5c:0d:6e:dd:ff:18: + 59:65:df:95:99:e3:3a:36:48:1f:3f:3a:e6:ce:85: + f3:0b:04:5e:92:ed:6f:8e:74:92:e4:37:46:da:5f: + 17:62:9c:82:40:06:fb:29:f8:55:f2:ba:23:75:ca: + 64:c0:45:03:12:bd:f5:17:15:7e:47:d5:bd:30:f2: + 99:ca:6b:e3:07:b0:ae:44:89:1e:10:26:ea:75:df: + 6f:07:b6:47:76:54:47:4f:6c:f6:68:fe:a8:cf:22: + 20:73:e8:19:55:8a:fe:f5:78:e8:51:88:52:80:1f: + 79:d4:c5:ae:8f:d2:2b:f6:41:01:42:01:cf:98:c2: + c9:25 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + EE:E8:68:42:DF:B1:F0:EF:6F:47:51:BD:D4:94:60:1F:05:85:A1:03 + X509v3 Authority Key Identifier: + keyid:0D:99:68:4F:C5:61:55:BC:AC:0F:CD:7D:A0:6F:E8:A8:77:56:D4:3D + + X509v3 Extended Key Usage: + TLS Web Client Authentication + Signature Algorithm: sha256WithRSAEncryption + a9:a6:26:8e:42:61:15:22:ee:fc:b5:e1:e4:6b:dd:ac:f5:15: + 11:39:10:9a:ca:6f:85:fd:cb:90:1c:b2:4f:fe:29:de:b0:e7: + 73:e2:f7:5e:63:8c:7f:c1:7e:75:2e:c9:9d:e9:c2:45:75:f3: + 27:ba:82:94:de:7f:6c:87:0c:5c:71:af:0f:14:00:68:35:f7: + 5a:4a:ff:f5:ef:35:dd:50:72:76:f0:6f:b6:7b:42:33:07:b4: + 24:44:0a:fd:9d:61:9e:44:e8:88:0f:02:76:c6:90:3f:9d:1b: + d8:3b:64:25:2a:a3:39:78:38:bd:20:89:4a:9c:bd:68:38:18: + 4c:cb:20:3a:9b:5b:5f:58:52:86:73:de:85:fe:d6:a1:c6:a7: + 86:b0:96:4b:fa:28:04:ad:5d:85:e8:a1:fc:ca:0f:3c:be:5c: + 90:7e:3e:84:ae:67:ee:9a:72:71:3c:b2:80:45:82:fc:7e:58: + 74:99:42:c5:c3:8a:4a:eb:e1:8b:d5:84:ce:25:aa:a1:75:79: + 94:66:ae:ee:df:30:15:0b:b5:c5:b1:2c:d5:0a:54:78:b6:2e: + 67:29:81:41:f6:16:49:31:96:e7:41:e1:99:6b:27:57:bb:7d: + 76:eb:e4:d5:59:aa:a2:5c:bd:1c:18:2a:fa:9d:28:1a:0b:b6: + bf:7d:58:1a +-----BEGIN CERTIFICATE----- +MIID7jCCAtagAwIBAgIBEjANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJjbjEQ +MA4GA1UECAwHYmVpamluZzEUMBIGA1UECgwLeWluZ2ZlaS1kZXYxFDASBgNVBAsM +C3lpbmdmZWktZGV2MRcwFQYDVQQDDA55aW5nZmVpLWRldi1jYTAeFw0yNDAxMzEw +MjU1MzJaFw0zNDAxMjgwMjU1MzJaMH0xCzAJBgNVBAYTAmNuMRAwDgYDVQQIDAdi +ZWlqaW5nMRQwEgYDVQQKDAt5aW5nZmVpLWRldjEUMBIGA1UECwwLeWluZ2ZlaS1k +ZXYxEDAOBgNVBAMMB2JmZS1kZXYxHjAcBgkqhkiG9w0BCQEWD2RldkBleGFtcGxl +Lm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ/wQzNSUPZmXgV +TFpSqae/1euScUPhN+UpG2j0b0zq+qSKfCkBKvp8gVvI66FAlLcrguIACDaE8LfS +Wh5Wl6o2/00HSS3+JTr56fGtTm4hl6n5oqWsgiMK0uCXzS8UsfCKqObFl0WU+I4/ +lmZbC4x8B2EYRJL3I1rCS4hYWV3KXA1u3f8YWWXflZnjOjZIHz865s6F8wsEXpLt +b450kuQ3RtpfF2KcgkAG+yn4VfK6I3XKZMBFAxK99RcVfkfVvTDymcpr4wewrkSJ +HhAm6nXfbwe2R3ZUR09s9mj+qM8iIHPoGVWK/vV46FGIUoAfedTFro/SK/ZBAUIB +z5jCySUCAwEAAaOBkTCBjjAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVu +U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQU7uhoQt+x8O9vR1G9 +1JRgHwWFoQMwHwYDVR0jBBgwFoAUDZloT8VhVbysD819oG/oqHdW1D0wEwYDVR0l +BAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADggEBAKmmJo5CYRUi7vy14eRr +3az1FRE5EJrKb4X9y5Acsk/+Kd6w53Pi915jjH/BfnUuyZ3pwkV18ye6gpTef2yH +DFxxrw8UAGg191pK//XvNd1Qcnbwb7Z7QjMHtCRECv2dYZ5E6IgPAnbGkD+dG9g7 +ZCUqozl4OL0giUqcvWg4GEzLIDqbW19YUoZz3oX+1qHGp4awlkv6KAStXYXoofzK +Dzy+XJB+PoSuZ+6acnE8soBFgvx+WHSZQsXDikrr4YvVhM4lqqF1eZRmru7fMBUL +tcWxLNUKVHi2LmcpgUH2FkkxludB4ZlrJ1e7fXbr5NVZqqJcvRwYKvqdKBoLtr99 +WBo= +-----END CERTIFICATE----- diff --git a/conf/tls_conf/backend_rs/r_bfe_dev_prv.pem b/conf/tls_conf/backend_rs/r_bfe_dev_prv.pem new file mode 100644 index 000000000..c576080b6 --- /dev/null +++ b/conf/tls_conf/backend_rs/r_bfe_dev_prv.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAsn/BDM1JQ9mZeBVMWlKpp7/V65JxQ+E35SkbaPRvTOr6pIp8 +KQEq+nyBW8jroUCUtyuC4gAINoTwt9JaHlaXqjb/TQdJLf4lOvnp8a1ObiGXqfmi +payCIwrS4JfNLxSx8Iqo5sWXRZT4jj+WZlsLjHwHYRhEkvcjWsJLiFhZXcpcDW7d +/xhZZd+VmeM6NkgfPzrmzoXzCwReku1vjnSS5DdG2l8XYpyCQAb7KfhV8rojdcpk +wEUDEr31FxV+R9W9MPKZymvjB7CuRIkeECbqdd9vB7ZHdlRHT2z2aP6ozyIgc+gZ +VYr+9XjoUYhSgB951MWuj9Ir9kEBQgHPmMLJJQIDAQABAoIBAFAKHzupVb/58+o3 +yqv5wx94Uuk2GlnwxIqaezL94GaiO0/K1U/huS7m426P0rDU75qPBTpn/0bLJ9GV +nllaRNnLnYEh0juwaWtfovp+1ttlbseGK9uUVip2cQbKqvQAmKWe14vbcDCAU1Ad +zUgKbUxKVVjBdAZekVjiJNJ3o2L9WPhf/uQo7A1XAJh2DajlTbgvrDM73W+47QuU +X4OHU0FMio6bxupu3OWl1bMrnKhuC4qczZWf2nOpcVQa89rtopuP4ENLJuWkbeGk +YQpNilEclnAa/Noumt/j/6GKC1EEHFsH2CNRRIazcZrsFhkSKc4pn1Y/WI3vj8kZ ++RYnJsUCgYEA6gr0x8suwRTAmVpoPk4XyP1x+eInG7onV5RjgsUtgruYd9naxRHg +2p8PHcv32pDs51Fa+4RldyMd/jec1SscRF5/+VOP9qeoRnaDqUu+uASgEO3OBxbP +JcWovyxRHIQxbYCQtqIr9bdzXw55MBLZou/sBUVTAkrIyPVyjWqZlV8CgYEAwz7L +YyYN615TsrzZKURxMjj94Nmob/NldSLRXaR3Ax7/ABtEOA685cwQxq7ONdkJTMIA +uR8u2GHZSzGiWnehuF6Zp7Xs71a57eFbs3ueZvvEba4Dff7hl7Y4tTlwrKndKjvP +J/5a2Ol8siQcRWAXHOdzggEHMSZ/sB4hWswly/sCgYEAhLRBpyemEwTZUBrbELjm +86gBgFajJi2fMSGKaxOygnYsNYjpauSAQnX99D87Aks6iM6wb/zaK3tV/lc6LgSL +uph6p7yh3JGj8JAyh0PTmDPHLtIoCAz+18QDsqJGO40ZGaXUaDn8Aw9J85QZUxDd +Jm4zvalZL+uHfarukRDolLECgYBUiupS4nWAh3XCnZeDEQna72avaFBROZmjIRJ7 +c+28wj009JmTlH4jGzvgbG0KUBKA1Div8Fq+g5AtyS498jNqvDvYrSQNdwZHhR/K +Fis++KHTxFfqxOU2Zkcj4d1yRpNn6EIJVVBNQL0n/g7n03XupCIWFw/gLoV343QZ +9vAe5QKBgA8ml59z1w3eUooc0yGfLhihXqCmM3IU006bFbODA30fBU4QKrHO8+Yx +Xbz9bi/1QLagLG6FzYQAkOjBlEBt5XLayYvwSb8xvWsm5A3vzTAbMFDOsDPEmRoH +dWtQccJcygOuK+PZtoZnNoJciNO5c9dZWD3xtIiURmX/kVtWrf6N +-----END RSA PRIVATE KEY----- diff --git a/conf/tls_conf/backend_rs/r_san_example.org.crt b/conf/tls_conf/backend_rs/r_san_example.org.crt new file mode 100644 index 000000000..cb1ba8d94 --- /dev/null +++ b/conf/tls_conf/backend_rs/r_san_example.org.crt @@ -0,0 +1,85 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 15 (0xf) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=cn, ST=beijing, O=yingfei-dev, OU=yingfei-dev, CN=yingfei-dev-ca + Validity + Not Before: Dec 7 09:57:42 2023 GMT + Not After : Dec 4 09:57:42 2033 GMT + Subject: C=cn, ST=beijing, O=yingfei-dev, OU=yingfei, CN=dev-test + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:d6:47:b0:17:69:91:4c:d0:66:c9:25:2e:38:f2: + 84:27:e2:7c:38:cc:04:b9:0c:8e:3d:cc:ef:4b:c5: + 35:2b:c1:82:d8:41:fc:25:c8:24:f2:e0:25:aa:f9: + 76:3c:e2:b2:50:fb:2d:ec:4e:16:d4:c1:da:1e:e3: + 51:9f:9d:c9:72:f9:cb:cc:14:9c:c1:82:c9:76:1f: + 98:dd:0e:b9:8a:20:d6:ab:f2:a6:f9:2f:23:81:f3: + af:e4:47:0c:55:95:94:de:ed:f6:a5:24:ee:32:e7: + 6b:79:e1:42:6f:a4:07:b2:95:1d:f5:a9:8e:60:42: + 70:42:bd:e2:30:18:68:74:52:32:98:a9:81:da:d8: + c6:6f:5e:1d:ce:79:b6:f3:ec:4f:ed:7d:22:57:d2: + 14:d0:fb:f2:50:d4:80:3b:89:ed:77:fd:45:6c:e6: + 52:6b:0a:52:71:ac:59:c8:d5:25:f5:40:03:fb:51: + b0:11:a7:00:79:d9:d8:4f:00:43:96:68:44:29:41: + dc:d2:cc:91:c8:61:95:41:4d:0e:66:5a:b5:15:67: + 3e:8a:6f:29:df:1c:8a:6f:ee:9e:97:9c:9e:69:71: + d3:34:52:75:e9:ea:e7:51:77:23:98:46:ca:47:a2: + d3:d3:97:03:41:4b:e3:33:11:72:2d:af:bf:2b:3e: + b3:51 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 5A:27:32:9D:E7:36:24:A3:C1:DC:2F:95:80:C5:CF:0C:85:E8:E6:AF + X509v3 Authority Key Identifier: + keyid:0D:99:68:4F:C5:61:55:BC:AC:0F:CD:7D:A0:6F:E8:A8:77:56:D4:3D + + X509v3 Subject Alternative Name: + DNS:example.org, DNS:www.example.org, DNS:example.com, DNS:*.example.com, IP Address:127.0.0.1, IP Address:192.168.0.100 + Signature Algorithm: sha256WithRSAEncryption + 1e:e8:e8:8a:ad:a8:0e:fc:c9:82:00:a1:ab:30:3c:a5:b9:dc: + d6:fb:86:ad:30:52:7f:61:be:90:a6:b8:56:bb:f1:0b:e6:39: + 38:65:09:6b:da:83:f7:65:ff:c4:21:de:b4:9e:8b:bd:1e:1c: + d1:d5:94:b8:18:79:f2:d0:06:51:39:67:13:40:3b:73:5b:cb: + ea:de:c1:19:76:f8:7b:0f:15:51:61:49:fb:98:f7:ea:4f:fc: + c2:fb:a7:f4:3c:48:64:14:79:b5:78:5b:20:10:b5:7a:2d:4c: + 04:51:60:ec:20:10:19:26:5f:e2:fd:32:59:67:e9:3f:48:8d: + f5:52:12:01:81:2c:c0:e5:72:cd:7d:0a:eb:7a:05:df:a0:77: + b9:ba:9a:7d:d1:4b:6a:44:e4:2d:98:af:bd:77:2b:f5:ef:26: + 4b:75:b3:97:d0:3a:bc:07:21:ef:71:92:30:fe:a2:79:e5:56: + d7:7e:c2:f3:57:ab:d7:de:fc:97:ed:20:0c:9a:cb:c5:5d:00: + 3b:61:29:e8:00:d4:39:e0:f2:4e:a4:03:c2:12:52:ff:e7:78: + f9:f7:c0:12:dc:36:a4:05:a2:f0:6b:47:e2:21:3d:a2:e1:a1: + 91:c7:ac:8f:b8:ae:58:65:e0:2b:57:80:eb:77:2d:48:ef:e6: + fb:b9:e1:20 +-----BEGIN CERTIFICATE----- +MIIEBzCCAu+gAwIBAgIBDzANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJjbjEQ +MA4GA1UECAwHYmVpamluZzEUMBIGA1UECgwLeWluZ2ZlaS1kZXYxFDASBgNVBAsM +C3lpbmdmZWktZGV2MRcwFQYDVQQDDA55aW5nZmVpLWRldi1jYTAeFw0yMzEyMDcw +OTU3NDJaFw0zMzEyMDQwOTU3NDJaMFoxCzAJBgNVBAYTAmNuMRAwDgYDVQQIDAdi +ZWlqaW5nMRQwEgYDVQQKDAt5aW5nZmVpLWRldjEQMA4GA1UECwwHeWluZ2ZlaTER +MA8GA1UEAwwIZGV2LXRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQDWR7AXaZFM0GbJJS448oQn4nw4zAS5DI49zO9LxTUrwYLYQfwlyCTy4CWq+XY8 +4rJQ+y3sThbUwdoe41Gfncly+cvMFJzBgsl2H5jdDrmKINar8qb5LyOB86/kRwxV +lZTe7falJO4y52t54UJvpAeylR31qY5gQnBCveIwGGh0UjKYqYHa2MZvXh3Oebbz +7E/tfSJX0hTQ+/JQ1IA7ie13/UVs5lJrClJxrFnI1SX1QAP7UbARpwB52dhPAEOW +aEQpQdzSzJHIYZVBTQ5mWrUVZz6KbynfHIpv7p6XnJ5pcdM0UnXp6udRdyOYRspH +otPTlwNBS+MzEXItr78rPrNRAgMBAAGjgc0wgcowCQYDVR0TBAIwADAsBglghkgB +hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE +FFonMp3nNiSjwdwvlYDFzwyF6OavMB8GA1UdIwQYMBaAFA2ZaE/FYVW8rA/NfaBv +6Kh3VtQ9ME8GA1UdEQRIMEaCC2V4YW1wbGUub3Jngg93d3cuZXhhbXBsZS5vcmeC +C2V4YW1wbGUuY29tgg0qLmV4YW1wbGUuY29thwR/AAABhwTAqABkMA0GCSqGSIb3 +DQEBCwUAA4IBAQAe6OiKragO/MmCAKGrMDyludzW+4atMFJ/Yb6QprhWu/EL5jk4 +ZQlr2oP3Zf/EId60nou9HhzR1ZS4GHny0AZROWcTQDtzW8vq3sEZdvh7DxVRYUn7 +mPfqT/zC+6f0PEhkFHm1eFsgELV6LUwEUWDsIBAZJl/i/TJZZ+k/SI31UhIBgSzA +5XLNfQrregXfoHe5upp90UtqROQtmK+9dyv17yZLdbOX0Dq8ByHvcZIw/qJ55VbX +fsLzV6vX3vyX7SAMmsvFXQA7YSnoANQ54PJOpAPCElL/53j598AS3DakBaLwa0fi +IT2i4aGRx6yPuK5YZeArV4Drdy1I7+b7ueEg +-----END CERTIFICATE----- diff --git a/conf/tls_conf/backend_rs/r_san_example.org_prv.pem b/conf/tls_conf/backend_rs/r_san_example.org_prv.pem new file mode 100644 index 000000000..b19d3d4c8 --- /dev/null +++ b/conf/tls_conf/backend_rs/r_san_example.org_prv.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA1kewF2mRTNBmySUuOPKEJ+J8OMwEuQyOPczvS8U1K8GC2EH8 +Jcgk8uAlqvl2POKyUPst7E4W1MHaHuNRn53JcvnLzBScwYLJdh+Y3Q65iiDWq/Km ++S8jgfOv5EcMVZWU3u32pSTuMudreeFCb6QHspUd9amOYEJwQr3iMBhodFIymKmB +2tjGb14dznm28+xP7X0iV9IU0PvyUNSAO4ntd/1FbOZSawpScaxZyNUl9UAD+1Gw +EacAednYTwBDlmhEKUHc0syRyGGVQU0OZlq1FWc+im8p3xyKb+6el5yeaXHTNFJ1 +6ernUXcjmEbKR6LT05cDQUvjMxFyLa+/Kz6zUQIDAQABAoIBAC4sYGuLGf49Ygix +9FXdHFEj4rSyccoWRIhYoq/nHOAC4NkMzvKtQBj95+ABxVK1XstIdMrYwN6zrva8 +8Re9/mzCGwIs5uJj9ll30Y7A34Y+MUP4E7baS4JzKlG8ZZIDm4K2MFHBtXpOl8A5 +pAE+jVIUA9Kt6LohVuNq21SVzdxSfNYC/+SLqSftkWa/ZsqdkiHM5Hl+fVedh516 +IaLNW5hSthGh5n8dHY5h/AKPjfoq77aYp5/CUtJTC9mYdZu1j/W/pBVTRfOnwLQd +SQ1Xmr7f6q9Vmz+HnajIbFg9hQ54blvtUJ7DnugWxfUcoxf7ue79fnYjOIUOkRWw +8Iid/mECgYEA+5s0p0j+gkNZN5QtVNStoT04+1DqA1O381gczeaiz6Njt/MT0y5W +OpCsILQ70CpEjWAV+f6PDJiesDMxdGV+v2TCqK8ml8GahEczBLnHoAkPWCVf2XOX +oNj/CkZ2kmWufHFR+kcQbeDt1vFFcYUa61hKDFyNjinMW79Qy+PQID0CgYEA2gWd +7thE05sqU7/1MntmVRONKoAgnJmHcfSpWwLyh6E4YX3iKDSgI/9RnAMF39KUUY/O +XFWyIwAM9soeXknVsV/SmCaPeaEDiLHqz98aUEfvdLYMnuR883GgoXc4JrLsLw4z +oSi9lbAZFn0ekJ5L+rSFrY4rz9QZgYZxsLjUXKUCgYAbkaUSU2g3w8Np2J2i9u7T +hQ7SUsphdPHqAxSc5xGd6MxLYqIgeKpQHnwN1VHcfFUonIer7d2kxrBUpDdeBqT9 +ub+ulgqHhFo29ko70VNzUKrSwL2g6Q6LPFutt4zUe7nDvvL5loHRWF0XOTafurL5 +aKIsepO0KRZQU0U6IgszDQKBgFs6ZHaP2mTtFY4L0a75Ab3xu20gRgUhHRLq/H6P +wipMpMnuodaPBr9pU53Dig65D8T9Nq1eUnbgy4vs0T5FCPz6iqWN5RVQ8aieQhIP +WfRj1Wfx0WAfXcWEM2G9ACr5TWj3OVVjNclP8X9+hW6gPky+gv03c0+4gZ+4QRRg +ksPdAoGBAKXVv8L5Qt8rmi+QpF/6DVSeMB6lEevf319UEtyZPiYSBn1JJ9H+3Ddc +TMriXgZrw+PoXNJTAguDeGgzmtye1vraP0cjt7aG6sQ4YtMTnnuja880vK8z4i3Q +HJAi5fI8NvlW92dQBAccgrzFIKpRx+6CHwtCkNrxL2vJmGGzp8BR +-----END RSA PRIVATE KEY----- diff --git a/docs/en_us/configuration/server_data_conf/cluster_conf.data.md b/docs/en_us/configuration/server_data_conf/cluster_conf.data.md index 2eb2ff8fc..ea771cba0 100644 --- a/docs/en_us/configuration/server_data_conf/cluster_conf.data.md +++ b/docs/en_us/configuration/server_data_conf/cluster_conf.data.md @@ -1,83 +1,88 @@ -# Cluster Configuration - -## Introduction - -cluster_conf.data records the cluster config. - -## Configuration - -| Config Item | Description | -| ----------- | ------------------------------------------------------------ | -| Version | String
Version of config file | -| Config | Struct
Map data, key is cluster name, value is cluster config detail | -| Config[k] | String
Cluster name | -| Config[v] | Object
Cluster's routing config | - -### Cluster Config Detail - -Notice: the following configs are in namespace Config[v]. - -#### Backend Config - -BackendConf is config for backend. - -| Config Item | Description | -| --------------------- | ------------------------------------------- | -| Protocol | String
Protocol for connecting backend. http and fcgi are supported. Default value is http. | -| TimeoutConnSrv | Int
Timeout for connecting backend, in ms. Default value is 2000. | -| TimeoutResponseHeader | Int
Timeout for reading response header, in ms. Default value is 60000. | -| MaxIdleConnsPerHost | Int
Max idle connections to each backend per BFE. Default value is 2. | -| MaxConnsPerHost | Int
Max number of concurrent connections to each backend per BFE. 0 means no limitation. Default value is 0. | -| RetryLevel | Int
Retry level if request fail. 0: retry after connecting backend fails; 1: retry after connecting backend fails or forwarding GET request fails. Default value is 0. | -| BackendConf.OutlierDetectionHttpCode | String
Backend HTTP status code outlier detection.
"" means disable detection, "500" means "500" is considered as backend failure.
Supports two formats: "\[0-9\]{3}"(e.g "500"), and "\[0-9\]xx"(e.g "4xx"). Multiple status codes are separated by "\|".
Default value is "", which means disable the detection. | -| FCGIConf | Object
Conf for FastCGI Protocol | -| FCGIConf.Root | String
The root folder of the website | -| FCGIConf.EnvVars | Map\[string\]string
Extra environment variable | - -#### Health Check Config - -CheckConf is config of backend check. - -| Config Item | Description | -| ------------- | ------------------------------------------------------------ | -| Schem | String
Protocol for health check (HTTP/TCP). Default value is http. | -| Uri | String
Uri used in health check (HTTP only). Default value is "/health_check". | -| Host | String
Host used in health check (HTTP only). Default value is "". | -| StatusCode | Int
Expected response code (HTTP only). Default value is 200. And 0 means any response code is considered valid. | -| FailNum | Int
Failure threshold (consecutive failures of forwarded requests), which will trigger BFE to set the backend instance to unavailable state and start the health check. | -| SuccNum | Int
Healthy threshold (consecutive successes of health check request), which will trigger BFE to set the backend instance to available state and stop the health check. | -| CheckTimeout | Int
Timeout for health check, in ms. Default value is 0, which means no timeout. | -| CheckInterval | Int
Interval of health check, in ms. Default value is 1000. | - -#### GSLB Config - -GslbBasic is cluster config for Gslb. - -| Config Item | Description | -| --------------------- | ------------------------------------------------------------ | -| CrossRetry | Int
Max cross sub-clusters retry times. Default value is 0. | -| RetryMax | Int
Max retry times within same sub-cluster. Default value is 2. | -| BalanceMode | String
Load Balance Mode. Supported mode: WRR(Weighted Round Robin), WLC(Weighted Least Connection). Default value is WRR. | -| HashConf | Struct
Hash config of session persistence
| -| HashConf.HashStrategy | Int
Hash Strategy of session persistence. Supported strategies: 0: ClientIdOnly, 1: ClientIpOnly, 2: ClientIdPreferred, 3: RequestURI. Default value is 1 (ClientIpOnly).
| -| HashConf.HashHeader | String
HashHeader is an optional request header which represents a unique client. Format for speicial cookie header is "Cookie:Key"
| -| HashConf.SessonSticky | Boolean
Set SessionSticky to "true" enable sticky session (ensures that all requests from the user during the session are sent to the same backend). If set to "false", the session persistence will be at sub-cluster level. | - -#### Cluster Basic Config - -ClusterBasic is basic config for cluster. - -| Config Item | Description | -| ---------------------- | ------------------------------------------------------------ | -| TimeoutReadClient | Int
Timeout for read client body in ms. Default value is 30000. | -| TimeoutWriteClient | Int
Timeout for write response to client. Default value is 60000. | -| TimeoutReadClientAgain | Int
Timeout for read client again in ms. Default value is 60000. | -| ReqWriteBufferSize | Int
Write buffer size for request in byte. Default and recommended value is 512. | -| ReqFlushInterval | Int
Interval to flush request in ms. Default and recommended value is 0, means disable periodic flush. | -| ResFlushInterval | Int
Interval to flush response in ms. Default and recommended value is -1, means not to cache response. 0 means disable periodic flush. | -| CancelOnClientClose | Bool
During reading response from backend, cancel the blocking status if client connection disconnected. Default and recommended value is false. | - -## Example +# Cluster Forwarding Configuration + +## Configuration Introduction + +`cluster_conf.data` is the configuration file for cluster forwarding. + +## Configuration Description + +### Basic Configuration + +| Configuration Item | Description | +| ------------------ | ----------- | +| Version | String
Configuration file version | +| Config | Object
Forwarding configuration parameters for each cluster | +| Config[k] | String
Cluster name | +| Config[v] | Object
Cluster forwarding configuration parameters | + +### Cluster Forwarding Configuration + +Note: The following configuration items are located in the namespace `Config[v]`, and the namespace is omitted in the configuration item names. + +#### Backend Basic Configuration + +| Configuration Item | Description | +| ------------------ | ----------- | +| BackendConf.Protocol | String
Protocol of the backend service, currently supports http/https and fcgi, default value is http | +| BackendConf.TimeoutConnSrv | Integer
Timeout for connecting to the backend, in milliseconds
Default value 2000 | +| BackendConf.TimeoutResponseHeader | Integer
Timeout for reading the response header from the backend, in milliseconds
Default value 60000 | +| BackendConf.MaxIdleConnsPerHost | Integer
Maximum number of idle persistent connections between the BFE instance and each backend
Default value 2 | +| BackendConf.MaxConnsPerHost | Integer
Maximum number of persistent connections between the BFE instance and each backend, 0 means no limit
Default value 0 | +| BackendConf.RetryLevel | Integer
Request retry level. 0: Retry when connection to backend fails; 1: Retry when connection to backend fails or forwarding GET request fails
Default value 0 | +| BackendConf.OutlierDetectionHttpCode | String
Backend response status code anomaly check, "" means no check, "500" means backend is considered failed if it returns 500
Supports two formats: "\[0-9\]{3}" (e.g., "500") and "\[0-9\]xx" (e.g., "4xx"); multiple status codes can be connected using '|'
Default value "", no backend response status code anomaly check | +| BackendConf.FCGIConf | Object
FastCGI protocol configuration | +| BackendConf.FCGIConf.Root | String
Root folder location of the website | +| BackendConf.FCGIConf.EnvVars | Map\[string\]string
Extended environment variables | + +#### Health Check Configuration + +| Configuration Item | Description | +| ------------------ | ----------- | +| CheckConf.Schem | String
Health check protocol, supports HTTP/HTTPS/TCP/TLS
Default value HTTP | +| CheckConf.Uri | String
Health check request URI (only HTTP/HTTPS)
Default value "/health_check" | +| CheckConf.Host | String
Health check request HOST (only HTTP/HTTPS)
Default value "" | +| CheckConf.StatusCode | Integer
Expected response status code (only HTTP/HTTPS)
Default value 200. Can also be configured as 0, meaning any status code is acceptable. | +| CheckConf.StatusCodeRange | String
Expected response status code (only HTTP/HTTPS)
See: Note 1. StatusCodeRange | +| CheckConf.FailNum | Integer
Health check activation threshold (after forwarding requests fail consecutively for FailNum times, the backend instance is marked as unavailable and health check is initiated)
Default value 5 | +| CheckConf.SuccNum | Integer
Health check success threshold (after health check succeeds consecutively for SuccNum times, the backend instance is marked as available)
Default value 1 | +| CheckConf.CheckTimeout | Integer
Health check timeout, in milliseconds
Default value 0 (no timeout) | +| CheckConf.CheckInterval | Integer
Health check interval, in milliseconds
Default value 1000 | + +#### GSLB Basic Configuration + +| Configuration Item | Description | +| ------------------ | ----------- | +| GslbBasic.CrossRetry | Integer
Maximum cross-sub-cluster retry count
Default value 0 | +| GslbBasic.RetryMax | Integer
Maximum retry count within a sub-cluster
Default value 2 | +| GslbBasic.BalanceMode | String
Load balancing mode (WRR: Weighted Round Robin; WLC: Weighted Least Connections)
Default value WRR | +| GslbBasic.HashConf | Object
Hash strategy configuration for session persistence | +| GslbBasic.HashConf.HashStrategy | Integer
Hash strategy for session persistence. 0: ClientIdOnly, 1: ClientIpOnly, 2: ClientIdPreferred, 3: RequestURI
Default value 1 (ClientIpOnly) | +| GslbBasic.HashConf.HashHeader | String
Hash request header for session persistence. Optional. Can be configured as a Header that uniquely identifies a client. If it is a cookie header, the format is: "Cookie:key" | +| GslbBasic.HashConf.SessionSticky | Boolean
Whether to enable session persistence (when enabled, requests from the same user can be sent to the same backend)
Default value False. When set to False, the session persistence level is at the sub-cluster level. | + +#### Cluster Basic Configuration + +| Configuration Item | Description | +| ------------------ | ----------- | +| ClusterBasic.TimeoutReadClient | Integer
Timeout for reading the client request body, in milliseconds
Default value 30000 | +| ClusterBasic.TimeoutWriteClient | Integer
Timeout for writing the response, in milliseconds
Default value 60000 | +| ClusterBasic.TimeoutReadClientAgain | Integer
Timeout for idle connections, in milliseconds
Default value 60000 | +| ClusterBasic.ReqWriteBufferSize | Integer
Request write buffer size, in Bytes. Default value 512. Recommended to use the default value. | +| ClusterBasic.ReqFlushInterval | Integer
Interval for flushing requests, in milliseconds. Default value 0, meaning no periodic flushing | +| ClusterBasic.ResFlushInterval | Integer
Interval for flushing responses, in milliseconds. Default value -1, meaning no caching of responses. Setting to 0 means no periodic flushing. Recommended to use the default value. | +| ClusterBasic.CancelOnClientClose | Boolean
Whether to cancel the blocking state when the client disconnects while the server is reading the backend response. Default value false. Recommended to use the default value. | + +#### Backend Service HTTPS Configuration + +| Configuration Item | Description | +| ------------------ | ----------- | +| HTTPSConf.RSHost | String
Hostname of the backend service instance, used to verify the server certificate.
Default value: Host field in the frontend request header. | +| HTTPSConf.BFEKeyFile | String
Private key file path, required when mutual authentication is supported
Private key used by the BFE engine when forwarding HTTPS requests to the backend. The private key file must be in PEM format | +| HTTPSConf.BFECertFile | String
Certificate file path, required when mutual authentication is supported
Certificate used by the BFE engine when forwarding HTTPS requests to the backend. The certificate file must be in x509 standard PEM format, and each PEM file can only contain one certificate | +| HTTPSConf.RSCAList | []String
Required when BackendConf.Protocol is https and server certificate verification is needed (i.e., RSInsecureSkipVerify is false). If not filled, the system default CA pool is used. List items are certificate file paths. Certificate files must be in x509 standard PEM format. Multiple CA certificates in the CA trust chain can be combined into one PEM file. | +| HTTPSConf.RSInsecureSkipVerify | Boolean
Server certificate verification switch
true: Do not verify, false: Verify (default) | + +## Configuration Example ```json { @@ -88,9 +93,8 @@ ClusterBasic is basic config for cluster. "TimeoutConnSrv": 2000, "TimeoutResponseHeader": 50000, "MaxIdleConnsPerHost": 0, - "MaxConnsPerHost": 0, "RetryLevel": 0, - "OutlierDetectionHttpCode": "5xx|400" + "OutlierDetectionHttpCode": "5xx|403" }, "CheckConf": { "Schem": "http", @@ -113,10 +117,51 @@ ClusterBasic is basic config for cluster. "TimeoutReadClient": 30000, "TimeoutWriteClient": 60000, "TimeoutReadClientAgain": 60000, + } + }, + "https_cluster_example": { + "BackendConf": { + "Protocol": "https", + "TimeoutConnSrv": 2000, + "TimeoutResponseHeader": 50000, + "MaxIdleConnsPerHost": 0, + "RetryLevel": 0 + }, + "CheckConf": { + "Schem": "https", + "Uri": "/", + "Host": "example.org", + "StatusCode": 200, + "FailNum": 10, + "CheckInterval": 1000 + }, + "GslbBasic": { + "CrossRetry": 0, + "RetryMax": 2, + "HashConf": { + "HashStrategy": 0, + "HashHeader": "Cookie:UID", + "SessionSticky": false + } + }, + "ClusterBasic": { + "TimeoutReadClient": 30000, + "TimeoutWriteClient": 60000, + "TimeoutReadClientAgain": 30000, "ReqWriteBufferSize": 512, "ReqFlushInterval": 0, "ResFlushInterval": -1, "CancelOnClientClose": false + }, + "HTTPSConf":{ + "RSHost": "www.example.org", + "BFEKeyFile": "../conf/tls_conf/backend_rs/r_bfe_dev_prv.pem", + "BFECertFile": "../conf/tls_conf/backend_rs/r_bfe_dev.crt", + "RSCAList": [ + "../conf/tls_conf/backend_rs/bfe_r_ca.crt", + "../conf/tls_conf/backend_rs/bfe_i_ca.crt" + ], + "RSInsecureSkipVerify": false } }, "fcgi_cluster_example": { @@ -125,6 +170,7 @@ ClusterBasic is basic config for cluster. "TimeoutConnSrv": 2000, "TimeoutResponseHeader": 50000, "MaxIdleConnsPerHost": 0, + "MaxConnsPerHost": 0, "RetryLevel": 0, "FCGIConf": { "Root": "/home/work", @@ -163,3 +209,15 @@ ClusterBasic is basic config for cluster. } } ``` + +## Notes + +### 1. StatusCodeRange + +- Response status code range. If StatusCode is configured, this validation condition will be ignored. +- Valid configuration examples: + 1. One of `"3xx"`, `"4xx"`, `"5xx"` + 2. Specific HTTP return codes, consistent with the StatusCode function + 3. The above (1) or (2) connected by the `"|"` symbol, for example: + - `"503|4xx"` + - `"501|409|30x"` \ No newline at end of file diff --git a/docs/zh_cn/configuration/server_data_conf/cluster_conf.data.md b/docs/zh_cn/configuration/server_data_conf/cluster_conf.data.md index c69880385..9bb7d851a 100644 --- a/docs/zh_cn/configuration/server_data_conf/cluster_conf.data.md +++ b/docs/zh_cn/configuration/server_data_conf/cluster_conf.data.md @@ -23,7 +23,7 @@ cluster_conf.data为集群转发配置文件。 | 配置项 | 描述 | | ----------------------------- | ------------------------------------------------------------ | -| BackendConf.Protocol | String
后端服务的协议,当前支持http和fcgi, 默认值http | +| BackendConf.Protocol | String
后端服务的协议,当前支持http/https和fcgi, 默认值http | | BackendConf.TimeoutConnSrv | Integer
连接后端的超时时间,单位是毫秒
默认值2000 | | BackendConf.TimeoutResponseHeader | Integer
从后端读响应头的超时时间,单位是毫秒
默认值60000 | | BackendConf.MaxIdleConnsPerHost | Integer
BFE实例与每个后端的最大空闲长连接数
默认值2 | @@ -38,15 +38,17 @@ cluster_conf.data为集群转发配置文件。 | 配置项 | 描述 | | ------------------------ | ------------------------------------------------------------ | -| CheckConf.Schem | String
健康检查协议,支持HTTP和TCP
默认值 HTTP | -| CheckConf.Uri | String
健康检查请求URI (仅HTTP)
默认值 "/health_check" | -| CheckConf.Host | String
健康检查请求HOST (仅HTTP)
默认值 "" | -| CheckConf.StatusCode | Integer
期待返回的响应状态码 (仅HTTP)
默认值 200。也可以配置为0,代表任意状态码均符合预期。 | +| CheckConf.Schem | String
健康检查协议,支持HTTP/HTTPS/TCP/TLS
默认值 HTTP | +| CheckConf.Uri | String
健康检查请求URI (仅HTTP/HTTPS)
默认值 "/health_check" | +| CheckConf.Host | String
健康检查请求HOST (仅HTTP/HTTPS)
默认值 "" | +| CheckConf.StatusCode | Integer
期待返回的响应状态码 (仅HTTP/HTTPS)
默认值 200。也可以配置为0,代表任意状态码均符合预期。 | +| CheckConf.StatusCodeRange | String
期待返回的响应状态码 (仅HTTP/HTTPS)
具体参见: 注解 1. StatusCodeRange| | CheckConf.FailNum | Integer
健康检查启动阈值(转发请求连续失败FailNum次后,将后端实例置为不可用状态,并启动健康检查)
默认值5 | | CheckConf.SuccNum | Integer
健康检查成功阈值(健康检查连续成功SuccNum次后,将后端实例置为可用状态)
默认值1 | | CheckConf.CheckTimeout | Integer
健康检查的超时时间,单位是毫秒
默认值0(无超时)| | CheckConf.CheckInterval | Integer
健康检查的间隔时间,单位是毫秒
默认值1000 | + #### GSLB基础配置 | 配置项 | 描述 | @@ -71,6 +73,16 @@ cluster_conf.data为集群转发配置文件。 | ClusterBasic.ResFlushInterval | Integer
刷新响应的间隔时间,单位是毫秒。默认值为-1,表示不对响应进行缓存。设置为0表示不进行周期性刷新。建议使用默认值。 | | ClusterBasic.CancelOnClientClose | Boolean
当服务端正在读后端响应时,如果客户端断连,是否取消该阻塞状态。默认值为false。建议使用默认值。 | +#### 后端服务HTTPS配置 + +| 配置项 | 描述 | +| --------------------------------- | ------------------------------------------------------------ | +| HTTPSConf.RSHost | String
后端服务实例的hostname,用来验证服务端证书。
默认值:前端请求头中的Host字段。| +| HTTPSConf.BFEKeyFile | String
私钥文件路径,支持双向认证时必填
BFE引擎向后端转发https请求时使用的私钥。私钥文件必须是pem格式 | +| HTTPSConf.BFECertFile | String
证书文件路径,支持双向认证时必填
BFE引擎向后端转发https请求时使用的证书,证书文件必须是符合x509标准的pem格式,且每个pem文件中只能包含一张证书 | +| HTTPSConf.RSCAList | []String
BackendConf.Protocol为https,并且需要验证服务端的证书(即RSInsecureSkipVerify为false)时必填,如果不填则使用系统默认CA池。列表项为证书文件路径,证书文件必须是符合x509标准的pem格式证书,允许将CA信任链中的多个CA证书合入一个pem文件中。| +| HTTPSConf.RSInsecureSkipVerify | Boolean
服务端证书验证开关
true:不验证,false:验证(默认)| + ## 配置示例 ```json @@ -108,6 +120,51 @@ cluster_conf.data为集群转发配置文件。 "TimeoutReadClientAgain": 60000, } }, + "https_cluster_example": { + "BackendConf": { + "Protocol": "https", + "TimeoutConnSrv": 2000, + "TimeoutResponseHeader": 50000, + "MaxIdleConnsPerHost": 0, + "RetryLevel": 0 + }, + "CheckConf": { + "Schem": "https", + "Uri": "/", + "Host": "example.org", + "StatusCode": 200, + "FailNum": 10, + "CheckInterval": 1000 + }, + "GslbBasic": { + "CrossRetry": 0, + "RetryMax": 2, + "HashConf": { + "HashStrategy": 0, + "HashHeader": "Cookie:UID", + "SessionSticky": false + } + }, + "ClusterBasic": { + "TimeoutReadClient": 30000, + "TimeoutWriteClient": 60000, + "TimeoutReadClientAgain": 30000, + "ReqWriteBufferSize": 512, + "ReqFlushInterval": 0, + "ResFlushInterval": -1, + "CancelOnClientClose": false + }, + "HTTPSConf":{ + "RSHost": "www.example.org", + "BFEKeyFile": "../conf/tls_conf/backend_rs/r_bfe_dev_prv.pem", + "BFECertFile": "../conf/tls_conf/backend_rs/r_bfe_dev.crt", + "RSCAList": [ + "../conf/tls_conf/backend_rs/bfe_r_ca.crt", + "../conf/tls_conf/backend_rs/bfe_i_ca.crt" + ], + "RSInsecureSkipVerify": false + } + }, "fcgi_cluster_example": { "BackendConf": { "Protocol": "fcgi", @@ -153,3 +210,15 @@ cluster_conf.data为集群转发配置文件。 } } ``` + +## 注解 + +### 1. StatusCodeRange + +- 响应状态码范围。如果配置了StatusCode,则会忽略此验证条件 +- 合法的配置项举例: + 1. `"3xx"`, `"4xx"`, `"5xx"` 其中之一 + 2. 特定的HTTP返回码,与StatusCode功能一致 + 3. `"|"` 符号连接的上述 (1)或 (2) 例如: + - `"503|4xx"` + - `"501|409|30x"`