Raw socket packet fuzzer — craft and send packets at every layer without pcap.
go build -o pfuzz ./cmd/pfuzzRequires root (raw sockets).
pfuzz --dump -type <tcp|udp|icmp|arp|ethernet> [flags]
--dump shows the raw hex packet without sending. Drop it to actually transmit.
| Type | Key flags |
|---|---|
| tcp | --tcp-dport `--tcp-flags syn |
| udp | --udp-dport --udp-payload <hex> |
| icmp | --icmp-type --icmp-seq |
| arp | --arp-src --arp-dst `--arp-op request |
| ethernet | --eth-dst --eth-type 0x0806 |
| Flag | Description | Default |
|---|---|---|
-type |
Packet layer | tcp |
-ip-src |
Source IP | interface IP |
-ip-dst |
Destination IP | — |
-ip-ttl |
IP TTL | 64 |
-ip-dscp |
DSCP value (0-63) | 0 |
-ip-id |
IP identification (0=random) | 0 |
-ip-df |
Don't Fragment flag | true |
-tcp-flags |
TCP flags (hyphen-separated) | syn |
-tcp-payload |
Payload as hex string | — |
-udp-payload |
Payload as hex string | — |
-count |
Packets to send (0=unlimited) | 1 |
-delay |
Delay between sends | 0 |
-iface |
Interface name | auto |
-hex |
Dump raw bytes before sending | false |
-dump |
Hex dump and exit (no send) | false |
Instead of constructing packets on the command line, you can drive pfuzz with a JSON config file that describes a full packet sequence. This is useful for scripted workflows like port scans, protocol state machines, or fuzzing campaigns.
| Flag | Description |
|---|---|
--replay |
Path to a JSON replay config file |
--dry-run |
Print packets as hex without sending |
--vars |
key=value,key2=value2 — overrides config vars (CLI wins) |
All packet fields mirror the CLI flags but use JSON keys (snake_case):
| Field | Description |
|---|---|
type |
Packet type (required) |
ip_src |
Source IP |
ip_dst |
Destination IP |
ip_ttl |
IP TTL |
ip_id |
IP identification |
ip_dscp |
DSCP value |
ip_df |
Don't Fragment flag |
ip_proto |
IP protocol |
tcp_sport |
TCP source port |
tcp_dport |
TCP destination port |
tcp_flags |
TCP flags (e.g. "syn", "syn-ack") |
tcp_seq |
TCP sequence number |
tcp_ack |
TCP acknowledgment number |
tcp_window |
TCP window size |
tcp_payload |
Payload as hex string |
udp_sport |
UDP source port |
udp_dport |
UDP destination port |
udp_payload |
Payload as hex string |
icmp_type |
ICMP type (default 8 = echo request) |
icmp_code |
ICMP code |
icmp_id |
ICMP identifier |
icmp_seq |
ICMP sequence number |
icmp_payload |
Payload as hex string |
ospf_type |
OSPF type: hello, dbd, lsr, lsu, lsa |
ospf_rid |
OSPF Router ID |
ospf_aid |
OSPF Area ID |
arp_op |
ARP operation: request, reply |
arp_src |
ARP sender IP |
arp_dst |
ARP target IP |
dst_mac |
Destination MAC |
src_mac |
Source MAC (auto-detected if omitted) |
eth_type |
EtherType (e.g. "0x0800", `"0x0806") |
pause |
Wait duration before next packet |
repeat |
Number of sends (0 = infinite) |
The replay engine supports three kinds of template substitution:
-
Variables —
${VAR_NAME}or${VAR_NAME:default}. Values come from the config'svarsmap first, then environment variables, then the default. -
Environment variables —
${SOME_ENV}without a default will resolve fromos.LookupEnv. -
Range expansion —
{min..max}expands into a cartesian product of values. For example,"ip_dst": "${SUBNET}.{1..5}"produces 5 packets, one per host. Multiple range fields on the same packet generate the full cartesian product.
{
"vars": { "SUBNET": "192.168.1" },
"packets": [
{
"type": "tcp",
"ip_src": "${SUBNET}.1",
"ip_dst": "${SUBNET}.{1..5}", // → 5 packets: .1 through .5
"tcp_dport": "{22..25}", // → 4 ports: 22, 23, 24, 25
"tcp_flags": "syn"
}
]
}This single packet generates 5 × 4 = 20 unique SYN probes.
Run a TLS handshake replay:
sudo ./pfuzz --replay examples/tls-handshake-replay.jsonDry-run a templated port scan:
sudo ./pfuzz --replay examples/templated-scan.json --dry-runOverride vars from the command line:
TARGET=10.0.0.5 PORT=8080 sudo ./pfuzz --replay examples/templated-scan.json \
--vars "TARGET=${TARGET},PORT=${PORT}"# SYN scan to port 443
sudo ./pfuzz -type tcp --ip-src 192.168.1.100 --ip-dst 10.0.0.1 --tcp-dport 443 --tcp-flags syn
# UDP DNS query (hex payload)
sudo ./pfuzz -type udp --ip-src 192.168.1.100 --ip-dst 8.8.8.8 --udp-dport 53 --udp-payload 01000001000000000000
# Ping (ICMP echo)
sudo ./pfuzz -type icmp --ip-src 192.168.1.100 --ip-dst 10.0.0.1 --icmp-seq 1 --count 0 --delay 1s
# ARP request
sudo ./pfuzz -type arp --arp-src 192.168.1.100 --arp-dst 192.168.1.1 --arp-op request
# TCP with custom payload and flags
sudo ./pfuzz -type tcp --ip-src 192.168.1.100 --ip-dst 10.0.0.1 --tcp-sport 54321 --tcp-dport 80 --tcp-flags syn-ack --tcp-payload 48454c4c4f
# Raw Ethernet frame (no IP)
sudo ./pfuzz -type ethernet --eth-dst ff:ff:ff:ff:ff:ff --eth-src 00:11:22:33:44:55 --eth-type 0x0806
# Burst 100 SYN packets
sudo ./pfuzz -type tcp --ip-src 192.168.1.100 --ip-dst 10.0.0.1 --tcp-dport 80 --tcp-flags syn --count 100pfuzz/
├── cmd/pfuzz/main.go # CLI flags, packet dispatch, hex dump
├── internal/packet/ # Ethernet, ARP, IPv4, TCP, UDP, ICMP, OSPF builders
├── internal/replay/ # JSON replay loader and session runner
└── internal/socket/ # AF_INET/SOCK_RAW + AF_PACKET/SOCK_RAW senders
- IP-layer packets (TCP/UDP/ICMP/OSPF):
AF_INET+SOCK_RAW+IPPROTO_RAW(255). Kernel doesn't add an IP header — we supply the full IP header.IP_HDRINCLset viasetsockopt. - ARP and raw Ethernet:
AF_PACKET+SOCK_RAW, bound to the interface at layer 2. Sends the full Ethernet frame directly. - No pcap dependency. No
gopacket. Pure stdlib +syscall.
All checksums (IP, TCP, UDP, ICMP) are computed in userspace before sending. The raw socket sends the fully-formed frame with correct checksums already baked in.
{ "interface": "eth0", // optional — auto-detected if omitted "pause": "50ms", // global default pause between packets "dry_run": true, // optional — same as --dry-run "vars": { // optional — template variables "SERVER": "10.0.0.1", "PORT": "443", "SUBNET": "192.168.1" }, "packets": [ { "type": "tcp", // tcp | udp | icmp | ospf | arp | ethernet "ip_src": "${SUBNET}.1", // template variable "ip_dst": "${SERVER}", "tcp_dport": "${PORT}", // vars + env vars "tcp_flags": "syn", "ip_ttl": 64, "pause": "50ms", // per-packet pause (overrides global) "repeat": 3 // send this packet N times (0 = infinite) } ] }