mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
Wire ExcludeOpcodes config into RecordingConn so configured opcodes (e.g. ping, nop, position) are filtered at record time. Add padded metadata with in-place PatchMetadata to populate CharID/UserID after login. Implement --mode replay using protbot's encrypted connection with timing-aware packet sending, auto-ping response, concurrent S→C collection, and byte-level payload diff reporting.
112 lines
2.9 KiB
Go
112 lines
2.9 KiB
Go
package pcap
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"erupe-ce/network"
|
|
)
|
|
|
|
// RecordingConn wraps a network.Conn and records all packets to a Writer.
|
|
// It is safe for concurrent use from separate send/recv goroutines.
|
|
type RecordingConn struct {
|
|
inner network.Conn
|
|
writer *Writer
|
|
startNs int64
|
|
excludeOpcodes map[uint16]struct{}
|
|
metaFile *os.File // capture file handle for metadata patching
|
|
meta *SessionMetadata // current metadata (mutated by SetSessionInfo)
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// NewRecordingConn wraps inner, recording all packets to w.
|
|
// startNs is the session start time in nanoseconds (used as the time base).
|
|
// excludeOpcodes is an optional list of opcodes to skip when recording.
|
|
func NewRecordingConn(inner network.Conn, w *Writer, startNs int64, excludeOpcodes []uint16) *RecordingConn {
|
|
var excl map[uint16]struct{}
|
|
if len(excludeOpcodes) > 0 {
|
|
excl = make(map[uint16]struct{}, len(excludeOpcodes))
|
|
for _, op := range excludeOpcodes {
|
|
excl[op] = struct{}{}
|
|
}
|
|
}
|
|
return &RecordingConn{
|
|
inner: inner,
|
|
writer: w,
|
|
startNs: startNs,
|
|
excludeOpcodes: excl,
|
|
}
|
|
}
|
|
|
|
// SetCaptureFile sets the file handle and metadata pointer for in-place metadata patching.
|
|
// Must be called before SetSessionInfo. Not required if metadata patching is not needed.
|
|
func (rc *RecordingConn) SetCaptureFile(f *os.File, meta *SessionMetadata) {
|
|
rc.mu.Lock()
|
|
rc.metaFile = f
|
|
rc.meta = meta
|
|
rc.mu.Unlock()
|
|
}
|
|
|
|
// SetSessionInfo updates the CharID and UserID in the capture file metadata.
|
|
// This is called after login when the session identity is known.
|
|
func (rc *RecordingConn) SetSessionInfo(charID, userID uint32) {
|
|
rc.mu.Lock()
|
|
defer rc.mu.Unlock()
|
|
|
|
if rc.meta == nil || rc.metaFile == nil {
|
|
return
|
|
}
|
|
|
|
rc.meta.CharID = charID
|
|
rc.meta.UserID = userID
|
|
|
|
// Best-effort patch — log errors are handled by the caller.
|
|
_ = PatchMetadata(rc.metaFile, *rc.meta)
|
|
}
|
|
|
|
// ReadPacket reads from the inner connection and records the packet as client-to-server.
|
|
func (rc *RecordingConn) ReadPacket() ([]byte, error) {
|
|
data, err := rc.inner.ReadPacket()
|
|
if err != nil {
|
|
return data, err
|
|
}
|
|
rc.record(DirClientToServer, data)
|
|
return data, nil
|
|
}
|
|
|
|
// SendPacket sends via the inner connection and records the packet as server-to-client.
|
|
func (rc *RecordingConn) SendPacket(data []byte) error {
|
|
err := rc.inner.SendPacket(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rc.record(DirServerToClient, data)
|
|
return nil
|
|
}
|
|
|
|
func (rc *RecordingConn) record(dir Direction, data []byte) {
|
|
var opcode uint16
|
|
if len(data) >= 2 {
|
|
opcode = binary.BigEndian.Uint16(data[:2])
|
|
}
|
|
|
|
if rc.excludeOpcodes != nil {
|
|
if _, excluded := rc.excludeOpcodes[opcode]; excluded {
|
|
return
|
|
}
|
|
}
|
|
|
|
rec := PacketRecord{
|
|
TimestampNs: time.Now().UnixNano(),
|
|
Direction: dir,
|
|
Opcode: opcode,
|
|
Payload: data,
|
|
}
|
|
|
|
rc.mu.Lock()
|
|
_ = rc.writer.WritePacket(rec)
|
|
rc.mu.Unlock()
|
|
}
|