Ich habe dieses Paket erstellt, um mithilfe des UDP-Protokolls für einen Golang-Spieleserver eine virtuelle zustandsbehaftete Verbindung zwischen Client und Server herzustellen (wie Sie wissen, ist das UDP-Protokoll zustandslos, Pakete kommen möglicherweise nicht in der richtigen Reihenfolge an und es gibt keine ACK).
Der udpsocket
unterstützt eine Nachahmung von DTLS-Handshake, Kryptografie, Sitzungsverwaltung und Authentifizierung. Es ist dafür verantwortlich, einen sicheren Kanal zwischen dem Client und dem Server auf UDP (Handshake) herzustellen, sie zu authentifizieren und zu verwalten, die Daten zu entschlüsseln und zu verschlüsseln und eine API bereitzustellen, um Daten an die Clients zu senden oder zu senden und sie abzuhören.
Der udpsocket
-Server akzeptiert einige Parameter zum Initiieren:
*net.UDPConn
Optionen können mit vordefinierten Funktionen übergeben werden:
udpsocket . WithAuthClient ()
udpsocket . WithTranscoder ()
udpsocket . WithSymmetricCrypto ()
udpsocket . WithAsymmetricCrypto ()
udpsocket . WithReadBufferSize ()
udpsocket . WithMinimumPayloadSize ()
udpsocket . WithProtocolVersion ()
udpsocket . WithHeartbeatExpiration ()
udpsocket . WithLogger ()
Eine Implementierung der AuthClient
Schnittstelle, die zur Authentifizierung des Benutzertokens verwendet wird. Wenn Ihr Server keine tokenbasierte Authentifizierung benötigt, übergeben Sie nichts, sodass der Server die Standardimplementierung ohne Authentifizierung verwendet.
Eine Implementierung der Transcoder
Schnittstelle, die zum Kodieren und Dekodieren der Daten zwischen Client und Server verwendet wird. Die Standardimplementierung des Transcoder
ist Protobuf
. Der Transcoder kann einige Standardnachrichtentypen wie Handshake und Ping kodieren und dekodieren und unterstützt außerdem allgemeine Marshal
und Unmarshal
-Methoden zur Unterstützung Ihres benutzerdefinierten Datentyps.
Eine Implementierung der crypto.Symmetric
Schnittstelle zum Verschlüsseln und Entschlüsseln über Algorithmen mit symmetrischen Schlüsseln. Die Standardimplementierung verwendet AES CBC
mit PKCS#7
-Padding.
Eine Implementierung der crypto.Asymmetric
Schnittstelle zum Verschlüsseln und Entschlüsseln mittels Public-Key-Kryptographie. Die Standardimplementierung verwendet RSA
mit benutzerdefinierter Schlüsselgröße.
ReadBufferSize
: Größe des LesepuffersMinimumPayloadSize
: Zum Ausschneiden der Daten, deren Größe nicht ausreicht (um einige Angriffsmethoden zu verhindern)ProtocolVersionMajor
: die Hauptversion des ProtokollsProtocolVersionMinor
: die Nebenversion des Protokolls 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
}
Der Server hat zwei Methoden exportiert, um eine Nachricht an den Client zu senden oder eine Nachricht für alle Clients zu senden. Um eine Nachricht an einen bestimmten Client zu senden, müssen Sie über die Client-ID verfügen.
Die Nutzlast ist eine Transcoder-codierte Nachricht und wird vom Server verschlüsselt verarbeitet.
Der Handler ist eine Funktion mit der Signatur func(id string, t byte, p []byte)
. Sie können Ihre Handlerfunktion mit der SetHandler
-Methode festlegen. Diese Funktion wird aufgerufen, wenn ein benutzerdefinierter Datensatz empfangen und authentifiziert wird. Der id
-Parameter ist die Client-ID (die vom Token abgerufen wird, oder eine neu generierte UUID, wenn keine Authentifizierung erforderlich ist), der t
Parameter ist der Datensatztyp und der p
-Parameter ist die entschlüsselte Nutzlast. Sie müssen sie für Ihre benutzerdefinierten Daten Unmarshal
Nachrichtentyp.
Jede Nachricht vom Client ist ein Record
. Der Datensatz verfügt über ein Format zum Parsen und Entschlüsseln.
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
Sie fragen sich vielleicht, warum die Größe der RSA-verschlüsselten Nachricht dem Datensatz vorangestellt wird, während die Größe des privaten RSA-Schlüssels bekannt und die Größe der verschlüsselten Nachricht berechenbar ist. Der Server
verwendet tatsächlich Asymmetric
Schnittstelle für die Verschlüsselung und der RSA implementiert sie. In anderen Implementierungen folgt die Größe der verschlüsselten Nachricht möglicherweise nicht der Größe des privaten Schlüssels. Daher müssen wir die Größe der verschlüsselten Nachricht im Handshaking übergeben, um sie trennen zu können AES-verschlüsselte und RSA-verschlüsselte Abschnitte.
Das erste Byte des Datensatzes ist der Typ und gibt an, wie der Datensatz analysiert werden soll. Unterstützte reservierte Typen:
1
2
3
4
5
Der Handshake-Prozess basiert auf einer Nachahmung des DTLS-Protokolls. Der Client sendet ClientHello
Datensatz. Dieser Datensatz enthält einen zufälligen Byte- und AES-Schlüssel und wird vom öffentlichen Schlüssel des Servers verschlüsselt, der zum Herunterladen auf TLS benötigt wird. Anschließend generiert der Server einen zufälligen Cookie basierend auf den Client-Parametern, verschlüsselt es mit dem Client-AES-Schlüssel (der von ClientHello
empfangen wird und als HelloVerify
Datensatz sendet. Der Client entschlüsselt den Datensatz und wiederholt die ClientHello
-Nachricht mit dem Cookie, dem Datensatz muss mit dem öffentlichen Schlüssel des Servers verschlüsselt werden. Anschließend wird das verschlüsselte Benutzertoken (mit AES) an den Datensatzkörper angehängt. Der Server registriert den Client nach der Cookie-Verifizierung, authentifiziert das Benutzertoken und gibt dann einen ServerHello
Datensatz zurück, der ein zufälliges Geheimnis enthält Sitzungs-ID. Der Handshake-Prozess wird hier durchgeführt.
Client Server
------ ------
ClientHello ------>
<----- HelloVerifyRequest
(contains cookie)
ClientHello ------>
(with cookie & token)
<----- ServerHello
(contains session ID)
Der Server
verwendet sowohl symmetrische als auch asymmetrische Verschlüsselungen, um mit dem Client zu kommunizieren.
Der ClientHello
Eintrag (für den Server) enthält einen sicheren 256-Bit-AES-Schlüssel und wird mit dem öffentlichen Schlüssel des Servers verschlüsselt, sodass der Server ihn mit dem privaten Schlüssel entschlüsseln kann.
Der HelloVerify
Eintrag (für den Client) wird mit dem AES-Schlüssel des Clients (der zuvor entschlüsselt wurde) verschlüsselt.
Wenn eine Benutzerauthentifizierung erforderlich ist, muss der Client das Benutzertoken mit dem ClientHello
senden (ein Datensatz, der auch Cookies enthält), aber die asymmetrische Verschlüsselung unterliegt einer Größenbeschränkung. Beispielsweise ist RSA nur in der Lage, Daten bis zu einer maximalen Menge zu verschlüsseln, die der Schlüsselgröße entspricht (z. B. 2048 Bits = 256 Bytes). Die Größe des Benutzertokens könnte größer sein, sodass das Benutzertoken dann vom Client-AES verschlüsselt werden muss Der Server könnte es nach der Validierung des ClientHello
Datensatzes entschlüsseln.
Die Struktur des Handshake-Datensatzes ist aufgrund der Verwendung der Hybridverschlüsselung etwas anders. Die zwei Bytes nach den Bytes der Protokollversion geben die Größe des Handshake-Körpers an, der durch den öffentlichen Schlüssel des Servers verschlüsselt wird. Die Größe des Handshake-Körpers hängt von der Schlüsselgröße ab, die Größe des verschlüsselten Körpers hängt von der Größe des RSA-Schlüssels ab.
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
Nach einem erfolgreichen Handshake gibt der Server eine geheime SessionID
im ServerHello
Datensatz zurück, die durch den Client-AES-Schlüssel verschlüsselt wird. Der Client muss die SessionID
an die benutzerdefinierten Datensatzbytes anhängen, sie dann mit dem AES-Schlüssel verschlüsseln, die Datensatzheader (Datensatztyp und Protokollversion) hinzufügen und dann die Bytes an den Server senden. Der Server entschlüsselt den Datensatzkörper mit dem AES-Schlüssel des Clients (der zuvor mit der IP und dem Port des Clients registriert wurde), analysiert dann die SessionID
aus dem entschlüsselten Körper, autorisiert die Sitzungs-ID und übergibt die Bytes an die Handler
-Funktion.
Für Ping
Datensatz sendet der Server sofort einen Pong
Datensatz an den Client.