feat(network): add protocol packet capture and replay system

Add a recording and replay foundation for the MHF network protocol.
A RecordingConn decorator wraps network.Conn to transparently capture
all decrypted packets to binary .mhfr files, with zero handler changes
and zero overhead when disabled.

- network/pcap: binary capture format (writer, reader, filters)
- RecordingConn: thread-safe Conn decorator with direction tracking
- CaptureOptions in config (disabled by default)
- Capture wired into all three server types (sign, entrance, channel)
- cmd/replay: CLI tool with dump, json, stats, and compare modes
- 19 new tests, all passing with -race
This commit is contained in:
Houmgaor
2026-02-23 18:50:44 +01:00
parent e5ffc4d52d
commit 7ef5efc549
20 changed files with 1716 additions and 15 deletions

110
network/pcap/reader.go Normal file
View File

@@ -0,0 +1,110 @@
package pcap
import (
"encoding/binary"
"encoding/json"
"fmt"
"io"
)
// Reader reads .mhfr capture files.
type Reader struct {
r io.Reader
Header FileHeader
Meta SessionMetadata
}
// NewReader creates a Reader, reading and validating the file header and metadata.
func NewReader(r io.Reader) (*Reader, error) {
// Read magic.
magicBuf := make([]byte, 4)
if _, err := io.ReadFull(r, magicBuf); err != nil {
return nil, fmt.Errorf("pcap: read magic: %w", err)
}
if string(magicBuf) != Magic {
return nil, fmt.Errorf("pcap: invalid magic %q, expected %q", string(magicBuf), Magic)
}
var hdr FileHeader
if err := binary.Read(r, binary.BigEndian, &hdr.Version); err != nil {
return nil, fmt.Errorf("pcap: read version: %w", err)
}
if hdr.Version != FormatVersion {
return nil, fmt.Errorf("pcap: unsupported version %d, expected %d", hdr.Version, FormatVersion)
}
var serverType byte
if err := binary.Read(r, binary.BigEndian, &serverType); err != nil {
return nil, fmt.Errorf("pcap: read server type: %w", err)
}
hdr.ServerType = ServerType(serverType)
if err := binary.Read(r, binary.BigEndian, &hdr.ClientMode); err != nil {
return nil, fmt.Errorf("pcap: read client mode: %w", err)
}
if err := binary.Read(r, binary.BigEndian, &hdr.SessionStartNs); err != nil {
return nil, fmt.Errorf("pcap: read session start: %w", err)
}
// Skip 4 reserved bytes.
if _, err := io.ReadFull(r, make([]byte, 4)); err != nil {
return nil, fmt.Errorf("pcap: read reserved: %w", err)
}
if err := binary.Read(r, binary.BigEndian, &hdr.MetadataLen); err != nil {
return nil, fmt.Errorf("pcap: read metadata len: %w", err)
}
// Skip 8 reserved bytes.
if _, err := io.ReadFull(r, make([]byte, 8)); err != nil {
return nil, fmt.Errorf("pcap: read reserved: %w", err)
}
// Read metadata JSON.
metaBytes := make([]byte, hdr.MetadataLen)
if _, err := io.ReadFull(r, metaBytes); err != nil {
return nil, fmt.Errorf("pcap: read metadata: %w", err)
}
var meta SessionMetadata
if err := json.Unmarshal(metaBytes, &meta); err != nil {
return nil, fmt.Errorf("pcap: unmarshal metadata: %w", err)
}
return &Reader{r: r, Header: hdr, Meta: meta}, nil
}
// ReadPacket reads the next packet record. Returns io.EOF when no more packets.
func (rd *Reader) ReadPacket() (PacketRecord, error) {
var rec PacketRecord
if err := binary.Read(rd.r, binary.BigEndian, &rec.TimestampNs); err != nil {
if err == io.EOF || err == io.ErrUnexpectedEOF {
return rec, io.EOF
}
return rec, fmt.Errorf("pcap: read timestamp: %w", err)
}
var dir byte
if err := binary.Read(rd.r, binary.BigEndian, &dir); err != nil {
return rec, fmt.Errorf("pcap: read direction: %w", err)
}
rec.Direction = Direction(dir)
if err := binary.Read(rd.r, binary.BigEndian, &rec.Opcode); err != nil {
return rec, fmt.Errorf("pcap: read opcode: %w", err)
}
var payloadLen uint32
if err := binary.Read(rd.r, binary.BigEndian, &payloadLen); err != nil {
return rec, fmt.Errorf("pcap: read payload len: %w", err)
}
rec.Payload = make([]byte, payloadLen)
if _, err := io.ReadFull(rd.r, rec.Payload); err != nil {
return rec, fmt.Errorf("pcap: read payload: %w", err)
}
return rec, nil
}