ฉันสร้างแพ็คเกจนี้เพื่อสร้างการเชื่อมต่อเสมือนแบบมีสถานะระหว่างไคลเอนต์และเซิร์ฟเวอร์โดยใช้โปรโตคอล 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
ตัวแปลงรหัสสามารถเข้ารหัสและถอดรหัสข้อความเริ่มต้นบางประเภท เช่น handshake & ping และยังรองรับวิธี Marshal
และ Unmarshal
ทั่วไปเพื่อรองรับประเภทข้อมูลที่คุณกำหนดเอง
การใช้งานอินเทอร์เฟซ crypto.Symmetric
เพื่อเข้ารหัสและถอดรหัสผ่านอัลกอริทึม Symmetric-keys การใช้งานเริ่มต้นใช้ 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
}
เซิร์ฟเวอร์ส่งออกสองวิธีเพื่อส่งข้อความไปยังไคลเอนต์หรือเผยแพร่ข้อความสำหรับไคลเอนต์ทั้งหมด หากต้องการส่งข้อความถึงลูกค้ารายใดรายหนึ่ง คุณต้องมีรหัสลูกค้า
เพย์โหลดเป็นข้อความที่เข้ารหัส Transcoder และเซิร์ฟเวอร์จะจัดการการเข้ารหัส
ตัวจัดการเป็นฟังก์ชันที่มีลายเซ็น 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 จากนั้นเซิร์ฟเวอร์จะสร้างการสุ่ม คุกกี้ตามพารามิเตอร์ไคลเอนต์ เข้ารหัสด้วยคีย์ AES ไคลเอนต์ (ซึ่ง ClientHello
ได้รับ & ส่งเป็นบันทึก HelloVerify
ไคลเอนต์ถอดรหัสบันทึก & ทำซ้ำข้อความ ClientHello
ด้วย คุกกี้ จำเป็นต้องมีการเข้ารหัสบันทึกด้วยคีย์สาธารณะของเซิร์ฟเวอร์ จากนั้นจึงเพิ่มโทเค็นผู้ใช้ที่เข้ารหัส (ด้วย AES) เข้ากับเนื้อหาบันทึก เซิร์ฟเวอร์จะลงทะเบียนไคลเอ็นต์หลังจากการตรวจสอบคุกกี้และรับรองความถูกต้องของโทเค็นผู้ใช้ จากนั้นส่งคืนบันทึก ServerHello
มีรหัสเซสชันลับแบบสุ่ม กระบวนการแฮนด์เชคเสร็จสิ้นที่นี่
Client Server
------ ------
ClientHello ------>
<----- HelloVerifyRequest
(contains cookie)
ClientHello ------>
(with cookie & token)
<----- ServerHello
(contains session ID)
Server
ใช้ทั้งการเข้ารหัสแบบสมมาตรและไม่สมมาตรเพื่อสื่อสารกับไคลเอนต์
บันทึก ClientHello
(สำหรับเซิร์ฟเวอร์) มีคีย์ AES 256 บิตที่ปลอดภัยและเข้ารหัสโดยคีย์สาธารณะของเซิร์ฟเวอร์ ดังนั้นเซิร์ฟเวอร์จึงสามารถถอดรหัสด้วยคีย์ส่วนตัวได้
บันทึก HelloVerify
(สำหรับไคลเอ็นต์) เข้ารหัสด้วยคีย์ AES ของไคลเอ็นต์ (ซึ่งเคยถอดรหัสมาก่อน)
หากจำเป็นต้องมีการตรวจสอบสิทธิ์ผู้ใช้ ไคลเอ็นต์จะต้องส่งโทเค็นผู้ใช้ด้วย ClientHello
(บันทึกที่มีคุกกี้ด้วย) แต่การเข้ารหัสแบบอสมมาตรจะมีข้อจำกัดด้านขนาด ตัวอย่างเช่น RSA สามารถเข้ารหัสข้อมูลในจำนวนสูงสุดเท่ากับขนาดคีย์เท่านั้น (เช่น 2,048 บิต = 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
จากเนื้อหาที่ถอดรหัส อนุญาต ID เซสชัน และส่งไบต์ไปยังฟังก์ชัน Handler
สำหรับบันทึก Ping
เซิร์ฟเวอร์จะส่งบันทึก Pong
ไปยังไคลเอนต์ทันที