最近需要用到服务器的Redis数据库,但又不想让数据库的监听端口暴露到外网,另外Redis本身不支持数据加密。还有什么方法让本地客户端连上服务器的数据库呢?

答案是使用代理。接下来的实现使用Go语言,由于它的静态连接特性,编译后放到服务器即可。它的思路很简单,把本地客户端的数据加密后送到服务器,服务器接收后解密转发到本地指定的端口。

1
2
client --> localproxy --加密的数据--> remoteproxy -转发到-> Redis
127.0.0.1:7744 remoteAddr:443 127.0.0.1:6379

如果client支持tls可以把localproxy这过程免去,即

1
2
tlsclient --加密的数据--> remoteproxy -转发到-> Redis
port:443 port:6379

下面的是TCP代理的实现,交叉编译后部署到Linux服务器后运行即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"io"
"log"
"net"
)

func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
l, err := net.Listen("tcp", ":62815")
if err != nil {
log.Panic(err)
}
for {
client, err := l.Accept()
if err != nil {
log.Println(err)
continue
}
db, err := net.Dial("tcp", "localhost:6379") // connect to Redis
if err != nil {
log.Println(err)
continue
}
go io.Copy(db, client) // copy client data to database
io.Copy(client, db) // copy database data to client
}
}

TCP本身并不安全,我们需要在TCP上添加一层加密SSL。可以通过openssl创建服务器的证书和密钥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"crypto/tls"
"io"
"log"
"net"
)

func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
cer, err := tls.LoadX509KeyPair("server.crt", "server.key")
if err != nil {
log.Fatal(err)
}
config := &tls.Config{Certificates: []tls.Certificate{cer}}

l, err := tls.Listen("tcp", ":62815", config)
defer l.Close()
if err != nil {
log.Panic(err)
}

for {
client, err := l.Accept()
if err != nil {
log.Println(err)
continue
}
go handleTCPRequest(client)
}
}

func handleTCPRequest(client net.Conn) {
remote, err := net.Dial("tcp", "localhost:6379") // connect to Redis
if err != nil {
log.Println(err)
return
}
go io.Copy(remote, client) // copy client data to database
io.Copy(client, remote) // copy database data to client
}

客户端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
"crypto/tls"
"io"
"log"
"net"
)

func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)

local, err := net.Listen("tcp", ":7744")
defer local.Close()
if err != nil {
log.Fatal(err)
}

for {
client, err := local.Accept()
if err != nil {
log.Println(err)
continue
}
go handleTLSRequest(client)

}
}

func handleTLSRequest(client net.Conn) {
conf := &tls.Config{}
remote, err := tls.Dial("tcp", "localhost:443", conf)
defer remote.Close()
if err != nil {
log.Fatal(err)
}

go io.Copy(remote, client)
io.Copy(client, remote)
}

上面的代码整合下写成命令行工具即可方便使用。如果client支持tls可以把localproxy这过程免去。

事实上,这种方法不仅适用于Redis,还适用于MongoDBMySQL,不过后两者就已经有SSL功能。从官方文档看,Redis推荐使用 spipe 作数据加密。另外,利用类似的方法不难让数据库走socks5代理,以后有空写写。

除此之外,还可以为代理添加其他功能:为多数据库作负载均衡、读写分离。不过数据加密解密和端口转发还是有一定的延时。