Files
Erupe/network/pcap/recording_conn.go
Houmgaor f712e3c04d feat(pcap): complete replay system with filtering, metadata, and live replay
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.
2026-02-23 19:34:30 +01:00

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()
}