Repository: dragonflyoss/Dragonfly2
Pull request #1602 (2022-08-29 11:54:58)

feat: add grpc mux transport
Signed-off-by: Jim Ma <majinjing3@gmail.com>

<!--- Provide a general summary of your changes in the Title above -->

## Description

Support both of tls and non-tls grpc server

## Related Issue

<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: -->

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->

## Screenshots (if appropriate)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation Update (if none of the other choices apply)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the **CONTRIBUTING** document.
- [ ] I have added tests to cover my changes.


codecov-commenter: # [Codecov](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1602?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss) Report
> Merging [#1602](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1602?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss) (a0ab02c) into [main](https://codecov.io/gh/dragonflyoss/Dragonfly2/commit/7cf8fe162047cfc45f8ff29fcf53ece129a2a610?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss) (7cf8fe1) will **decrease** coverage by `0.43%`.
> The diff coverage is `16.32%`.

[![Impacted file tree graph](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1602/graphs/tree.svg?width=650&height=150&src=pr&token=PUICJYSAT7&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss)](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1602?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss)

```diff
@@            Coverage Diff             @@
##             main    #1602      +/-   ##
==========================================
- Coverage   42.45%   42.01%   -0.44%     
==========================================
  Files         120      124       +4     
  Lines       11313    11440     +127     
==========================================
+ Hits         4803     4807       +4     
- Misses       6004     6124     +120     
- Partials      506      509       +3     
```

| Flag | Coverage Δ | |
|---|---|---|
| Object-compatibility-e2etests | `?` | |
| e2etests | `?` | |
| unittests | `42.01% <16.32%> (-0.44%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1602?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss) | Coverage Δ | |
|---|---|---|
| [client/config/fixme.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1602/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-Y2xpZW50L2NvbmZpZy9maXhtZS5nbw==) | `0.00% <0.00%> (ø)` | |
| [client/daemon/rpcserver/rpcserver.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1602/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-Y2xpZW50L2RhZW1vbi9ycGNzZXJ2ZXIvcnBjc2VydmVyLmdv) | `19.22% <0.00%> (-0.43%)` | :arrow_down: |
| [client/daemon/rpcserver/seeder.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1602/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-Y2xpZW50L2RhZW1vbi9ycGNzZXJ2ZXIvc2VlZGVyLmdv) | `53.43% <0.00%> (-0.53%)` | :arrow_down: |
| [pkg/rpc/mux.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1602/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-cGtnL3JwYy9tdXguZ28=) | `20.00% <20.00%> (ø)` | |
| [client/daemon/peer/piece\_manager.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1602/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-Y2xpZW50L2RhZW1vbi9wZWVyL3BpZWNlX21hbmFnZXIuZ28=) | `52.18% <100.00%> (-0.53%)` | :arrow_down: |
| [pkg/rpc/interceptor.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1602/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-cGtnL3JwYy9pbnRlcmNlcHRvci5nbw==) | `0.00% <0.00%> (ø)` | |
| [pkg/rpc/server\_listen.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1602/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-cGtnL3JwYy9zZXJ2ZXJfbGlzdGVuLmdv) | `0.00% <0.00%> (ø)` | |
| [pkg/rpc/vsock.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1602/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-cGtnL3JwYy92c29jay5nbw==) | `0.00% <0.00%> (ø)` | |


Patch:
diff --git a/client/config/fixme.go b/client/config/fixme.go
index 1ab8f71d..ffef7459 100644
--- a/client/config/fixme.go
+++ b/client/config/fixme.go
@@ -20,12 +20,14 @@ import (
 	"crypto/tls"
 
 	"github.com/johanbrandhorst/certify"
+
+	"d7y.io/dragonfly/v2/pkg/net/ip"
 )
 
 func GetCertificate(certifyClient *certify.Certify) func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
 	return func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
 		// FIXME peers need pure ip cert, certify checks the ServerName, so workaround here
-		hello.ServerName = "peer"
+		hello.ServerName = ip.IPv4
 		return certifyClient.GetCertificate(hello)
 	}
 }
diff --git a/client/config/peerhost.go b/client/config/peerhost.go
index 1c217634..80e6fea4 100644
--- a/client/config/peerhost.go
+++ b/client/config/peerhost.go
@@ -174,10 +174,25 @@ func ConvertPattern(p string, defaultPattern commonv1.Pattern) commonv1.Pattern
 }
 
 type GlobalSecurityOption struct {
-	AutoIssueCert bool                 `mapstructure:"autoIssueCert" yaml:"autoIssueCert"`
-	CACert        serialize.PEMContent `mapstructure:"caCert" yaml:"caCert"`
-	TLSVerify     bool                 `mapstructure:"tlsVerify" yaml:"tlsVerify"`
-}
+	// AutoIssueCert indicates to issue client certificates for all grpc call
+	// if AutoIssueCert is false, any other option in Security will be ignored
+	AutoIssueCert bool `mapstructure:"autoIssueCert" yaml:"autoIssueCert"`
+	// CACert is the root CA certificate for all grpc tls handshake, it can be path or PEM format string
+	CACert serialize.PEMContent `mapstructure:"caCert" yaml:"caCert"`
+	// TLSPrefer indicates to verify client certificates for grpc ServerHandshake
+	TLSVerify bool `mapstructure:"tlsVerify" yaml:"tlsVerify"`
+	// TLSPolicy controls the grpc shandshake behaviors:
+	// force: both ClientHandshake and ServerHandshake are only support tls
+	// prefer: ServerHandshake supports tls and insecure (non-tls), ClientHandshake will only support tls
+	// default or empty: ServerHandshake supports tls and insecure (non-tls), ClientHandshake will only support insecure (non-tls)
+	TLSPolicy string `mapstructure:"tlsPolicy" yaml:"tlsPolicy"`
+}
+
+const (
+	TLSPolicyForce   = "force"
+	TLSPolicyPrefer  = "prefer"
+	TLSPolicyDefault = "default"
+)
 
 type SchedulerOption struct {
 	// Manager is to get the scheduler configuration remotely.
diff --git a/client/config/peerhost_darwin.go b/client/config/peerhost_darwin.go
index 90fd917e..57c8ca33 100644
--- a/client/config/peerhost_darwin.go
+++ b/client/config/peerhost_darwin.go
@@ -81,7 +81,7 @@ var peerHostConfig = func() *DaemonOption {
 			DownloadGRPC: ListenOption{
 				Security: SecurityOption{
 					Insecure:  true,
-					TLSVerify: true,
+					TLSVerify: false,
 				},
 				UnixListen: &UnixListenOption{},
 			},
@@ -176,5 +176,11 @@ var peerHostConfig = func() *DaemonOption {
 				Duration: time.Minute,
 			},
 		},
+		Security: GlobalSecurityOption{
+			AutoIssueCert: false,
+			CACert:        serialize.PEMContent(""),
+			TLSVerify:     false,
+			TLSPolicy:     TLSPolicyDefault,
+		},
 	}
 }
diff --git a/client/config/peerhost_linux.go b/client/config/peerhost_linux.go
index 8ee1bcf4..89e86296 100644
--- a/client/config/peerhost_linux.go
+++ b/client/config/peerhost_linux.go
@@ -29,6 +29,7 @@ import (
 	"d7y.io/dragonfly/v2/pkg/dfnet"
 	"d7y.io/dragonfly/v2/pkg/net/fqdn"
 	"d7y.io/dragonfly/v2/pkg/net/ip"
+	"d7y.io/dragonfly/v2/pkg/serialize"
 )
 
 var peerHostConfig = func() *DaemonOption {
@@ -81,7 +82,7 @@ var peerHostConfig = func() *DaemonOption {
 			DownloadGRPC: ListenOption{
 				Security: SecurityOption{
 					Insecure:  true,
-					TLSVerify: true,
+					TLSVerify: false,
 				},
 				UnixListen: &UnixListenOption{},
 			},
@@ -175,5 +176,11 @@ var peerHostConfig = func() *DaemonOption {
 				Duration: time.Minute,
 			},
 		},
+		Security: GlobalSecurityOption{
+			AutoIssueCert: false,
+			CACert:        serialize.PEMContent(""),
+			TLSVerify:     false,
+			TLSPolicy:     TLSPolicyDefault,
+		},
 	}
 }
diff --git a/client/daemon/daemon.go b/client/daemon/daemon.go
index 1c7f7cc7..f339e09d 100644
--- a/client/daemon/daemon.go
+++ b/client/daemon/daemon.go
@@ -141,17 +141,19 @@ func New(opt *config.DaemonOption, d dfpath.Dfpath) (Daemon, error) {
 				Logger:       zapadapter.New(logger.CoreLogger.Desugar()),
 				Cache: cache.NewCertifyMutliCache(
 					certify.NewMemCache(),
-					certify.DirCache(path.Join(d.CacheDir(), "certs"))),
+					certify.DirCache(path.Join(d.CacheDir(), cache.CertifyCacheDirName))),
 			}
 
 			// issue a certificate to reduce first time delay
-			_, err := certifyClient.GetCertificate(&tls.ClientHelloInfo{
+			cert, err := certifyClient.GetCertificate(&tls.ClientHelloInfo{
 				ServerName: ip.IPv4,
 			})
 			if err != nil {
 				logger.Errorf("issue certificate error: %s", err.Error())
 				return nil, err
 			}
+			logger.Debugf("request cert from manager, common name: %s, issuer: %s",
+				cert.Leaf.Subject.CommonName, cert.Leaf.Issuer.CommonName)
 		}
 
 		// New dynconfig manager client.
@@ -337,6 +339,7 @@ func loadGPRCTLSCredentials(opt config.SecurityOption, certifyClient *certify.Ce
 	}
 
 	opt.TLSConfig.ClientCAs = certPool
+	opt.TLSConfig.RootCAs = certPool
 
 	// Load server's certificate and private key
 	if certifyClient == nil {
@@ -347,7 +350,6 @@ func loadGPRCTLSCredentials(opt config.SecurityOption, certifyClient *certify.Ce
 		opt.TLSConfig.Certificates = []tls.Certificate{serverCert}
 	} else {
 		// enable auto issue certificate
-		opt.TLSConfig.Certificates = nil
 		opt.TLSConfig.GetCertificate = config.GetCertificate(certifyClient)
 		opt.TLSConfig.GetClientCertificate = certifyClient.GetClientCertificate
 	}
@@ -356,7 +358,15 @@ func loadGPRCTLSCredentials(opt config.SecurityOption, certifyClient *certify.Ce
 		opt.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
 	}
 
-	return credentials.NewTLS(opt.TLSConfig), nil
+	switch security.TLSPolicy {
+	case config.TLSPolicyDefault, config.TLSPolicyPrefer:
+		return rpc.NewMuxTransportCredentials(opt.TLSConfig,
+			rpc.WithTLSPreferClientHandshake(security.TLSPolicy == config.TLSPolicyPrefer)), nil
+	case config.TLSPolicyForce:
+		return credentials.NewTLS(opt.TLSConfig), nil
+	default:
+		return nil, fmt.Errorf("invalid tlsPolicy: %s", security.TLSPolicy)
+	}
 }
 
 func loadGlobalGPRCTLSCredentials(certifyClient *certify.Certify, security config.GlobalSecurityOption) (credentials.TransportCredentials, error) {
@@ -370,17 +380,26 @@ func loadGlobalGPRCTLSCredentials(certifyClient *certify.Certify, security confi
 		return nil, fmt.Errorf("failed to add global CA's certificate")
 	}
 
-	config := &tls.Config{
+	tlsConfig := &tls.Config{
 		ClientCAs:            certPool,
+		RootCAs:              certPool,
 		GetCertificate:       config.GetCertificate(certifyClient),
 		GetClientCertificate: certifyClient.GetClientCertificate,
 	}
 
 	if security.TLSVerify {
-		config.ClientAuth = tls.RequireAndVerifyClientCert
+		tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
 	}
 
-	return credentials.NewTLS(config), nil
+	switch security.TLSPolicy {
+	case config.TLSPolicyDefault, config.TLSPolicyPrefer:
+		return rpc.NewMuxTransportCredentials(tlsConfig,
+			rpc.WithTLSPreferClientHandshake(security.TLSPolicy == config.TLSPolicyPrefer)), nil
+	case config.TLSPolicyForce:
+		return credentials.NewTLS(tlsConfig), nil
+	default:
+		return nil, fmt.Errorf("invalid tlsPolicy: %s", security.TLSPolicy)
+	}
 }
 
 func (*clientDaemon) prepareTCPListener(opt config.ListenOption, withTLS bool) (net.Listener, int, error) {
diff --git a/client/daemon/peer/piece_manager.go b/client/daemon/peer/piece_manager.go
index 237797c1..2bbf0920 100644
--- a/client/daemon/peer/piece_manager.go
+++ b/client/daemon/peer/piece_manager.go
@@ -485,7 +485,7 @@ func (pm *pieceManager) downloadKnownLengthSource(ctx context.Context, pt Task,
 
 		pt.ReportPieceResult(request, result, nil)
 		pt.PublishPieceInfo(pieceNum, uint32(result.Size))
-		if supportConcurrent {
+		if supportConcurrent && pieceNum+2 < maxPieceNum {
 			// the time unit of FinishTime and BeginTime is ns
 			speed := float64(pieceSize) / float64((result.FinishTime-result.BeginTime)/1000000)
 			if speed < float64(pm.concurrentOption.ThresholdSpeed) {
diff --git a/client/daemon/rpcserver/rpcserver.go b/client/daemon/rpcserver/rpcserver.go
index f8ade718..c0fada39 100644
--- a/client/daemon/rpcserver/rpcserver.go
+++ b/client/daemon/rpcserver/rpcserver.go
@@ -38,6 +38,8 @@ import (
 	"go.uber.org/atomic"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/credentials"
+	grpcpeer "google.golang.org/grpc/peer"
 	"google.golang.org/grpc/status"
 	"google.golang.org/protobuf/types/known/emptypb"
 
@@ -211,7 +213,28 @@ func (s *server) sendFirstPieceTasks(
 	return sendExistPieces(sync.Context(), log, get, request, sync, sentMap, false)
 }
 
+func printAuthInfo(ctx context.Context) {
+	if peerInfo, ok := grpcpeer.FromContext(ctx); ok {
+		if tlsInfo, ok := peerInfo.AuthInfo.(credentials.TLSInfo); ok {
+			for i, pc := range tlsInfo.State.PeerCertificates {
+				logger.Debugf("peer cert depth %d, issuer: %#v", i, pc.Issuer.CommonName)
+				logger.Debugf("peer cert depth %d, common name: %#v", i, pc.Subject.CommonName)
+				if len(pc.IPAddresses) > 0 {
+					logger.Debugf("peer cert depth %d, ip: %#v", i, pc.IPAddresses)
+				}
+				if len(pc.DNSNames) > 0 {
+					logger.Debugf("peer cert depth %d, dns: %#v", i, pc.DNSNames)
+				}
+			}
+		}
+	}
+}
+
 func (s *server) SyncPieceTasks(sync dfdaemonv1.Daemon_SyncPieceTasksServer) error {
+	if logger.IsDebug() {
+		printAuthInfo(sync.Context())
+	}
+
 	request, err := sync.Recv()
 	if err != nil {
 		logger.Errorf("receive first sync piece tasks request error: %s", err.Error())
diff --git a/client/daemon/rpcserver/seeder.go b/client/daemon/rpcserver/seeder.go
index 7a18ded6..9865375b 100644
--- a/client/daemon/rpcserver/seeder.go
+++ b/client/daemon/rpcserver/seeder.go
@@ -52,6 +52,10 @@ func (s *seeder) SyncPieceTasks(tasksServer cdnsystemv1.Seeder_SyncPieceTasksSer
 }
 
 func (s *seeder) ObtainSeeds(seedRequest *cdnsystemv1.SeedRequest, seedsServer cdnsystemv1.Seeder_ObtainSeedsServer) error {
+	if logger.IsDebug() {
+		printAuthInfo(seedsServer.Context())
+	}
+
 	metrics.SeedPeerConcurrentDownloadGauge.Inc()
 	defer metrics.SeedPeerConcurrentDownloadGauge.Dec()
 	metrics.SeedPeerDownloadCount.Add(1)
diff --git a/go.mod b/go.mod
index 1b66cf6e..6db3df70 100644
--- a/go.mod
+++ b/go.mod
@@ -7,10 +7,10 @@ require (
 	github.com/RichardKnop/machinery v1.10.6
 	github.com/Showmax/go-fqdn v1.0.0
 	github.com/VividCortex/mysqlerr v1.0.0
-	github.com/aliyun/aliyun-oss-go-sdk v2.2.4+incompatible
+	github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible
 	github.com/appleboy/gin-jwt/v2 v2.8.0
 	github.com/aws/aws-sdk-go v1.44.44
-	github.com/bits-and-blooms/bitset v1.2.2
+	github.com/bits-and-blooms/bitset v1.3.0
 	github.com/casbin/casbin/v2 v2.52.2
 	github.com/casbin/gorm-adapter/v3 v3.5.0
 	github.com/colinmarc/hdfs/v2 v2.3.0
@@ -18,7 +18,7 @@ require (
 	github.com/docker/go-connections v0.4.0
 	github.com/docker/go-units v0.4.0
 	github.com/gammazero/deque v0.2.0
-	github.com/gin-contrib/cors v1.3.1
+	github.com/gin-contrib/cors v1.4.0
 	github.com/gin-contrib/static v0.0.1
 	github.com/gin-gonic/gin v1.8.1
 	github.com/go-echarts/statsview v0.3.4
@@ -57,7 +57,7 @@ require (
 	github.com/spf13/viper v1.12.0
 	github.com/stretchr/testify v1.8.0
 	github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe
-	github.com/swaggo/gin-swagger v1.5.1
+	github.com/swaggo/gin-swagger v1.5.2
 	github.com/swaggo/swag v1.8.4
 	go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.34.0
 	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.34.0
@@ -79,7 +79,7 @@ require (
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0
 	gopkg.in/yaml.v3 v3.0.1
 	gorm.io/driver/mysql v1.3.6
-	gorm.io/driver/postgres v1.3.8
+	gorm.io/driver/postgres v1.3.9
 	gorm.io/gorm v1.23.8
 	gorm.io/plugin/soft_delete v1.1.0
 	k8s.io/component-base v0.23.3
@@ -96,7 +96,6 @@ require (
 	github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
 	github.com/KyleBanks/depth v1.2.1 // indirect
 	github.com/RichardKnop/logging v0.0.0-20190827224416-1a693bdd4fae // indirect
-	github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
 	github.com/cespare/xxhash/v2 v2.1.2 // indirect
diff --git a/go.sum b/go.sum
index 03d0b1cb..ff6b9ef4 100644
--- a/go.sum
+++ b/go.sum
@@ -127,8 +127,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/aliyun/aliyun-oss-go-sdk v2.2.4+incompatible h1:cD1bK/FmYTpL+r5i9lQ9EU6ScAjA173EVsii7gAc6SQ=
-github.com/aliyun/aliyun-oss-go-sdk v2.2.4+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
+github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible h1:QoRMR0TCctLDqBCMyOu1eXdZyMw3F7uGA9qPn2J4+R8=
+github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
@@ -157,8 +157,6 @@ github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZw
 github.com/aws/aws-sdk-go-v2 v1.8.1/go.mod h1:xEFuWz+3TYdlPRuo+CqATbeDWIWyaT5uAPwPaWtgse0=
 github.com/aws/aws-sdk-go-v2/service/acmpca v1.6.3/go.mod h1:b8TQvsqtuWslJ4o+eXiHV59P4t20d0EJJ/YVICGJu/o=
 github.com/aws/smithy-go v1.7.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
-github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA=
-github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
 github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
 github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -167,8 +165,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/bits-and-blooms/bitset v1.2.2 h1:J5gbX05GpMdBjCvQ9MteIg2KKDExr7DrgK+Yc15FvIk=
-github.com/bits-and-blooms/bitset v1.2.2/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
+github.com/bits-and-blooms/bitset v1.3.0 h1:h7mv5q31cthBTd7V4kLAZaIThj1e8vPGcSqpPue9KVI=
+github.com/bits-and-blooms/bitset v1.3.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
 github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
@@ -301,15 +299,14 @@ github.com/gammazero/deque v0.2.0/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZ
 github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
 github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA=
-github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk=
+github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
+github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
 github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
 github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
 github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
 github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
 github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
 github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
-github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
 github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
 github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
 github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
@@ -364,11 +361,9 @@ github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrK
 github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
 github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
 github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
 github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
 github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
-github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
 github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
 github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
@@ -781,7 +776,6 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c=
-github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
 github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
 github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
 github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
@@ -818,7 +812,6 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
 github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
 github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
@@ -1052,7 +1045,6 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
 github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
 github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
-github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 github.com/schollz/progressbar/v3 v3.8.7 h1:rtje4lnXVD1Dy/RtPpGd2ijLCmQ7Su3G2ia8dJcRKIo=
 github.com/schollz/progressbar/v3 v3.8.7/go.mod h1:W5IEwbJecncFGBvuEh4A7HT1nZZ6WNIL2i3qbnI0WKY=
@@ -1135,8 +1127,8 @@ github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs
 github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo=
 github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc=
 github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
-github.com/swaggo/gin-swagger v1.5.1 h1:PFmlJU1LPn8DjrR0meVLX5gyFdgcPOkLcoFRRFx7WcY=
-github.com/swaggo/gin-swagger v1.5.1/go.mod h1:Cbj/MlHApPOjZdf4joWFXLLgmZVPyh54GPvPPyVjVZM=
+github.com/swaggo/gin-swagger v1.5.2 h1:dj2es17EaOHoy0Owu4xn3An1mI8/xjdFyIH6KAbOdYo=
+github.com/swaggo/gin-swagger v1.5.2/go.mod h1:Cbj/MlHApPOjZdf4joWFXLLgmZVPyh54GPvPPyVjVZM=
 github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
 github.com/swaggo/swag v1.8.4 h1:oGB351qH1JqUqK1tsMYEE5qTBbPk394BhsZxmUfebcI=
 github.com/swaggo/swag v1.8.4/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg=
@@ -1893,8 +1885,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
 gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
-gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
-gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
 gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@@ -1929,8 +1919,8 @@ gorm.io/driver/mysql v1.3.2/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2
 gorm.io/driver/mysql v1.3.6 h1:BhX1Y/RyALb+T9bZ3t07wLnPZBukt+IRkMn8UZSNbGM=
 gorm.io/driver/mysql v1.3.6/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
 gorm.io/driver/postgres v1.2.2/go.mod h1:Ik3tK+a3FMp8ORZl29v4b3M0RsgXsaeMXh9s9eVMXco=
-gorm.io/driver/postgres v1.3.8 h1:8bEphSAB69t3odsCR4NDzt581iZEWQuRM27Cg6KgfPY=
-gorm.io/driver/postgres v1.3.8/go.mod h1:qB98Aj6AhRO/oyu/jmZsi/YM9g6UzVCjMxO/6frFvcA=
+gorm.io/driver/postgres v1.3.9 h1:lWGiVt5CijhQAg0PWB7Od1RNcBw/jS4d2cAScBcSDXg=
+gorm.io/driver/postgres v1.3.9/go.mod h1:qw/FeqjxmYqW5dBcYNBsnhQULIApQdk7YuuDPktVi1U=
 gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=
 gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
 gorm.io/driver/sqlserver v1.2.1/go.mod h1:nixq0OB3iLXZDiPv6JSOjWuPgpyaRpOIIevYtA4Ulb4=
@@ -1945,7 +1935,7 @@ gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
 gorm.io/gorm v1.23.0/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
 gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
 gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
-gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
+gorm.io/gorm v1.23.7/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
 gorm.io/gorm v1.23.8 h1:h8sGJ+biDgBA1AD1Ha9gFCx7h8npU7AsLdlkX0n2TpE=
 gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
 gorm.io/plugin/dbresolver v1.1.0/go.mod h1:tpImigFAEejCALOttyhWqsy4vfa2Uh/vAUVnL5IRF7Y=
diff --git a/internal/dflog/logger.go b/internal/dflog/logger.go
index 27a72abb..d6292bf2 100644
--- a/internal/dflog/logger.go
+++ b/internal/dflog/logger.go
@@ -215,6 +215,10 @@ func (log *SugaredLoggerOnWith) Debug(args ...any) {
 	CoreLogger.Debugw(fmt.Sprint(args...), log.withArgs...)
 }
 
+func (log *SugaredLoggerOnWith) IsDebug() bool {
+	return coreLogLevelEnabler.Enabled(zap.DebugLevel)
+}
+
 func Infof(template string, args ...any) {
 	CoreLogger.Infof(template, args...)
 }
@@ -243,6 +247,14 @@ func Debugf(template string, args ...any) {
 	CoreLogger.Debugf(template, args...)
 }
 
+func Debug(args ...any) {
+	CoreLogger.Debug(args...)
+}
+
+func IsDebug() bool {
+	return coreLogLevelEnabler.Enabled(zap.DebugLevel)
+}
+
 func Fatalf(template string, args ...any) {
 	CoreLogger.Fatalf(template, args...)
 }
diff --git a/manager/config/config.go b/manager/config/config.go
index 4ab2155a..30065b09 100644
--- a/manager/config/config.go
+++ b/manager/config/config.go
@@ -214,10 +214,10 @@ type MetricsConfig struct {
 }
 
 type TCPListenConfig struct {
-	// Listen stands listen interface, like: 0.0.0.0, 192.168.0.1.
+	// Listen is listen interface, like: 0.0.0.0, 192.168.0.1.
 	Listen string `mapstructure:"listen" yaml:"listen"`
 
-	// PortRange stands listen port.
+	// PortRange is listen port.
 	PortRange TCPListenPortRange `yaml:"port" mapstructure:"port"`
 }
 
diff --git a/manager/manager.go b/manager/manager.go
index 4c166313..d07ae7fb 100644
--- a/manager/manager.go
+++ b/manager/manager.go
@@ -22,10 +22,13 @@ import (
 	"embed"
 	"io/fs"
 	"net/http"
+	"path"
 	"time"
 
 	"github.com/gin-contrib/static"
+	"github.com/johanbrandhorst/certify"
 	"google.golang.org/grpc"
+	zapadapter "logur.dev/adapter/zap"
 
 	logger "d7y.io/dragonfly/v2/internal/dflog"
 	"d7y.io/dragonfly/v2/manager/cache"
@@ -38,7 +41,10 @@ import (
 	"d7y.io/dragonfly/v2/manager/rpcserver"
 	"d7y.io/dragonfly/v2/manager/searcher"
 	"d7y.io/dragonfly/v2/manager/service"
+	pkgcache "d7y.io/dragonfly/v2/pkg/cache"
 	"d7y.io/dragonfly/v2/pkg/dfpath"
+	"d7y.io/dragonfly/v2/pkg/issuer"
+	"d7y.io/dragonfly/v2/pkg/net/ip"
 	"d7y.io/dragonfly/v2/pkg/objectstorage"
 	"d7y.io/dragonfly/v2/pkg/rpc"
 )
@@ -154,7 +160,7 @@ func New(cfg *config.Config, d dfpath.Dfpath) (*Server, error) {
 		return nil, err
 	}
 
-	// Initialize global certificate.
+	// Initialize signing certificate and tls credentials of grpc server.
 	var options []rpcserver.Option
 	if cfg.Security.Enable {
 		cert, err := tls.X509KeyPair([]byte(cfg.Security.CACert), []byte(cfg.Security.CAKey))
@@ -162,7 +168,30 @@ func New(cfg *config.Config, d dfpath.Dfpath) (*Server, error) {
 			return nil, err
 		}
 
-		options = append(options, rpcserver.WithCertificate(&cert))
+		// Manager GRPC server's tls varify must be false. If ClientCAs are required for client verification,
+		// the client cannot call the IssueCertificate api.
+		transportCredentials, err := rpc.NewServerCredentialsByCertify(false, &cert, &certify.Certify{
+			CommonName:   ip.IPv4,
+			Issuer:       issuer.NewDragonflyManagerIssuer(&cert),
+			RenewBefore:  time.Hour,
+			CertConfig:   nil,
+			IssueTimeout: 0,
+			Logger:       zapadapter.New(logger.CoreLogger.Desugar()),
+			Cache: pkgcache.NewCertifyMutliCache(
+				certify.NewMemCache(),
+				certify.DirCache(path.Join(d.CacheDir(), pkgcache.CertifyCacheDirName))),
+		})
+		if err != nil {
+			return nil, err
+		}
+
+		options = append(
+			options,
+			// Set ca certificate for issuing certificate.
+			rpcserver.WithSelfSignedCert(&cert),
+			// Set tls credentials for grpc server.
+			rpcserver.WithGRPCServerOptions([]grpc.ServerOption{grpc.Creds(transportCredentials)}),
+		)
 	}
 
 	// Initialize GRPC server
diff --git a/manager/rpcserver/cert.go b/manager/rpcserver/cert.go
index 1114d445..8be893b6 100644
--- a/manager/rpcserver/cert.go
+++ b/manager/rpcserver/cert.go
@@ -36,8 +36,11 @@ import (
 	logger "d7y.io/dragonfly/v2/internal/dflog"
 )
 
+// defaultValidityDuration is default validity duration of certificate.
+const defaultValidityDuration = 365 * 24 * time.Hour
+
 func (s *Server) IssueCertificate(ctx context.Context, req *securityv1.CertificateRequest) (*securityv1.CertificateResponse, error) {
-	if s.cert == nil {
+	if s.selfSignedCert == nil {
 		return nil, status.Errorf(codes.Unavailable, "ca is missing for this manager instance")
 	}
 
@@ -86,7 +89,7 @@ func (s *Server) IssueCertificate(ctx context.Context, req *securityv1.Certifica
 	now := time.Now()
 	duration := time.Duration(req.ValidityDuration) * time.Second
 	if duration == 0 {
-		duration = time.Hour
+		duration = defaultValidityDuration
 	}
 
 	logger.Infof("valid csr: %#v", csr.Subject)
@@ -102,18 +105,18 @@ func (s *Server) IssueCertificate(ctx context.Context, req *securityv1.Certifica
 		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
 	}
 
-	cert, err := x509.CreateCertificate(rand.Reader, &template, s.x509Cert, csr.PublicKey, s.cert.PrivateKey)
+	cert, err := x509.CreateCertificate(rand.Reader, &template, s.selfSignedCert.X509Cert, csr.PublicKey, s.selfSignedCert.TLSCert.PrivateKey)
 	if err != nil {
 		return nil, fmt.Errorf("failed to generate certificate, error: %s", err)
 	}
 
-	// Encode into PEM format.
+	// Build the certificate chain.
 	var certPEM bytes.Buffer
 	if err = pem.Encode(&certPEM, &pem.Block{Type: "CERTIFICATE", Bytes: cert}); err != nil {
 		return nil, err
 	}
 
 	return &securityv1.CertificateResponse{
-		CertificateChain: append([]string{certPEM.String()}, s.certChain...),
+		CertificateChain: append([]string{certPEM.String()}, s.selfSignedCert.PEMCertChain...),
 	}, nil
 }
diff --git a/manager/rpcserver/rpcserver.go b/manager/rpcserver/rpcserver.go
index 75624048..f6d6b149 100644
--- a/manager/rpcserver/rpcserver.go
+++ b/manager/rpcserver/rpcserver.go
@@ -50,6 +50,18 @@ import (
 	managerserver "d7y.io/dragonfly/v2/pkg/rpc/manager/server"
 )
 
+// SelfSignedCert is self signed certificate.
+type SelfSignedCert struct {
+	// TLSCert is certificate of tls.
+	TLSCert *tls.Certificate
+
+	// X509Cert is certificate of x509.
+	X509Cert *x509.Certificate
+
+	// PEMCertChain is certificate chain of pem.
+	PEMCertChain []string
+}
+
 // Server is grpc server.
 type Server struct {
 	// Manager configuration.
@@ -76,39 +88,35 @@ type Server struct {
 	// serverOptions is server options of grpc.
 	serverOptions []grpc.ServerOption
 
-	// cert certificates to sign certificates.
-	cert *tls.Certificate
-
-	// x509Cert certificates to sign certificates.
-	x509Cert *x509.Certificate
-
-	// certChain is PEM-encoded certificate chain.
-	certChain []string
+	// selfSignedCert is self signed certificate.
+	selfSignedCert *SelfSignedCert
 }
 
 // Option is a functional option for rpc server.
 type Option func(s *Server) error
 
-// WithCertificate set the root tls certificate, x509 certificate and PEM-encoded certificate chain.
-func WithCertificate(cert *tls.Certificate) Option {
+// WithCertificate set the self signed certificate for server.
+func WithSelfSignedCert(tlsCert *tls.Certificate) Option {
 	return func(s *Server) error {
-		s.cert = cert
-
-		// Parse x509 certificate from tls certificate.
-		var err error
-		s.x509Cert, err = x509.ParseCertificate(cert.Certificate[0])
+		x509CACert, err := x509.ParseCertificate(tlsCert.Certificate[0])
 		if err != nil {
 			return err
 		}
 
-		// Initialize PEM-encoded certificate chain from tls certificate.
-		for _, cert := range cert.Certificate {
-			var certChainPEM bytes.Buffer
-			if err = pem.Encode(&certChainPEM, &pem.Block{Type: "CERTIFICATE", Bytes: cert}); err != nil {
+		var pemCertChain []string
+		for _, cert := range tlsCert.Certificate {
+			var certPEM bytes.Buffer
+			if err := pem.Encode(&certPEM, &pem.Block{Type: "CERTIFICATE", Bytes: cert}); err != nil {
 				return err
 			}
 
-			s.certChain = append(s.certChain, certChainPEM.String())
+			pemCertChain = append(pemCertChain, certPEM.String())
+		}
+
+		s.selfSignedCert = &SelfSignedCert{
+			TLSCert:      tlsCert,
+			X509Cert:     x509CACert,
+			PEMCertChain: pemCertChain,
 		}
 
 		return nil
diff --git a/pkg/cache/certify.go b/pkg/cache/certify.go
index cd5224a7..fcb2559b 100644
--- a/pkg/cache/certify.go
+++ b/pkg/cache/certify.go
@@ -23,18 +23,23 @@ import (
 	"github.com/johanbrandhorst/certify"
 )
 
+const (
+	// CertifyCacheDirName is dir name of certify cache.
+	CertifyCacheDirName = "certs"
+)
+
 type certifyCache struct {
 	caches []certify.Cache
 }
 
 // NewCertifyMutliCache returns a certify.Cache with multiple caches
 // Such as, cache.NewCertifyMutliCache(certify.NewMemCache(), certify.DirCache("certs"))
-// This multiple cache will get certs from mem cache first, then dir cache to avoid read from filesystem every times
+// This multiple cache will get certs from mem cache first, then dir cache to avoid read from filesystem every times.
 func NewCertifyMutliCache(caches ...certify.Cache) certify.Cache {
 	return &certifyCache{caches: caches}
 }
 
-// Get gets cert from cache one by one, if found, puts it back to all previous cachs
+// Get gets cert from cache one by one, if found, puts it back to all previous caches.
 func (c *certifyCache) Get(ctx context.Context, key string) (cert *tls.Certificate, err error) {
 	var foundCacheIdx int = -1
 	for i, cache := range c.caches {
@@ -55,7 +60,7 @@ func (c *certifyCache) Get(ctx context.Context, key string) (cert *tls.Certifica
 	return nil, certify.ErrCacheMiss
 }
 
-// Put puts cert to all caches
+// Put puts cert to all caches.
 func (c *certifyCache) Put(ctx context.Context, key string, cert *tls.Certificate) error {
 	for _, cache := range c.caches {
 		if err := cache.Put(ctx, key, cert); err != nil {
@@ -65,7 +70,7 @@ func (c *certifyCache) Put(ctx context.Context, key string, cert *tls.Certificat
 	return nil
 }
 
-// Delete deletes cert from all caches
+// Delete deletes cert from all caches.
 func (c *certifyCache) Delete(ctx context.Context, key string) error {
 	for _, cache := range c.caches {
 		if err := cache.Delete(ctx, key); err != nil {
diff --git a/pkg/issuer/dragonfly_manager.go b/pkg/issuer/dragonfly_manager.go
new file mode 100644
index 00000000..ec9d176d
--- /dev/null
+++ b/pkg/issuer/dragonfly_manager.go
@@ -0,0 +1,165 @@
+/*
+ *     Copyright 2022 The Dragonfly 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 issuer
+
+import (
+	"context"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/tls"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"math/big"
+	"net"
+	"net/url"
+	"time"
+
+	"github.com/johanbrandhorst/certify"
+
+	"d7y.io/dragonfly/v2/pkg/net/ip"
+)
+
+var (
+	// defaultSubjectCommonName is default common name of subject.
+	defaultSubjectCommonName = "manager"
+
+	// defaultSubjectOrganization is default organization of subject.
+	defaultSubjectOrganization = []string{"Dragonfly"}
+
+	// defaultSubjectOrganizationalUnit is default organizational unit of subject.
+	defaultSubjectOrganizationalUnit = []string{"Manager"}
+
+	// defaultIPAddresses is default ip addresses of certificate.
+	defaultIPAddresses = []net.IP{net.ParseIP(ip.IPv4)}
+
+	// defaultDNSNames is default dns names of certificate.
+	defaultDNSNames = []string{"dragonfly-manager", "dragonfly-manager.dragonfly-system.svc"}
+
+	// defaultValidityDuration is default validity duration of certificate.
+	defaultValidityDuration = 10 * 365 * 24 * time.Hour
+)
+
+// GC provides issuer function.
+type dragonflyManagerIssuer struct {
+	tlsCACert        *tls.Certificate
+	dnsNames         []string
+	emailAddresses   []string
+	ipAddresses      []net.IP
+	uris             []*url.URL
+	validityDuration time.Duration
+}
+
+// Option is a functional option for configuring the dragonflyManagerIssuer.
+type Option func(i *dragonflyManagerIssuer)
+
+// WithDNSNames set the dnsNames for dragonflyManagerIssuer.
+func WithDNSNames(dnsNames []string) Option {
+	return func(i *dragonflyManagerIssuer) {
+		i.dnsNames = dnsNames
+	}
+}
+
+// WithEmailAddresses set the emailAddresses for dragonflyManagerIssuer.
+func WithEmailAddresses(emailAddrs []string) Option {
+	return func(i *dragonflyManagerIssuer) {
+		i.emailAddresses = emailAddrs
+	}
+}
+
+// WithIPAddresses set the ipAddresses for dragonflyManagerIssuer.
+func WithIPAddresses(ipAddrs []net.IP) Option {
+	return func(i *dragonflyManagerIssuer) {
+		i.ipAddresses = ipAddrs
+	}
+}
+
+// WithURIs set the uris for dragonflyManagerIssuer.
+func WithURIs(uris []*url.URL) Option {
+	return func(i *dragonflyManagerIssuer) {
+		i.uris = uris
+	}
+}
+
+// WithValidityDuration set the validityDuration for dragonflyManagerIssuer.
+func WithValidityDuration(d time.Duration) Option {
+	return func(i *dragonflyManagerIssuer) {
+		i.validityDuration = d
+	}
+}
+
+// NewDragonflyManagerIssuer returns a new certify.Issuer instence.
+func NewDragonflyManagerIssuer(tlsCACert *tls.Certificate, opts ...Option) certify.Issuer {
+	i := &dragonflyManagerIssuer{
+		tlsCACert:        tlsCACert,
+		dnsNames:         defaultDNSNames,
+		ipAddresses:      defaultIPAddresses,
+		validityDuration: defaultValidityDuration,
+	}
+
+	for _, opt := range opts {
+		opt(i)
+	}
+
+	return i
+}
+
+// Issue returns tls Certificate of issuing.
+func (i *dragonflyManagerIssuer) Issue(ctx context.Context, commonName string, certConfig *certify.CertConfig) (*tls.Certificate, error) {
+	x509CACert, err := x509.ParseCertificate(i.tlsCACert.Certificate[0])
+	if err != nil {
+		return nil, err
+	}
+
+	pk, err := rsa.GenerateKey(rand.Reader, 4096)
+	if err != nil {
+		return nil, err
+	}
+
+	serial, err := rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil))
+	if err != nil {
+		return nil, err
+	}
+
+	now := time.Now()
+	template := x509.Certificate{
+		SerialNumber: serial,
+		Subject: pkix.Name{
+			CommonName:         defaultSubjectCommonName,
+			Organization:       defaultSubjectOrganization,
+			OrganizationalUnit: defaultSubjectOrganizationalUnit,
+		},
+		DNSNames:              i.dnsNames,
+		EmailAddresses:        i.emailAddresses,
+		IPAddresses:           i.ipAddresses,
+		URIs:                  i.uris,
+		NotBefore:             now.Add(-10 * time.Minute).UTC(),
+		NotAfter:              now.Add(i.validityDuration).UTC(),
+		BasicConstraintsValid: true,
+		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageDataEncipherment | x509.KeyUsageKeyEncipherment,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
+	}
+
+	cert, err := x509.CreateCertificate(rand.Reader, &template, x509CACert, &pk.PublicKey, i.tlsCACert.PrivateKey)
+	if err != nil {
+		return nil, err
+	}
+
+	return &tls.Certificate{
+		Certificate: append([][]byte{cert}, i.tlsCACert.Certificate...),
+		PrivateKey:  pk,
+	}, nil
+}
diff --git a/pkg/rpc/credential.go b/pkg/rpc/credential.go
new file mode 100644
index 00000000..e1faab08
--- /dev/null
+++ b/pkg/rpc/credential.go
@@ -0,0 +1,50 @@
+/*
+ *     Copyright 2022 The Dragonfly 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 rpc
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+	"errors"
+
+	"github.com/johanbrandhorst/certify"
+	"google.golang.org/grpc/credentials"
+)
+
+// NewServerCredentialsByCertify returns server transport credentials by certify.
+func NewServerCredentialsByCertify(tlsVerify bool, tlsCACert *tls.Certificate, certifyClient *certify.Certify) (credentials.TransportCredentials, error) {
+	if !tlsVerify {
+		return credentials.NewTLS(&tls.Config{
+			GetCertificate: certifyClient.GetCertificate,
+		}), nil
+	}
+
+	certPool := x509.NewCertPool()
+	for _, cert := range tlsCACert.Certificate {
+		if !certPool.AppendCertsFromPEM(cert) {
+			return nil, errors.New("invalid CA Cert")
+		}
+	}
+
+	tlsConfig := &tls.Config{
+		GetCertificate: certifyClient.GetCertificate,
+		ClientAuth:     tls.RequireAndVerifyClientCert,
+		ClientCAs:      certPool,
+	}
+
+	return credentials.NewTLS(tlsConfig), nil
+}
diff --git a/pkg/rpc/mux.go b/pkg/rpc/mux.go
new file mode 100644
index 00000000..56ae8d0f
--- /dev/null
+++ b/pkg/rpc/mux.go
@@ -0,0 +1,145 @@
+/*
+ *     Copyright 2022 The Dragonfly 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 rpc
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"fmt"
+	"net"
+
+	"github.com/soheilhy/cmux"
+	"google.golang.org/grpc/credentials"
+)
+
+const (
+	// cmux's TLS matcher read at least 3 + 1 bytes
+	tlsRecordPrefix = 4
+)
+
+type muxTransportCredentials struct {
+	credentials credentials.TransportCredentials
+	tlsMatcher  cmux.Matcher
+	tlsPrefer   bool
+}
+
+func WithTLSPreferClientHandshake(prefer bool) func(m *muxTransportCredentials) {
+	return func(m *muxTransportCredentials) {
+		m.tlsPrefer = prefer
+	}
+}
+
+func NewMuxTransportCredentials(c *tls.Config, opts ...func(m *muxTransportCredentials)) credentials.TransportCredentials {
+	m := &muxTransportCredentials{
+		tlsMatcher:  cmux.TLS(),
+		credentials: credentials.NewTLS(c),
+	}
+
+	for _, opt := range opts {
+		opt(m)
+	}
+
+	return m
+}
+
+func (m *muxTransportCredentials) ClientHandshake(ctx context.Context, s string, conn net.Conn) (net.Conn, credentials.AuthInfo, error) {
+	if m.tlsPrefer {
+		return m.credentials.ClientHandshake(ctx, s, conn)
+	}
+	return conn, info{credentials.CommonAuthInfo{SecurityLevel: credentials.NoSecurity}}, nil
+}
+
+func (m *muxTransportCredentials) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) {
+	var prefix = make([]byte, tlsRecordPrefix)
+
+	n, err := conn.Read(prefix)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	if n != tlsRecordPrefix {
+		_ = conn.Close()
+		return nil, nil, fmt.Errorf("short read handshake")
+	}
+
+	conn = &muxConn{
+		Conn: conn,
+		buf:  prefix,
+	}
+
+	// tls
+	if m.tlsMatcher(bytes.NewReader(prefix)) {
+		return m.credentials.ServerHandshake(conn)
+	}
+
+	// non-tls
+	return conn, info{credentials.CommonAuthInfo{SecurityLevel: credentials.NoSecurity}}, nil
+}
+
+func (m *muxTransportCredentials) Info() credentials.ProtocolInfo {
+	return credentials.ProtocolInfo{
+		ProtocolVersion:  "",
+		SecurityProtocol: "mux",
+		ServerName:       "",
+	}
+}
+
+func (m *muxTransportCredentials) Clone() credentials.TransportCredentials {
+	return &muxTransportCredentials{
+		tlsMatcher:  cmux.TLS(),
+		credentials: m.credentials.Clone(),
+	}
+}
+
+func (m *muxTransportCredentials) OverrideServerName(s string) error {
+	return m.credentials.OverrideServerName(s)
+}
+
+// info contains the auth information for an insecure connection.
+// It implements the AuthInfo interface.
+type info struct {
+	credentials.CommonAuthInfo
+}
+
+// AuthType returns the type of info as a string.
+func (info) AuthType() string {
+	return "insecure"
+}
+
+type muxConn struct {
+	net.Conn
+	buf []byte
+}
+
+func (m *muxConn) Read(b []byte) (int, error) {
+	if len(m.buf) == 0 {
+		return m.Conn.Read(b)
+	}
+
+	wn := copy(b, m.buf)
+	if wn < len(m.buf) {
+		m.buf = m.buf[wn:]
+		return wn, nil
+	}
+
+	m.buf = nil
+	b = b[wn:]
+
+	n, err := m.Conn.Read(b)
+	return n + wn, err
+}


Test patch:
diff --git a/manager/rpcserver/cert_test.go b/manager/rpcserver/cert_test.go
index c1a18fb8..84bdeabc 100644
--- a/manager/rpcserver/cert_test.go
+++ b/manager/rpcserver/cert_test.go
@@ -89,7 +89,7 @@ func TestIssueCertificate(t *testing.T) {
 					DB:  &gorm.DB{},
 					RDB: &redis.Client{},
 				},
-				nil, nil, nil, nil, WithCertificate(&ca))
+				nil, nil, nil, nil, WithSelfSignedCert(&ca))
 			require.Nilf(err, "newServer should be ok")
 
 			ctx := peer.NewContext(
@@ -110,7 +110,7 @@ func TestIssueCertificate(t *testing.T) {
 
 			assert.Nilf(err, "IssueCertificate should be ok")
 			assert.NotNilf(resp, "IssueCertificate should not be nil")
-			assert.Equal(len(resp.CertificateChain), len(server.certChain)+1)
+			assert.Equal(len(resp.CertificateChain), 2)
 
 			cert := readCert(resp.CertificateChain[0])
 			assert.Equal(len(cert.IPAddresses), 1)
diff --git a/pkg/rpc/mux_test.go b/pkg/rpc/mux_test.go
new file mode 100644
index 00000000..9cb9a476
--- /dev/null
+++ b/pkg/rpc/mux_test.go
@@ -0,0 +1,69 @@
+/*
+ *     Copyright 2022 The Dragonfly 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 rpc
+
+import (
+	"bytes"
+	"io/ioutil"
+	"net"
+	"testing"
+
+	testifyassert "github.com/stretchr/testify/assert"
+)
+
+type testConn struct {
+	net.Conn
+	buf *bytes.Buffer
+}
+
+func (conn *testConn) Read(b []byte) (int, error) {
+	return conn.buf.Read(b)
+}
+
+func Test_muxConn(t *testing.T) {
+	assert := testifyassert.New(t)
+
+	testCases := []struct {
+		name    string
+		bufSize int
+		data    []byte
+	}{
+		{
+			name:    "buf size equal data size",
+			bufSize: 4,
+			data:    []byte("hell"),
+		},
+		{
+			name:    "buf size less than data size",
+			bufSize: 4,
+			data:    []byte("hello world"),
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			mc := &muxConn{
+				buf:  tc.data[:tc.bufSize],
+				Conn: &testConn{Conn: nil, buf: bytes.NewBuffer(tc.data[tc.bufSize:])},
+			}
+
+			data, err := ioutil.ReadAll(mc)
+			assert.Nil(err, "read all should ok")
+			assert.Equal(tc.data, data, "data shloud be same")
+		})
+	}
+}