Files
Erupe/server/channelserver/sys_capture.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.8 KiB
Go

package channelserver
import (
"fmt"
"net"
"os"
"path/filepath"
"time"
"erupe-ce/network"
"erupe-ce/network/pcap"
"go.uber.org/zap"
)
// startCapture wraps a network.Conn with a RecordingConn if capture is enabled.
// Returns the (possibly wrapped) conn, the RecordingConn (nil if capture disabled),
// and a cleanup function that must be called on session close.
func startCapture(server *Server, conn network.Conn, remoteAddr net.Addr, serverType pcap.ServerType) (network.Conn, *pcap.RecordingConn, func()) {
capCfg := server.erupeConfig.Capture
if !capCfg.Enabled {
return conn, nil, func() {}
}
switch serverType {
case pcap.ServerTypeSign:
if !capCfg.CaptureSign {
return conn, nil, func() {}
}
case pcap.ServerTypeEntrance:
if !capCfg.CaptureEntrance {
return conn, nil, func() {}
}
case pcap.ServerTypeChannel:
if !capCfg.CaptureChannel {
return conn, nil, func() {}
}
}
outputDir := capCfg.OutputDir
if outputDir == "" {
outputDir = "captures"
}
if err := os.MkdirAll(outputDir, 0o755); err != nil {
server.logger.Warn("Failed to create capture directory", zap.Error(err))
return conn, nil, func() {}
}
now := time.Now()
filename := fmt.Sprintf("%s_%s_%s.mhfr",
serverType.String(),
now.Format("20060102_150405"),
sanitizeAddr(remoteAddr.String()),
)
path := filepath.Join(outputDir, filename)
f, err := os.Create(path)
if err != nil {
server.logger.Warn("Failed to create capture file", zap.Error(err), zap.String("path", path))
return conn, nil, func() {}
}
startNs := now.UnixNano()
hdr := pcap.FileHeader{
Version: pcap.FormatVersion,
ServerType: serverType,
ClientMode: byte(server.erupeConfig.RealClientMode),
SessionStartNs: startNs,
}
meta := pcap.SessionMetadata{
Host: server.erupeConfig.Host,
RemoteAddr: remoteAddr.String(),
}
w, err := pcap.NewWriter(f, hdr, meta)
if err != nil {
server.logger.Warn("Failed to initialize capture writer", zap.Error(err))
_ = f.Close()
return conn, nil, func() {}
}
server.logger.Info("Capture started", zap.String("file", path))
rc := pcap.NewRecordingConn(conn, w, startNs, capCfg.ExcludeOpcodes)
rc.SetCaptureFile(f, &meta)
cleanup := func() {
if err := w.Flush(); err != nil {
server.logger.Warn("Failed to flush capture", zap.Error(err))
}
if err := f.Close(); err != nil {
server.logger.Warn("Failed to close capture file", zap.Error(err))
}
server.logger.Info("Capture saved", zap.String("file", path))
}
return rc, rc, cleanup
}
// sanitizeAddr replaces characters that are problematic in filenames.
func sanitizeAddr(addr string) string {
out := make([]byte, 0, len(addr))
for i := 0; i < len(addr); i++ {
c := addr[i]
if c == ':' {
out = append(out, '_')
} else {
out = append(out, c)
}
}
return string(out)
}