Hice este paquete para establecer una conexión virtual con estado entre el cliente y el servidor usando el protocolo UDP para un servidor de juegos golang (como usted sabe, el protocolo UDP no tiene estado, es posible que los paquetes no lleguen en orden y no haya ACK).
El udpsocket
admite una imitación del protocolo de enlace DTLS, criptografía, gestión de sesiones y autenticación. Es responsable de crear un canal seguro entre el cliente y el servidor en UDP (apretón de manos), autenticarlos y administrarlos, descifrar y cifrar los datos y proporcionar una API para enviar o transmitir datos a los clientes y escucharlos.
El servidor udpsocket
acepta algunos parámetros para iniciar:
*net.UDPConn
Las opciones se pueden pasar usando funciones predefinidas:
udpsocket . WithAuthClient ()
udpsocket . WithTranscoder ()
udpsocket . WithSymmetricCrypto ()
udpsocket . WithAsymmetricCrypto ()
udpsocket . WithReadBufferSize ()
udpsocket . WithMinimumPayloadSize ()
udpsocket . WithProtocolVersion ()
udpsocket . WithHeartbeatExpiration ()
udpsocket . WithLogger ()
Una implementación de la interfaz AuthClient
que se utiliza para autenticar el token de usuario. Si su servidor no necesita ninguna autenticación basada en token, no pase nada, de modo que el servidor utilizará la implementación predeterminada que no tiene autenticación.
Una implementación de la interfaz Transcoder
que se utiliza para codificar y decodificar los datos entre el cliente y el servidor. La implementación predeterminada del Transcoder
es Protobuf
. El transcodificador puede codificar y decodificar algunos tipos de mensajes predeterminados, como protocolo de enlace y ping, y también admite métodos generales Marshal
y Unmarshal
para admitir su tipo de datos personalizado.
Una implementación de la interfaz crypto.Symmetric
para cifrar y descifrar mediante algoritmos de claves simétricas. La implementación predeterminada utiliza AES CBC
con relleno PKCS#7
.
Una implementación de la interfaz crypto.Asymmetric
para cifrar y descifrar mediante criptografía de clave pública. La implementación predeterminada utiliza RSA
con un tamaño de clave personalizado.
ReadBufferSize
: Tamaño del búfer de lecturaMinimumPayloadSize
: para cortar los datos que no tienen el tamaño suficiente (para evitar algunos métodos de ataque)ProtocolVersionMajor
: la versión principal del protocolo.ProtocolVersionMinor
: la versión menor del 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
}
El servidor exportó dos métodos para enviar un mensaje al cliente o transmitir un mensaje para todos los clientes. Para enviar un mensaje a un determinado cliente, debe tener el ID del cliente.
La carga útil es un mensaje codificado por Transcoder y el servidor maneja el cifrado.
El controlador es una función con firma func(id string, t byte, p []byte)
. Puede configurar su función de controlador mediante el método SetHandler
. Esta función se llama cuando se recibe y autentica un registro de tipo personalizado. El parámetro id
es el ID del cliente (que se obtiene del token o un nuevo UUID generado si no se requiere autenticación), el parámetro t
es el tipo de registro y el parámetro p
es la carga útil descifrada, debe Unmarshal
en su archivo personalizado. tipo de mensaje.
Cada mensaje del cliente es un Record
. El registro tiene un formato para analizar y descifrar.
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
Quizás se pregunte por qué el tamaño del mensaje cifrado RSA se antepone al registro mientras que el tamaño de la clave privada RSA se conoce y el tamaño del mensaje cifrado se puede calcular. El Server
en realidad usa una interfaz Asymmetric
para el cifrado y RSA la implementa, por lo que tal vez en otras implementaciones, el tamaño del mensaje cifrado no sigue el tamaño de la clave privada, por lo que debemos pasar el tamaño del mensaje cifrado en el protocolo de enlace para poder separar el Secciones cifradas con AES y cifradas con RSA.
El primer byte del registro es el tipo & indica cómo analizar el registro. tipos reservados admitidos:
1
2
3
4
5
El proceso de protocolo de enlace se basa en una imitación del protocolo DTLS, el cliente envía el registro ClientHello
, este registro contiene bytes aleatorios y una clave AES y está cifrado con la clave pública del servidor, que se necesita para descargar en TLS, luego el servidor genera un protocolo aleatorio. cookie basada en los parámetros del cliente, la cifra con la clave AES del cliente (que es recibida por ClientHello
y la envía como registro HelloVerify
. El cliente descifra el registro y repite el mensaje ClientHello
con la cookie, es necesario cifrar el registro con la clave pública del servidor, luego agregar el token de usuario cifrado (con AES) al cuerpo del registro, el servidor registrará al cliente después de la verificación de cookies y autenticará el token de usuario y luego devolverá un registro ServerHello
que contiene un. ID de sesión secreta aleatoria. El proceso de protocolo de enlace se realiza aquí.
Client Server
------ ------
ClientHello ------>
<----- HelloVerifyRequest
(contains cookie)
ClientHello ------>
(with cookie & token)
<----- ServerHello
(contains session ID)
El Server
utiliza cifrados simétricos y asimétricos para comunicarse con el cliente.
El registro ClientHello
(para el servidor) contiene una clave AES segura de 256 bits y cifrada con la clave pública del servidor, por lo que el servidor podría descifrarla con la clave privada.
El registro HelloVerify
(para el cliente) se cifra con la clave AES del cliente (que se descifró antes).
Si se requiere autenticación de usuario, el cliente debe enviar el token de usuario con ClientHello
(un registro que también contiene cookies), pero el cifrado asimétrico tiene una limitación de tamaño. por ejemplo, RSA solo puede cifrar datos hasta una cantidad máxima igual al tamaño de la clave (por ejemplo, 2048 bits = 256 bytes) y el tamaño del token de usuario podría ser mayor, por lo que el cliente AES debe cifrar el token de usuario, luego el servidor podría descifrarlo después de la validación del registro ClientHello
.
La estructura del registro de protocolo de enlace es un poco diferente debido al uso de cifrado híbrido. Los dos bytes después de los bytes de la versión del protocolo indican el tamaño del cuerpo del protocolo de enlace que está cifrado por la clave pública del servidor. el tamaño del cuerpo del protocolo de enlace pasa debido al tamaño de la clave, el tamaño del cuerpo cifrado depende del tamaño de la clave 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
Después de un protocolo de enlace exitoso, el servidor devuelve un SessionID
secreto en el registro ServerHello
que está cifrado con la clave AES del cliente. El cliente debe agregar el SessionID
a los bytes del registro personalizado, luego cifrarlo con la clave AES, agregar los encabezados del registro (tipo de registro y versión del protocolo) y luego enviar los bytes al servidor. El servidor descifrará el cuerpo del registro mediante la clave AES del cliente (que está registrada anteriormente con la IP y el puerto del cliente), luego analizará el SessionID
del cuerpo descifrado, autorizará el ID de la sesión y pasará los bytes a la función Handler
.
Para el registro Ping
, el servidor envía inmediatamente un registro Pong
al cliente.