J'ai créé ce package pour établir une connexion virtuelle avec état entre le client et le serveur en utilisant le protocole UDP pour un serveur de jeu Golang (comme vous le savez, le protocole UDP est sans état, les paquets peuvent ne pas arriver dans l'ordre et il n'y a pas d'ACK).
L' udpsocket
prend en charge une imitation de la prise de contact DTLS, de la cryptographie, de la gestion de session et de l'authentification. Il est chargé de créer un canal sécurisé entre le client et le serveur sur UDP (handshake), de les authentifier et de les gérer, de décrypter et de chiffrer les données et de fournir une API pour envoyer ou diffuser des données aux clients et les écouter.
Le serveur udpsocket
accepte certains paramètres pour initier :
*net.UDPConn
Les options peuvent être transmises à l'aide de fonctions prédéfinies :
udpsocket . WithAuthClient ()
udpsocket . WithTranscoder ()
udpsocket . WithSymmetricCrypto ()
udpsocket . WithAsymmetricCrypto ()
udpsocket . WithReadBufferSize ()
udpsocket . WithMinimumPayloadSize ()
udpsocket . WithProtocolVersion ()
udpsocket . WithHeartbeatExpiration ()
udpsocket . WithLogger ()
Une implémentation de l'interface AuthClient
utilisée pour authentifier le jeton utilisateur. Si votre serveur n'a besoin d'aucune authentification basée sur un jeton, ne transmettez rien, le serveur utilisera donc son implémentation par défaut qui n'a aucune authentification.
Une implémentation de l'interface Transcoder
utilisée pour encoder et décoder les données entre le client et le serveur. L'implémentation par défaut du Transcoder
est Protobuf
. Le transcodeur peut encoder et décoder certains types de messages par défaut, comme la prise de contact et le ping, et prend également en charge les méthodes générales Marshal
et Unmarshal
pour prendre en charge votre type de données personnalisé.
Une implémentation de l'interface crypto.Symmetric
pour crypter et déchiffrer via des algorithmes à clés symétriques. L'implémentation par défaut utilise AES CBC
avec le remplissage PKCS#7
.
Une implémentation d'une interface crypto.Asymmetric
pour crypter et déchiffrer via la cryptographie à clé publique. L'implémentation par défaut utilise RSA
avec une taille de clé personnalisée.
ReadBufferSize
: Taille du buffer de lectureMinimumPayloadSize
: pour couper les données qui n'ont pas assez de taille (pour éviter certaines méthodes d'attaque)ProtocolVersionMajor
: la version majeure du protocoleProtocolVersionMinor
: la version mineure du protocole 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
}
Le serveur a exporté deux méthodes pour envoyer un message au client ou diffuser un message pour tous les clients. Pour envoyer un message à un certain client, vous devez disposer de l'ID client.
La charge utile est un message codé par transcodeur et chiffré est géré par le serveur.
Le gestionnaire est une fonction avec la signature func(id string, t byte, p []byte)
. Vous pouvez définir votre fonction de gestionnaire par la méthode SetHandler
. Cette fonction est appelée lorsqu'un enregistrement de type personnalisé est reçu et authentifié. Le paramètre id
est l'ID client (qui est récupéré à partir du jeton, ou un nouvel UUID généré si aucune authentification n'est requise), le paramètre t
est le type d'enregistrement et le paramètre p
est la charge utile déchiffrée, vous devez le Unmarshal
selon votre personnalisation. type de message.
Chaque message du client est un Record
. L'enregistrement a un format pour analyser et décrypter.
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
Vous pouvez vous demander pourquoi la taille du message chiffré RSA est ajoutée au début de l'enregistrement alors que la taille de la clé privée RSA est connue et que la taille du message chiffré est calculable. Le Server
utilise en fait une interface Asymmetric
pour le chiffrement et RSA l'implémente, donc peut-être que dans d'autres implémentations, la taille du message chiffré ne suit pas la taille de la clé privée, nous devons donc transmettre la taille du message chiffré lors de l'établissement de liaison pour pouvoir séparer le Sections chiffrées AES et RSA.
Le premier octet de l'enregistrement est le type et indique comment analyser l'enregistrement. types réservés pris en charge :
1
2
3
4
5
Le processus de prise de contact est basé sur une imitation du protocole DTLS, le client envoie un enregistrement ClientHello
, cet enregistrement contient des octets aléatoires et une clé AES et crypté par la clé publique du serveur, qui est nécessaire pour télécharger sur TLS, puis le serveur génère un aléatoire cookie basé sur les paramètres du client, le crypte avec la clé AES du client (qui est reçue par ClientHello
et l'envoie en tant qu'enregistrement HelloVerify
. Le client déchiffre l'enregistrement et répète le message ClientHello
avec le cookie, l'enregistrement doit être chiffré avec la clé publique du serveur, puis ajoutez le jeton utilisateur chiffré (avec AES) au corps de l'enregistrement, le serveur enregistrera le client après la vérification du cookie et authentifiera le jeton utilisateur, puis renverra un enregistrement ServerHello
contenant un secret aléatoire. ID de session. Le processus de prise de contact est effectué ici.
Client Server
------ ------
ClientHello ------>
<----- HelloVerifyRequest
(contains cookie)
ClientHello ------>
(with cookie & token)
<----- ServerHello
(contains session ID)
Le Server
utilise des cryptages symétriques et asymétriques pour communiquer avec le client.
L'enregistrement ClientHello
(pour le serveur) contient une clé AES sécurisée de 256 bits et chiffrée par la clé publique du serveur, afin que le serveur puisse la déchiffrer avec la clé privée.
L'enregistrement HelloVerify
(pour le client) chiffre avec la clé AES du client (qui a été déchiffrée auparavant).
Si l'authentification de l'utilisateur est requise, le client doit envoyer le jeton utilisateur avec ClientHello
(un enregistrement qui contient également un cookie), mais le cryptage asymétrique a une limitation de taille. par exemple, le RSA est uniquement capable de chiffrer les données jusqu'à un montant maximum égal à la taille de la clé (par exemple 2048 bits = 256 octets) et la taille du jeton utilisateur pourrait être supérieure, le jeton utilisateur doit donc être crypté par le client AES, puis le serveur pourrait le décrypter après validation de l'enregistrement ClientHello
.
La structure des enregistrements de prise de contact est un peu différente en raison de l'utilisation du cryptage hybride. les deux octets après les octets de la version du protocole indiquent la taille du corps de la poignée de main qui est chiffré par la clé publique du serveur. la taille du corps de la poignée de main passe en raison de la taille de la clé, la taille du corps chiffré dépend de la taille de la clé 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
Après une négociation réussie, le serveur renvoie un SessionID
secret dans l'enregistrement ServerHello
qui est chiffré par la clé AES du client. Le client doit ajouter le SessionID
aux octets d'enregistrement personnalisés, puis le chiffrer par la clé AES, ajouter les en-têtes d'enregistrement (type d'enregistrement et version du protocole), puis envoyer les octets au serveur. Le serveur déchiffrera le corps de l'enregistrement par la clé AES du client (qui est préalablement enregistrée avec l'adresse IP et le port du client), puis analysera l' SessionID
du corps déchiffré, autorisera l'ID de session et transmettra les octets à la fonction Handler
.
Pour l'enregistrement Ping
, le serveur envoie immédiatement un enregistrement Pong
au client.