Tactical PCAP dissection and traffic replay engine with protocol-aware mutation for network testing.
Packet Replay Forge is a web-based tool for security researchers and network
engineers who need to dissect captured traffic, surgically mutate it at any
protocol layer, and replay the rewritten packets — without touching Wireshark
for the analysis half and tcpreplay for the send half.
Upload a PCAP, browse flows reconstructed from 5-tuples, inspect every field of every layer the way Wireshark renders them, then apply one of four mutation strategies (field replace, regex substitute on payloads, byte fuzz, bit flip) to a single packet, a whole flow, or the entire capture. The mutated packets are rebuilt through Scapy so lengths and checksums recompute automatically; you can then stream them on loopback with live per-packet progress over SSE, or download the result as a new PCAP.
Stock tools split this workflow awkwardly:
- Wireshark is read-only — you can stare at your packets but you can't rewrite them.
- tcpreplay sends bytes at you verbatim. If you want to change a TTL, a TCP flag, or an HTTP header you're writing a tcprewrite pipeline by hand.
- Scapy scripts give you full control, but no flow view, no diff preview, no history — and nothing you can hand to a teammate.
Packet Replay Forge collapses all three into a single UI:
- Dissect — Scapy walks every layer; the frontend renders a Wireshark-style collapsible field tree plus a hex dump.
- Mutate — pick a target (packet / flow / all), pick a strategy, set the parameters. You get a diff preview of the raw frame and a list of every Scapy field that changed (including the checksums the engine recomputed for you).
- Replay — stream live to loopback with rate limiting and loop count,
or export as a new
.pcapfor use with any downstream tool.
Three-pane dark UI: flow list on the left, packet inspector + D3 charts (flow timeline, protocol donut, size histogram) in the center, mutation editor and replay engine on the right.
┌─────────────┐ HTTP/SSE ┌──────────────────────────────┐
│ React SPA │ ───────────▶ │ FastAPI backend │
│ (Vite/TS/D3)│ ◀─────────── │ │
└─────────────┘ JSON/event │ ┌──────────┐ ┌────────────┐ │
│ │ Parser │→ │ FlowEngine │ │
│ │ (Scapy) │ └────────────┘ │
│ └──────────┘ │ │
│ │ ▼ │
│ │ ┌────────────┐ │
│ │ │ Mutation │ │
│ │ │ Engine │ │
│ │ └────────────┘ │
│ ▼ │ │
│ ┌────────────────────────┐ │
│ │ In-memory SessionStore │ │
│ │ (PCAP → dissected AST) │ │
│ └────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────┐ │
│ │ Replay │ │
│ │ Engine │ │
│ └────────────┘ │
└──────────────────────────────┘
- User drops a PCAP on the frontend →
POST /sessions(multipart). pcap/parser.pycallsrdpcap, walks each packet's layer chain, and turns every Scapy field into a JSON-friendly node (bytes → utf-8 / hex,FlagValue→ human string,EDecimal→ float, etc.).pcap/flows.pygroups packets by a normalized 5-tuple so that A→B and B→A collapse into a single bidirectional flow.- The parsed session (raw Scapy packets + summary metadata + flow list) is
stored in an in-memory
SessionStorekeyed by a random 12-char UUID; no database. - Inspection endpoints (
/flows,/packets,/packets/{idx},/stats) serve the UI with typed Pydantic DTOs. - Mutation endpoint (
POST /sessions/{id}/mutate) accepts aMutationSpec, applies it throughmutation/engine.py, stores the rewritten packet list under a mutation id, and returns a diff preview with per-field change annotations. - Replay endpoint (
GET /sessions/{id}/replay/download) streams a freshly written PCAP back to the browser;/replay/streamyields SSE progress events while sending live packets on loopback.
- Upload
.pcap/.pcapng/.capfiles up to 100 MB - Per-packet dissection across Ethernet / IPv4 / IPv6 / TCP / UDP / ICMP / DNS / Raw
- Bidirectional 5-tuple flow reconstruction with bytes, packets, duration
- Wireshark-style layered field tree + hex+ascii dump
- Session stats: protocol byte breakdown, packet size histogram
field_replace— set a single Scapy field on a named layer. Lengths and checksums recompute on rebuild.regex_sub— regex replacement on any Raw payload. Great for HTTP headers, DNS labels, plaintext protocols.byte_fuzz— seeded random byte rewrite over a[offset, offset+len)range.bit_flip— flip a single bit at any byte/bit offset.
Each mutation targets a scope: a single packet, every packet in a flow, or every packet in the session. Diff previews include the hex-encoded before/after frames and a list of which Scapy fields actually changed.
- Export a mutated packet list as a downloadable
.pcap - Live loopback replay via Scapy
sendpwith configurable rate limit (pps), loop count, and interface - Server-Sent-Events stream of per-packet progress: sent, bytes, errors, elapsed
- Safety rail: non-loopback interfaces are refused unless
allow_unsafe=trueis passed explicitly
- Dark three-pane layout, bespoke protocol palette
- D3 flow timeline (horizontal bars colored by protocol)
- D3 protocol byte donut with live legend and total label
- D3 packet size histogram on a symlog scale
- Toast notifications, drag-and-drop upload, SSE-driven progress bar
You need Python 3.12+ and Node 20+.
cd backend
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
./run.sh # uvicorn app.main:app on :8000Health check:
curl http://127.0.0.1:8000/health
# {"status":"ok","service":"packet-replay-forge"}cd frontend
npm install
npm run dev # vite on :5173 with /api → :8000 proxyOpen http://localhost:5173 and drag a PCAP onto the dropzone.
# upload
SID=$(curl -sf -F "file=@/path/to/capture.pcap" \
http://localhost:8000/sessions | jq -r .session_id)
# inspect
curl -sf http://localhost:8000/sessions/$SID/flows | jq
curl -sf http://localhost:8000/sessions/$SID/stats | jq
# mutate: set IP.ttl = 1 on every packet in the session
MID=$(curl -sf -X POST http://localhost:8000/sessions/$SID/mutate \
-H "Content-Type: application/json" \
-d '{"strategy":"field_replace","target_layer":"IP","target_field":"ttl","value":1}' \
| jq -r .mutation_id)
# download the rewritten pcap
curl -sf "http://localhost:8000/sessions/$SID/replay/download?mutation_id=$MID" \
-o mutated.pcap
# stream a live replay on loopback (SSE)
curl -sN "http://localhost:8000/sessions/$SID/replay/stream?mutation_id=$MID&iface=lo0&pps=100"Most PCAP rewriting tools let you overwrite bytes at offsets, which means you
have to manually fix up every length field, every checksum, every option
list. Packet Replay Forge operates on Scapy's field tree:
def field_replace(pkt, layer_name, field, value):
new_pkt = pkt.__class__(bytes(pkt)) # clone by re-parse
target = find_layer(new_pkt, layer_name)
setattr(target, field, coerce(value))
_force_recompute(new_pkt) # drop cached len/chksum
return new_pkt.__class__(bytes(new_pkt)) # rebuild → recompute_force_recompute walks every layer in the chain and deletes any cached
len, plen, or chksum field. When Scapy rebuilds the packet on
bytes(new_pkt), it recomputes them from scratch — so when you change
IP.ttl, the IP header checksum updates too; when you change
Raw.load length, IP.len and the TCP checksum both update.
Scapy registers link-layer types lazily — if you from scapy.layers.inet import IP without ever touching scapy.all, the DLT tables stay partially
populated and wrpcap crashes with KeyError: <class 'scapy.packet.Raw'>.
The parser and replay modules intentionally import scapy.all to force full
registration. (I learned this the slow way; look for the comment in
backend/app/pcap/parser.py.)
FastAPI's StreamingResponse plus a tiny yield f"data: {json.dumps(ev)}\n\n"
generator is enough to push live progress events to the browser. On the
client, EventSource delivers them with automatic reconnect. Each tick is
one packet send, so pps is enforced by sleeping for
max(0, target_time - elapsed) after every send — simple drift-correcting
rate limit, no jitter accumulator needed.
5-tuples are normalized so A:x→B:y and B:y→A:x collapse into one flow:
a, b = (src, sport), (dst, dport)
if a > b:
a, b = b, a
key = (proto, a[0], b[0], a[1], b[1], proto)The display endpoints use the first-observed direction; the matching endpoints use the normalized form.
backend/
app/
main.py FastAPI entrypoint + CORS + router wiring
models.py Pydantic DTOs shared with the frontend
storage.py In-memory SessionStore
pcap/parser.py Scapy → layered JSON dissector
pcap/flows.py 5-tuple flow grouping
mutation/strategies field_replace, bit_flip, byte_fuzz, regex_sub
mutation/engine.py Spec → packet list + diff
replay/engine.py write_pcap + live loopback sendp w/ rate limit
routes/ sessions, inspect, mutate, replay
frontend/
src/
App.tsx Three-pane layout + session state
api.ts Typed fetch client (proxied via Vite /api)
types.ts Mirrors of backend Pydantic models
components/ Header, Upload, FlowList, PacketInspector,
MutationEditor, ReplayPanel, charts/*
- Live replay defaults to
lo0. Sending on a real interface requiresallow_unsafe=trueand adequate privileges (sudo), and the engine will surface BPF errors to you directly via the SSE stream rather than swallowing them. - The mutation engine never touches the original parsed packet list; it
always clones via
pkt.__class__(bytes(pkt))before mutating. - There is no persistence. Sessions live in process memory and disappear when the server restarts — by design.
MIT
