Eu fiz este pacote para fazer uma conexão virtual com estado entre o cliente e o servidor usando o protocolo UDP para um servidor de jogo golang (como você sabe, o protocolo UDP não tem estado, os pacotes podem não chegar em ordem e não há ACK).
O udpsocket
suporta uma simulação de handshake DTLS, criptografia, gerenciamento de sessão e autenticação. É responsável por criar um canal seguro entre o cliente e o servidor no UDP (handshake), autenticá-los e gerenciá-los, descriptografar e criptografar os dados e fornecer uma API para enviar ou transmitir dados aos clientes e ouvi-los.
O servidor udpsocket
aceita alguns parâmetros para iniciar:
*net.UDPConn
As opções podem ser passadas usando funções predefinidas:
udpsocket . WithAuthClient ()
udpsocket . WithTranscoder ()
udpsocket . WithSymmetricCrypto ()
udpsocket . WithAsymmetricCrypto ()
udpsocket . WithReadBufferSize ()
udpsocket . WithMinimumPayloadSize ()
udpsocket . WithProtocolVersion ()
udpsocket . WithHeartbeatExpiration ()
udpsocket . WithLogger ()
Uma implementação da interface AuthClient
usada para autenticar o token do usuário. Se o seu servidor não precisar de nenhuma autenticação baseada em token, não passe nada, então o servidor usará a implementação padrão que não possui autenticação.
Uma implementação da interface Transcoder
que é usada para codificar e decodificar os dados entre o cliente e o servidor. A implementação padrão do Transcoder
é Protobuf
. O transcodificador pode codificar e decodificar alguns tipos de mensagens padrão, como handshake e ping, e também oferece suporte a métodos gerais Marshal
e Unmarshal
para oferecer suporte ao seu tipo de dados personalizado.
Uma implementação da interface crypto.Symmetric
para criptografar e descriptografar por meio de algoritmos de chaves simétricas. A implementação padrão usa AES CBC
com preenchimento PKCS#7
.
Uma implementação de interface crypto.Asymmetric
para criptografar e descriptografar por meio de criptografia de chave pública. A implementação padrão usa RSA
com tamanho de chave personalizado.
ReadBufferSize
: Tamanho do buffer de leituraMinimumPayloadSize
: para cortar os dados que não possuem tamanho suficiente (para evitar alguns métodos de ataque)ProtocolVersionMajor
: a versão principal do protocoloProtocolVersionMinor
: a versão secundária do protocolo 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
}
O servidor exportou dois métodos para enviar uma mensagem ao cliente ou transmitir uma mensagem para todos os clientes. Para enviar uma mensagem para um determinado cliente, você deve ter o ID do cliente.
A carga útil é uma mensagem codificada pelo Transcoder e a criptografia é tratada pelo servidor.
O manipulador é uma função com assinatura func(id string, t byte, p []byte)
. Você pode definir sua função de manipulador pelo método SetHandler
. Esta função é chamada quando um registro de tipo personalizado é recebido e autenticado. O parâmetro id
é o ID do cliente (que é obtido do token ou de um novo UUID gerado se nenhuma autenticação for necessária), o parâmetro t
é o tipo de registro e o parâmetro p
é a carga útil descriptografada, você deve Unmarshal
-lo de acordo com seu costume tipo de mensagem.
Cada mensagem do cliente é um Record
. O registro tem um formato para analisar e descriptografar.
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
Você pode perguntar por que o tamanho da mensagem criptografada RSA é anexado ao registro enquanto o tamanho da chave privada RSA é conhecido e o tamanho da mensagem criptografada é calculável. O Server
na verdade usa interface Asymmetric
para criptografia e o RSA a implementa, então talvez em outras implementações, o tamanho da mensagem criptografada não siga o tamanho da chave privada, portanto, precisamos passar o tamanho da mensagem criptografada no handshake para poder separar o Seções criptografadas AES e criptografadas RSA.
O primeiro byte do registro é o tipo e indica como analisar o registro. tipos reservados suportados:
1
2
3
4
5
O processo de handshake é feito com base em uma imitação do protocolo DTLS, o cliente envia o registro ClientHello
, este registro contém bytes aleatórios e chave AES e criptografado pela chave pública do servidor, que é necessária para download no TLS, então o servidor gera um aleatório cookie com base nos parâmetros do cliente, criptografa-o com a chave AES do cliente (que é recebida por ClientHello
e o envia como registro HelloVerify
. O cliente descriptografa o registro e repete a mensagem ClientHello
com o cookie, o registro é precisava ser criptografado com a chave pública do servidor e, em seguida, anexa o token do usuário criptografado (com AES) ao corpo do registro, o servidor registrará o cliente após a verificação do cookie e autenticará o token do usuário e retornará um registro ServerHello
contendo uma sessão secreta aleatória. ID. O processo de handshake é feito aqui.
Client Server
------ ------
ClientHello ------>
<----- HelloVerifyRequest
(contains cookie)
ClientHello ------>
(with cookie & token)
<----- ServerHello
(contains session ID)
O Server
usa criptografia simétrica e assimétrica para se comunicar com o cliente.
O registro ClientHello
(para o servidor) contém uma chave AES segura de 256 bits e criptografada pela chave pública do servidor, para que o servidor possa descriptografá-la com a chave privada.
O registro HelloVerify
(para o cliente) criptografa com a chave AES do cliente (que foi descriptografada anteriormente).
Se a autenticação do usuário for necessária, o cliente deverá enviar o token do usuário com ClientHello
(um registro que também contém cookie), mas a criptografia assimétrica tem uma limitação de tamanho. por exemplo, o RSA só é capaz de criptografar dados em uma quantidade máxima igual ao tamanho da chave (por exemplo, 2.048 bits = 256 bytes) e o tamanho do token do usuário pode ser maior, portanto, o token do usuário deve ser criptografado pelo AES do cliente, então o servidor poderia descriptografá-lo após a validação do registro ClientHello
.
A estrutura do registro de handshake é um pouco diferente devido ao uso de criptografia híbrida. os dois bytes após os bytes da versão do protocolo indicam o tamanho do corpo do handshake que é criptografado pela chave pública do servidor. o tamanho do corpo do handshake está passando devido ao tamanho da chave, o tamanho do corpo criptografado depende do tamanho da chave 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
Após um handshake bem-sucedido, o servidor retorna um SessionID
secreto no registro ServerHello
que é criptografado pela chave AES do cliente. O cliente deve anexar o SessionID
aos bytes de registro personalizados, depois criptografá-lo pela chave AES, adicionar os cabeçalhos de registro (tipo de registro e versão do protocolo) e, em seguida, enviar os bytes ao servidor. O servidor irá descriptografar o corpo do registro pela chave AES do cliente (que é registrada anteriormente com o IP e porta do cliente), em seguida, analisará o SessionID
do corpo descriptografado, autorizará o ID da sessão e passará os bytes para a função Handler
.
Para registro Ping
, o servidor envia imediatamente um registro Pong
para o cliente.