318 lines
7.8 KiB
Go
318 lines
7.8 KiB
Go
package udp
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"eq2emu/internal/opcodes"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// Common protocol errors
|
|
var (
|
|
ErrPacketTooSmall = errors.New("packet too small")
|
|
ErrInvalidCRC = errors.New("invalid CRC")
|
|
ErrInvalidOpcode = errors.New("invalid opcode")
|
|
)
|
|
|
|
// ProtocolPacket represents a low-level UDP protocol packet with opcode and payload
|
|
type ProtocolPacket struct {
|
|
Opcode uint8 // Protocol operation code (1-2 bytes when serialized)
|
|
Data []byte // Packet payload data
|
|
Raw []byte // Original raw packet data for debugging
|
|
}
|
|
|
|
// ApplicationPacket represents a higher-level game application packet
|
|
type ApplicationPacket struct {
|
|
Opcode uint16 // Application-level operation code
|
|
Data []byte // Application payload data
|
|
}
|
|
|
|
// ParseProtocolPacket parses raw UDP data into a ProtocolPacket
|
|
// Handles variable opcode sizing and CRC validation based on EQ2 protocol
|
|
func ParseProtocolPacket(data []byte) (*ProtocolPacket, error) {
|
|
if len(data) < 2 {
|
|
return nil, ErrPacketTooSmall
|
|
}
|
|
|
|
var opcode uint8
|
|
var dataStart int
|
|
|
|
// EQ2 protocol uses 1-byte opcodes normally, 2-byte for opcodes >= 0xFF
|
|
// When opcode >= 0xFF, it's prefixed with 0x00
|
|
if data[0] == 0x00 && len(data) > 2 {
|
|
opcode = data[1]
|
|
dataStart = 2
|
|
} else {
|
|
opcode = data[0]
|
|
dataStart = 1
|
|
}
|
|
|
|
// Extract payload, handling CRC for non-session packets
|
|
var payload []byte
|
|
if requiresCRC(opcode) {
|
|
if len(data) < dataStart+2 {
|
|
return nil, ErrPacketTooSmall
|
|
}
|
|
|
|
// Payload excludes the 2-byte CRC suffix
|
|
payload = data[dataStart : len(data)-2]
|
|
|
|
// Validate CRC on the entire packet from beginning
|
|
if !ValidateCRC(data) {
|
|
return nil, fmt.Errorf("%w for opcode 0x%02X", ErrInvalidCRC, opcode)
|
|
}
|
|
} else {
|
|
payload = data[dataStart:]
|
|
}
|
|
|
|
return &ProtocolPacket{
|
|
Opcode: opcode,
|
|
Data: payload,
|
|
Raw: data,
|
|
}, nil
|
|
}
|
|
|
|
// Serialize converts ProtocolPacket back to wire format with proper opcode encoding and CRC
|
|
func (p *ProtocolPacket) Serialize() []byte {
|
|
var result []byte
|
|
|
|
// Handle variable opcode encoding
|
|
if p.Opcode == 0xFF {
|
|
// 2-byte opcode format: [0x00][actual_opcode][data]
|
|
result = make([]byte, 2+len(p.Data))
|
|
result[0] = 0x00
|
|
result[1] = p.Opcode
|
|
copy(result[2:], p.Data)
|
|
} else {
|
|
// 1-byte opcode format: [opcode][data]
|
|
result = make([]byte, 1+len(p.Data))
|
|
result[0] = p.Opcode
|
|
copy(result[1:], p.Data)
|
|
}
|
|
|
|
// Add CRC for packets that require it
|
|
if requiresCRC(p.Opcode) {
|
|
result = AppendCRC(result)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// String provides human-readable representation for debugging
|
|
func (p *ProtocolPacket) String() string {
|
|
return fmt.Sprintf("ProtocolPacket{Opcode: 0x%02X, DataLen: %d}", p.Opcode, len(p.Data))
|
|
}
|
|
|
|
// ParseApplicationPacket parses application-level packet from decrypted/decompressed data
|
|
func ParseApplicationPacket(data []byte) (*ApplicationPacket, error) {
|
|
if len(data) < 2 {
|
|
return nil, errors.New("application packet requires at least 2 bytes for opcode")
|
|
}
|
|
|
|
// Application opcodes are always little-endian 16-bit values
|
|
opcode := binary.LittleEndian.Uint16(data[0:2])
|
|
|
|
return &ApplicationPacket{
|
|
Opcode: opcode,
|
|
Data: data[2:],
|
|
}, nil
|
|
}
|
|
|
|
// Serialize converts ApplicationPacket to byte array for transmission
|
|
func (p *ApplicationPacket) Serialize() []byte {
|
|
result := make([]byte, 2+len(p.Data))
|
|
binary.LittleEndian.PutUint16(result[0:2], p.Opcode)
|
|
copy(result[2:], p.Data)
|
|
return result
|
|
}
|
|
|
|
// String provides human-readable representation for debugging
|
|
func (p *ApplicationPacket) String() string {
|
|
return fmt.Sprintf("ApplicationPacket{Opcode: 0x%04X, DataLen: %d}", p.Opcode, len(p.Data))
|
|
}
|
|
|
|
// requiresCRC determines if a protocol opcode requires CRC validation
|
|
// Session control packets (SessionRequest, SessionResponse, OutOfSession) don't use CRC
|
|
func requiresCRC(opcode uint8) bool {
|
|
switch opcode {
|
|
case opcodes.OpSessionRequest, opcodes.OpSessionResponse, opcodes.OpOutOfSession:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
// PacketCombiner groups small packets together to reduce UDP overhead
|
|
type PacketCombiner struct {
|
|
PendingPackets []*ProtocolPacket // Direct access to pending packets
|
|
MaxSize int // Direct access to max size
|
|
}
|
|
|
|
// NewPacketCombiner creates a combiner with specified max size
|
|
func NewPacketCombiner(maxSize int) *PacketCombiner {
|
|
return &PacketCombiner{
|
|
MaxSize: maxSize,
|
|
}
|
|
}
|
|
|
|
// Add queues a packet for potential combining
|
|
func (pc *PacketCombiner) Add(packet *ProtocolPacket) {
|
|
pc.PendingPackets = append(pc.PendingPackets, packet)
|
|
}
|
|
|
|
// Flush returns combined packets and clears the queue
|
|
func (pc *PacketCombiner) Flush() []*ProtocolPacket {
|
|
count := len(pc.PendingPackets)
|
|
if count == 0 {
|
|
return nil
|
|
}
|
|
|
|
if count == 1 {
|
|
// Single packet - no combining needed
|
|
packet := pc.PendingPackets[0]
|
|
pc.Clear()
|
|
return []*ProtocolPacket{packet}
|
|
}
|
|
|
|
// Combine multiple packets
|
|
combined := pc.combine()
|
|
pc.Clear()
|
|
return []*ProtocolPacket{combined}
|
|
}
|
|
|
|
// combine merges all pending packets into a single combined packet
|
|
func (pc *PacketCombiner) combine() *ProtocolPacket {
|
|
var buf bytes.Buffer
|
|
|
|
for _, packet := range pc.PendingPackets {
|
|
serialized := packet.Serialize()
|
|
pc.writeSizeHeader(&buf, len(serialized))
|
|
buf.Write(serialized)
|
|
}
|
|
|
|
return &ProtocolPacket{
|
|
Opcode: opcodes.OpCombined,
|
|
Data: buf.Bytes(),
|
|
}
|
|
}
|
|
|
|
// writeSizeHeader writes packet size using variable-length encoding
|
|
func (pc *PacketCombiner) writeSizeHeader(buf *bytes.Buffer, size int) {
|
|
if size >= 255 {
|
|
// Large packet - use 3-byte header [0xFF][low][high]
|
|
buf.WriteByte(0xFF)
|
|
buf.WriteByte(byte(size))
|
|
buf.WriteByte(byte(size >> 8))
|
|
} else {
|
|
// Small packet - use 1-byte header
|
|
buf.WriteByte(byte(size))
|
|
}
|
|
}
|
|
|
|
// ShouldCombine determines if packets should be combined based on total size
|
|
func (pc *PacketCombiner) ShouldCombine() bool {
|
|
if len(pc.PendingPackets) < 2 {
|
|
return false
|
|
}
|
|
|
|
totalSize := 0
|
|
for _, packet := range pc.PendingPackets {
|
|
serialized := packet.Serialize()
|
|
totalSize += len(serialized)
|
|
|
|
// Add size header overhead
|
|
if len(serialized) >= 255 {
|
|
totalSize += 3
|
|
} else {
|
|
totalSize += 1
|
|
}
|
|
}
|
|
|
|
return totalSize <= pc.MaxSize
|
|
}
|
|
|
|
// Clear removes all pending packets
|
|
func (pc *PacketCombiner) Clear() {
|
|
pc.PendingPackets = pc.PendingPackets[:0] // Reuse slice capacity
|
|
}
|
|
|
|
// ParseCombinedPacket splits combined packet into individual packets
|
|
func ParseCombinedPacket(data []byte) ([]*ProtocolPacket, error) {
|
|
var packets []*ProtocolPacket
|
|
offset := 0
|
|
|
|
for offset < len(data) {
|
|
size, headerSize, err := readSizeHeader(data, offset)
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
offset += headerSize
|
|
|
|
if offset+size > len(data) {
|
|
break // Incomplete packet
|
|
}
|
|
|
|
// Parse individual packet
|
|
packetData := data[offset : offset+size]
|
|
if packet, err := ParseProtocolPacket(packetData); err == nil {
|
|
packets = append(packets, packet)
|
|
}
|
|
|
|
offset += size
|
|
}
|
|
|
|
return packets, nil
|
|
}
|
|
|
|
// readSizeHeader reads variable-length size header
|
|
func readSizeHeader(data []byte, offset int) (size, headerSize int, err error) {
|
|
if offset >= len(data) {
|
|
return 0, 0, errors.New("insufficient data")
|
|
}
|
|
|
|
if data[offset] == 0xFF {
|
|
// 3-byte size header
|
|
if offset+2 >= len(data) {
|
|
return 0, 0, errors.New("insufficient data for 3-byte header")
|
|
}
|
|
size = int(data[offset+1]) | (int(data[offset+2]) << 8)
|
|
headerSize = 3
|
|
} else {
|
|
// 1-byte size header
|
|
size = int(data[offset])
|
|
headerSize = 1
|
|
}
|
|
|
|
return size, headerSize, nil
|
|
}
|
|
|
|
// ValidateCombinedPacket checks if combined packet data is well-formed
|
|
func ValidateCombinedPacket(data []byte) error {
|
|
offset := 0
|
|
count := 0
|
|
|
|
for offset < len(data) {
|
|
size, headerSize, err := readSizeHeader(data, offset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
offset += headerSize
|
|
|
|
if offset+size > len(data) {
|
|
return errors.New("packet extends beyond data boundary")
|
|
}
|
|
|
|
offset += size
|
|
count++
|
|
|
|
if count > 100 { // Sanity check
|
|
return errors.New("too many packets in combined packet")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|