我製作這個套件是為了使用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
記錄。