eq2go/internal/udp/protocol.go

318 lines
7.8 KiB
Go

package udp
import (
"bytes"
"encoding/binary"
"eq2emu/internal/common/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
}