Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ result.db.*
dump.data
runtime.trace

bin/redis-full-check
bin/redis-full-check*

.DS_Store

Expand Down
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,21 @@ Application Options:
-s, --source=SOURCE Set host:port of source redis.
-p, --sourcepassword=Password Set source redis password (format: password or username:password)
--sourceauthtype=AUTH-TYPE useless for opensource redis, valid value:auth/adminauth (default: auth)
--sourcetls Enable TLS for all source Redis connections
--sourcetlscacert=FILE CA certificate file for source Redis TLS verification
--sourcetlscert=FILE Client certificate file for source Redis mutual TLS
--sourcetlskey=FILE Client private key file for source Redis mutual TLS
--sourcetlsservername=NAME Override the source Redis TLS certificate server name
--sourcetlsskipverify Skip source Redis TLS certificate verification (insecure)
-t, --target=TARGET Set host:port of target redis.
-a, --targetpassword=Password Set target redis password (format: password or username:password)
--targetauthtype=AUTH-TYPE useless for opensource redis, valid value:auth/adminauth (default: auth)
--targettls Enable TLS for all target Redis connections
--targettlscacert=FILE CA certificate file for target Redis TLS verification
--targettlscert=FILE Client certificate file for target Redis mutual TLS
--targettlskey=FILE Client private key file for target Redis mutual TLS
--targettlsservername=NAME Override the target Redis TLS certificate server name
--targettlsskipverify Skip target Redis TLS certificate verification (insecure)
-d, --db=Sqlite3-DB-FILE sqlite3 db file for store result. If exist, it will be removed and a new file is created. (default: result.db)
--comparetimes=COUNT Total compare count, at least 1. In the first round, all keys will be compared. The subsequent rounds of the comparison
will be done on the previous results. (default: 3)
Expand Down Expand Up @@ -82,6 +94,26 @@ Or you can build redis-full-check yourself according to the following steps:<br>
* ./build.sh
* ./bin/redis-full-check -s $(source_redis_ip_port) -p $(source_password) -t $(target_redis_ip_port) -a $(target_password) # these parameters should be given by users

## TLS

TLS is configured independently for the source and target. If the CA is already trusted by the operating system, the `tlscacert` option can be omitted. Client certificate and key options must be specified together when mutual TLS is required.

```sh
./bin/redis-full-check \
-s source.example.com:6379 -p source_password --sourcetls --sourcetlscacert /path/to/ca.crt \
-t target.example.com:6379 -a target_password --targettls --targettlscacert /path/to/ca.crt
```

In Redis Cluster mode, TLS applies to the startup node, every primary returned by `CLUSTER SLOTS`, redirects, and the per-node full scan connections. Use the role prefix to discover all primary nodes from one startup node:

```sh
./bin/redis-full-check \
-s 'master@redis-cluster.example.com:6379' --sourcedbtype 1 --sourcetls --sourcetlscacert /path/to/ca.crt \
-t 'master@target-cluster.example.com:6379' --targetdbtype 1 --targettls --targettlscacert /path/to/ca.crt
```

By default, each cluster node certificate is verified against the hostname or IP advertised by Redis. Set `sourcetlsservername` or `targettlsservername` only when every node certificate uses the same explicit server name. The `tlsskipverify` options disable certificate verification and should only be used for temporary diagnostics.

Here comes the sqlite3 example to display the conflict result:<br>
```
$ sqlite3 result.db.3 # result.db.x shows the x-round comparison conflict result. len == -1 means inconsistent key type.
Expand Down Expand Up @@ -114,4 +146,4 @@ We also provide some tools for synchronization in Shake series.<br>

# License
- On `20230427` and later, we distribute this library under the new [Apache2.0](https://www.apache.org/licenses/LICENSE-2.0) protocol, `1.4.10` is the first version to support the Apache2.0 protocol.
- Prior to 20230427, it was distributed under the [GPLV3.0](https://www.gnu.org/licenses/gpl-3.0.html) protocol.
- Prior to 20230427, it was distributed under the [GPLV3.0](https://www.gnu.org/licenses/gpl-3.0.html) protocol.
20 changes: 11 additions & 9 deletions src/full_check/client/address.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package client

import (
"strings"
"crypto/tls"
"fmt"
"strings"

"full_check/common"
)
Expand All @@ -15,7 +16,7 @@ const (
RoleSlave = "slave"
)

func HandleAddress(address, password, authType string) ([]string, error) {
func HandleAddress(address, password, authType string, tlsConfig *tls.Config) ([]string, error) {
if strings.Contains(address, AddressSplitter) {
arr := strings.Split(address, AddressSplitter)
if len(arr) != 2 {
Expand All @@ -33,15 +34,15 @@ func HandleAddress(address, password, authType string) ([]string, error) {
role = RoleMaster
}

return fetchNodeList(clusterList[0], password, authType, role)
return fetchNodeList(clusterList[0], password, authType, role, tlsConfig)
} else {
clusterList := strings.Split(address, AddressClusterSplitter)
if len(clusterList) <= 1 {
return clusterList, nil
}

// fetch master
masterList, err := fetchNodeList(clusterList[0], password, authType, common.TypeMaster)
masterList, err := fetchNodeList(clusterList[0], password, authType, common.TypeMaster, tlsConfig)
if err != nil {
return nil, err
}
Expand All @@ -50,7 +51,7 @@ func HandleAddress(address, password, authType string) ([]string, error) {
return clusterList, nil
}

slaveList, err := fetchNodeList(clusterList[0], password, authType, common.TypeSlave)
slaveList, err := fetchNodeList(clusterList[0], password, authType, common.TypeSlave, tlsConfig)
if err != nil {
return nil, err
}
Expand All @@ -65,12 +66,13 @@ func HandleAddress(address, password, authType string) ([]string, error) {
}
}

func fetchNodeList(oneNode, password, authType, role string) ([]string, error) {
func fetchNodeList(oneNode, password, authType, role string, tlsConfig *tls.Config) ([]string, error) {
// create client to fetch
client, err := NewRedisClient(RedisHost{
Addr: []string{oneNode},
Password: password,
Authtype: authType,
Addr: []string{oneNode},
Password: password,
Authtype: authType,
TLSConfig: tlsConfig,
}, 0)
if err != nil {
return nil, fmt.Errorf("fetch cluster info failed[%v]", err)
Expand Down
33 changes: 25 additions & 8 deletions src/full_check/client/client.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package client

import (
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"strconv"
"strings"
"time"
"errors"

"full_check/common"

Expand All @@ -28,6 +29,7 @@ type RedisHost struct {
Authtype string // "auth" or "adminauth"
DBType int
DBFilterList map[int]struct{} // whitelist
TLSConfig *tls.Config
}

func (p RedisHost) String() string {
Expand Down Expand Up @@ -91,12 +93,15 @@ func (p *RedisClient) Connect() error {
var err error
if p.redisHost.IsCluster() == false {
// single db or proxy
if p.redisHost.TimeoutMs == 0 {
p.conn, err = redis.Dial("tcp", p.redisHost.Addr[0])
} else {
p.conn, err = redis.DialTimeout("tcp", p.redisHost.Addr[0], time.Millisecond*time.Duration(p.redisHost.TimeoutMs),
time.Millisecond*time.Duration(p.redisHost.TimeoutMs), time.Millisecond*time.Duration(p.redisHost.TimeoutMs))
}
p.conn, err = p.dial(p.redisHost.Addr[0])
} else if p.redisHost.TLSConfig != nil {
p.conn, err = NewTLSClusterConn(TLSClusterOptions{
StartNodes: p.redisHost.Addr,
Password: p.redisHost.Password,
AuthType: p.redisHost.Authtype,
Timeout: time.Duration(p.redisHost.TimeoutMs) * time.Millisecond,
TLSConfig: p.redisHost.TLSConfig,
})
} else {
// cluster
cluster, err := redigoCluster.NewCluster(
Expand Down Expand Up @@ -140,6 +145,18 @@ func (p *RedisClient) Connect() error {
return nil
}

func (p *RedisClient) dial(address string) (redis.Conn, error) {
options := make([]redis.DialOption, 0, 5)
if p.redisHost.TimeoutMs != 0 {
timeout := time.Duration(p.redisHost.TimeoutMs) * time.Millisecond
options = append(options, redis.DialConnectTimeout(timeout), redis.DialReadTimeout(timeout), redis.DialWriteTimeout(timeout))
}
if p.redisHost.TLSConfig != nil {
options = append(options, redis.DialUseTLS(true), redis.DialTLSConfig(p.redisHost.TLSConfig))
}
return redis.Dial("tcp", address, options...)
}

func (p *RedisClient) Do(commandName string, args ...interface{}) (interface{}, error) {
var err error
var result interface{}
Expand Down Expand Up @@ -179,7 +196,7 @@ type combine struct {
}

func (c combine) String() string {
all := make([]string, 0, len(c.params) + 1)
all := make([]string, 0, len(c.params)+1)
all = append(all, c.command)
for _, ele := range c.params {
all = append(all, string(ele.([]byte)))
Expand Down
53 changes: 53 additions & 0 deletions src/full_check/client/tls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package client

import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
)

func LoadTLSConfig(enabled bool, caFile, certFile, keyFile, serverName string, skipVerify bool) (*tls.Config, error) {
if !enabled {
if caFile != "" || certFile != "" || keyFile != "" || serverName != "" || skipVerify {
return nil, fmt.Errorf("TLS options require TLS to be enabled")
}
return nil, nil
}

if (certFile == "") != (keyFile == "") {
return nil, fmt.Errorf("TLS client certificate and key must be specified together")
}

config := &tls.Config{
MinVersion: tls.VersionTLS12,
ServerName: serverName,
InsecureSkipVerify: skipVerify,
}

if caFile != "" {
caPEM, err := os.ReadFile(caFile)
if err != nil {
return nil, fmt.Errorf("read TLS CA certificate %q: %w", caFile, err)
}

rootCAs, err := x509.SystemCertPool()
if err != nil || rootCAs == nil {
rootCAs = x509.NewCertPool()
}
if !rootCAs.AppendCertsFromPEM(caPEM) {
return nil, fmt.Errorf("TLS CA certificate %q does not contain a valid PEM certificate", caFile)
}
config.RootCAs = rootCAs
}

if certFile != "" {
certificate, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, fmt.Errorf("load TLS client certificate %q and key %q: %w", certFile, keyFile, err)
}
config.Certificates = []tls.Certificate{certificate}
}

return config, nil
}
Loading