我制作这个包是为了使用 golang 游戏服务器的 UDP 协议在客户端和服务器之间建立虚拟有状态连接(如您所知,UDP 协议是无状态的,数据包可能不会按顺序到达并且没有 ACK)。
udpsocket
支持模拟 DTLS 握手、加密、会话管理和身份验证。它负责在 UDP(握手)上的客户端和服务器之间建立安全通道,对它们进行身份验证和管理,解密和加密数据,并提供 API 来向客户端发送或广播数据并监听它们。
udpsocket
服务器接受一些参数来启动:
*net.UDPConn
的实例可以使用预定义函数传递选项:
udpsocket . WithAuthClient ()
udpsocket . WithTranscoder ()
udpsocket . WithSymmetricCrypto ()
udpsocket . WithAsymmetricCrypto ()
udpsocket . WithReadBufferSize ()
udpsocket . WithMinimumPayloadSize ()
udpsocket . WithProtocolVersion ()
udpsocket . WithHeartbeatExpiration ()
udpsocket . WithLogger ()
AuthClient
接口的实现,用于验证用户令牌。如果您的服务器不需要任何基于令牌的身份验证,请不要传递任何内容,因此服务器将使用没有身份验证的默认实现。
Transcoder
接口的实现,用于对客户端和服务器之间的数据进行编码和解码。 Transcoder
的默认实现是Protobuf
。转码器可以对一些默认消息类型进行编码和解码,例如握手和 ping,并且还支持常规Marshal
和Unmarshal
方法来支持您的自定义数据类型。
crypto.Symmetric
接口的实现,用于通过对称密钥算法进行加密和解密。默认实现使用带有PKCS#7
填充的AES CBC
。
crypto.Asymmetric
接口的实现,用于通过公钥加密进行加密和解密。默认实现使用具有自定义密钥大小的RSA
。
ReadBufferSize
:读取缓冲区的大小MinimumPayloadSize
:切割大小不够的数据(防止某些攻击方法)ProtocolVersionMajor
:协议主要版本ProtocolVersionMinor
:协议次要版本 package main
import (
"context"
"log"
"net"
"fmt"
"crypto/rsa"
"time"
"os"
"demo/auth"
"demo/encoding"
"github.com/theredrad/udpsocket"
"github.com/theredrad/udpsocket/crypto"
)
var (
pk * rsa. PrivateKey
udpServerIP = "127.0.0.1"
udpServerPort = "7009"
defaultRSAPrivateKeySize = 2048
)
func main () {
f , err := auth . NewFirebaseClient ( context . Background (), "firebase-config.json" ) // firebase implementation of auth client to validate firebase-issued tokens
if err != nil {
panic ( err )
}
udpAddr , err := net . ResolveUDPAddr ( "udp" , fmt . Sprintf ( "%s:%s" , udpServerIP , udpServerPort ))
if err != nil {
panic ( err )
}
udpConn , err := net . ListenUDP ( "udp" , udpAddr )
if err != nil {
panic ( err )
}
defer udpConn . Close ()
pk , err = crypto . GenerateRSAKey ( defaultRSAPrivateKeySize )
if err != nil {
panic ( err )
}
r := crypto . NewRSAFromPK ( pk ) // creating a new instance of the RSA implementation
if err != nil {
panic ( err )
}
a := crypto . NewAES ( crypto . AES_CBC ) // creating a new instance of the AES implementation
t := & encoding. MessagePack {} // an implementation of msgpack for the Transcoder
s , err := udpsocket . NewServer ( udpConn ,
udpsocket . WithAuthClient ( f ),
udpsocket . WithTranscoder ( t ),
udpsocket . WithSymmetricCrypto ( a ),
udpsocket . WithAsymmetricCrypto ( r ),
udpsocket . WithReadBufferSize ( 2048 ),
udpsocket . WithMinimumPayloadSize ( 4 ),
udpsocket . WithProtocolVersion ( 1 , 0 ),
udpsocket . WithHeartbeatExpiration ( 3 * time . Second ),
udpsocket . WithLogger ( log . New ( os . Stdout , "udp server: " , log . Ldate )),
)
if err != nil {
panic ( err )
}
s . SetHandler ( incomingHandler )
go s . Serve () // start to run the server, listen to incoming records
// TODO: need to serve the public key on HTTPS (TLS) to secure the download for the client
}
func incomingHandler ( id string , t byte , p [] byte ) {
// handle the incoming
}
服务器导出两种方法来向客户端发送消息或向所有客户端广播消息。要向特定客户端发送消息,您必须拥有客户端 ID。
有效负载是转码器编码的消息,并由服务器处理加密。
该处理程序是一个具有func(id string, t byte, p []byte)
签名的函数。您可以通过SetHandler
方法设置处理程序函数。当收到并验证自定义类型记录时调用此函数。 id
参数是客户端 ID(从令牌中获取,如果不需要身份验证,则从新生成的 UUID 获取), t
参数是记录类型, p
参数是解密的有效负载,您必须Unmarshal
为您的自定义消息类型。
来自客户端的每条消息都是一个Record
。该记录具有解析和解密的格式。
1 0 1 1 0 2 52 91 253 115 22 78 39 28 5 192 47 211...
|-| |-| |-| |------------------------------------------|
a b c d
a: record type
b: record protocol major version
c: record protocol minor version
d: record body
您可能会问,既然 RSA 私钥大小已知并且加密消息大小是可计算的,为什么 RSA 加密消息大小会预先添加到记录中呢? Server
实际上使用Asymmetric
接口进行加密,RSA实现它,所以也许在其他实现中,加密消息大小不遵循私钥大小,因此我们需要在握手中传递加密消息大小以便能够分离AES 加密和 RSA 加密部分。
记录的第一个字节是类型&指示如何解析记录。支持的保留类型:
1
2
3
4
5
握手过程是基于 DTLS 协议的模仿,客户端发送ClientHello
记录,该记录包含随机字节和 AES 密钥并由服务器公钥加密,需要在 TLS 上下载,然后服务器生成一个随机字节基于客户端参数的cookie,使用客户端AES密钥对其进行加密(由ClientHello
接收并将其作为HelloVerify
记录发送。客户端解密该记录并使用cookie重复ClientHello
消息,该记录需要与服务器加密公钥,然后将加密的用户令牌(使用 AES)附加到记录主体中,服务器将在 cookie 验证后注册客户端并验证用户令牌,然后返回包含随机秘密会话 ID 的ServerHello
记录。这里。
Client Server
------ ------
ClientHello ------>
<----- HelloVerifyRequest
(contains cookie)
ClientHello ------>
(with cookie & token)
<----- ServerHello
(contains session ID)
Server
使用对称和非对称加密与客户端通信。
ClientHello
记录(对于服务器)包含一个安全的 256 位 AES 密钥,并由服务器公钥加密,因此服务器可以使用私钥对其进行解密。
HelloVerify
记录(对于客户端)使用客户端 AES 密钥(之前已解密)进行加密。
如果需要进行用户认证,客户端必须将用户令牌与ClientHello
(一条包含cookie的记录)一起发送,但非对称加密有大小限制。例如,RSA 只能将数据加密到等于密钥大小的最大数量(例如 2048 位 = 256 字节),并且用户令牌大小可能更大,因此用户令牌必须由客户端 AES 加密,然后服务器可以在验证ClientHello
记录后对其进行解密。
由于使用混合加密,握手记录结构略有不同。协议版本字节后的两个字节表示由服务器公钥加密的握手体的大小。握手正文大小由于密钥大小而传递,加密正文大小取决于 RSA 密钥大小。
1 0 1 1 0 2 52 91 253 115 22 78 39 28 5 192 47 211 ... 4 22 64 91 195 37 225
|-| |-| |-| |-| |-| |----------------------------------------| |---------------------|
a b c d e f g
a: record type (1 => handshake)
b: record protocol major version (0 => 0.1v)
c: record protocol minor version (1 => 0.1v)
d: handshake body size ([1] 0 => 256 bytes, key size: 2048 bits) (first digit number in base of 256)
e: handshake body size (1 [0] => 256 bytes, key size: 2048 bits) (second digit number in base of 256)
f: handshake body which is encrypted by the server public key & contains the client AES key
g: user token which is encrypted by the client AES key size
握手成功后,服务器会在ServerHello
记录中返回一个由客户端 AES 密钥加密的秘密SessionID
。客户端必须将SessionID
附加到自定义记录字节,然后使用 AES 密钥对其进行加密,添加记录标头(记录类型和协议版本),然后将字节发送到服务器。服务器将通过客户端 AES 密钥(之前使用客户端的 IP 和端口注册)解密记录正文,然后从解密的正文中解析SessionID
,授权会话 ID 并将字节传递给Handler
函数。
对于Ping
记录,服务器立即向客户端发送Pong
记录。