Я создал этот пакет для создания виртуального соединения с сохранением состояния между клиентом и сервером с использованием протокола UDP для игрового сервера golang (как вы знаете, протокол 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
. Транскодер может кодировать и декодировать некоторые типы сообщений по умолчанию, такие как рукопожатие и пинг, а также поддерживает общие методы Marshal
и Unmarshal
для поддержки вашего пользовательского типа данных.
Реализация интерфейса crypto.Symmetric
для шифрования и дешифрования с помощью алгоритмов симметричных ключей. Реализация по умолчанию использует AES CBC
с дополнением PKCS#7
.
Реализация 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
}
Сервер экспортировал два метода для отправки сообщения клиенту или широковещательного сообщения для всех клиентов. Чтобы отправить сообщение определенному клиенту, вам необходимо иметь идентификатор клиента.
Полезная нагрузка представляет собой сообщение, закодированное транскодером, и зашифрованное сообщение обрабатывается сервером.
Обработчик — это функция с сигнатурой func(id string, t byte, p []byte)
. Вы можете установить функцию-обработчик с помощью метода SetHandler
. Эта функция вызывается, когда запись пользовательского типа получена и аутентифицирована. Параметр 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
. Клиент расшифровывает запись и повторяет сообщение ClientHello
с файлом cookie, запись необходимо зашифровать с помощью открытого ключа сервера, затем добавить зашифрованный токен пользователя (с помощью AES) в тело записи, сервер зарегистрирует клиента после проверки файлов cookie и аутентифицирует токен пользователя, а затем вернет запись 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
После успешного установления связи сервер возвращает секретный SessionID
в записи ServerHello
, который зашифрован клиентским ключом AES. Клиент должен добавить SessionID
к байтам пользовательской записи, затем зашифровать его с помощью ключа AES, добавить заголовки записей (тип записи и версию протокола), а затем отправить байты на сервер. Сервер расшифровывает тело записи с помощью ключа AES клиента (который ранее зарегистрирован с IP-адресом и портом клиента), затем анализирует SessionID
из расшифрованного тела, авторизует идентификатор сеанса и передает байты функции Handler
.
Для записи Ping
сервер немедленно отправляет клиенту запись Pong
.