Scapy

Download version 0.9.15beta : scapy.py (needs python >= 2.2) (works only on linux for the moment)
Get the archive scapy-0.9.15.tgz (man page, ChangeLogs, etc.)
See changelog, bugs, todo list.
The "work-in-progress" version is here
Packages: debian, RPM

Download the scapy presentation slides, LSM03
You will also find an article in Linux Magazine (France) 52.

Bugs, suggestions, etc → biondi@cartel-securite.fr.
Mailing-list: scapy@scapy.tuxfamily.org (subscribe: scapy-subscribe@scapy.tuxfamily.org) (Archive)

Intro

Scapy is a powerful interactive packet manipulation tool, packet generator, network scanner, network discovery, packet sniffer, etc. It can for the moment replace hping, 85% of nmap, arpspoof, arp-sk, arping, tcpdump, tethereal, p0f, ....

Scapy uses the python interpreter as a command board. That means that you can use directly python language (assign variables, use loops, define functions, etc.) If you give a file as parameter when you run scapy, your session (variables, functions, intances, ...) will be saved when you leave the interpretor, and restored the next time you launch scapy.

Scapy is not user proof yet. But it is almost reliable. Some more things need to be done to support more platforms.

The idea is simple. Those kind of tools do two things : sending packets and receiving answers. That's what scapy does : you define a set of packets, it sends them, receives answers, matches requests with answers and returns a list of packet couples (request, answer) and a list of unmatched packets. This has the big advantage over tools like nmap or hping that an answer is not reduced to (open/closed/filtered), but is the whole packet.

On top of this can be build more high level functions, for example one that does traceroutes and give as a result only the start TTL of the request and the source IP of the answer. One that pings a whole network and gives the list of machines answering. One that does a portscan and returns a LaTeX report.

Quick demo : an interactive session

If you are new to python and you really don't understand a word because of that, or if you want to learn this language, take an hour to read the very good tutorial from Guido Van Rossum here: http://www.python.org/doc/current/tut/tut.html. After that, you'll know python :) (really!)

First, we introduce the Net class, which implicitely defines a set of IP addresses. Note that this class does not need to be used to give set of addresses as parameters, it will be automatically used. We also see that sessions work :)

# ./scapy.py -s mysession
New session [mysession]
Welcome to Scapy (0.9beta)
>>> Net("192.168.1.0/24")
<Net 192.168.1.0/24>
>>> target=Net("www.target.com")
>>> targetnet=Net("www.target.com/30")
>>> [ip for ip in targetnet]
['173.29.39.100', '173.29.39.101', '173.29.39.102', '173.29.39.103']
>>> ^D

# ./scapy.py -s mysession
Using session [mysession]
Welcome to Scapy (0.9beta)
>>> target
<Net www.target.com>

The configuration is hold into a variable named conf, that is saved with the session.

>>> conf
L2listen   = <class scapy.L2ListenSocket at 0x83a77dc>
L2socket   = <class scapy.L2Socket at 0x83aabbc>
L3socket   = <class scapy.L3PacketSocket at 0x83aa8cc>
filter     = 'not implemented'
histfile   = '/home/pbi/.scapy_history'
iff        = 'eth0'
promisc    = 'not implemented'
session    = ''
sniff_promisc = 0
stealth    = 'not implemented'
verb       = 2
>>> conf.verb=1

Now, let's manipulate some packets. Here you can see layers that are supported for the moment. It's really easy to add one.

>>> ls()
DNSRR      : DNS Resource Record
DNSQR      : DNS Question Record
LLC        : LLC
Dot1Q      : 802.1Q
ICMPerror  : ICMP in ICMP citation
Ether      : Ethernet
Raw        : Raw
LLPPP      : PPP Link Layer
TCP        : TCP
TCPerror   : TCP in ICMP citation
ICMP       : ICMP
Dot3       : 802.3
Packet     : abstract packet
IP         : IP
Padding    : Padding
IPerror    : IP in ICMP citation
ARP        : ARP
DNS        : DNS
EAPOL      : EAPOL
UDPerror   : UDP in ICMP citation
STP        : Spanning Tree Protocol
UDP        : UDP
EAP        : EAP
>>> ls(Ether)
dst        : DestMACField (None)
src        : SourceMACField (None)
type       : XShortField (0)
>>> ls(IP)
version    : BitField (4)
ihl        : BitField (None)
tos        : XByteField (0)
len        : ShortField (None)
id         : ShortField (1)
flags      : BitField (0)
frag       : BitField (0)
ttl        : ByteField (64)
proto      : ByteField (0)
chksum     : XShortField (None)
src        : SourceIPField (None)
dst        : IPField ('127.0.0.1')
options    : IPoptionsField ('')
>>> IP()
<IP |''>
>>> a=IP(dst="172.16.1.40")
>>> a
<IP dst=172.16.1.40 |''>
>>> a.dst
'172.16.1.40'
>>> a.ttl
64
A layer has default values for every field, so that you don't have to fill them all. If you give a value to the field, it will overload the default value. If you delete the field, the default value will be back. Moreover, fields with default values are not displayed.
>>> a.ttl=32
>>> a
<IP dst=172.16.1.40 ttl=32 |''>
>>> del(a.ttl)
>>> a
<IP dst=172.16.1.40 |''>
>>> a.ttl
64
Fields can be done human readable. For example IP and TCP flags : (note the rfc3514 compliance for IP).
>>> t=TCP()
>>> t.flags="SA"
>>> t.flags     
18
>>> t
<TCP flags=SA |>
>>> t.flags=23
>>> t
<TCP flags=FSRA |>
>>> 
>>> i=IP(flags="DF+MF")
>>> i.flags
3
>>> i
<IP flags=MF+DF |>
>>> i.flags=6
>>> i
<IP flags=DF+evil |>
Some default values are not constant values. For example, the source IP of a packet will default to the IP of the interface that should be used to send a packet to the given destination, according to the local routing tables.
>>> a.dst
'172.16.1.40'
>>> a.src
'172.16.1.24'
>>> del(a.dst)
>>> a.dst
'127.0.0.1'
>>> a.src
'127.0.0.1'
>>> a.dst="192.168.11.10"
>>> a.src
'192.168.11.1'
>>> a.dst=target
>>> a.src
'172.16.1.24'
>>> a.src="1.2.3.4"
>>> a
<IP src=1.2.3.4 dst=<Net www.target.com> |''>
Here, you can guess that my routing table looks like :
$ route -n 
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
172.16.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0
192.168.11.0    0.0.0.0         255.255.255.0   U     0      0        0 eth1
0.0.0.0         172.16.1.1      0.0.0.0         UG    0      0        0 eth0

The / operator has been used as a composition operator between two layers. When doing so, the lower layer can have one or more of its defaults fields overloaded according to the upper layer. (You still can give the value you want). A string can be used as a raw layer.

>>> IP()
<IP |''>
>>> IP()/TCP()
<IP proto=6 |<TCP |''>>
>>> Ether()/IP()/TCP()
<Ether type=0x800 |<IP proto=6 |<TCP |''>>>
>>> IP()/TCP()/"GET /index.html HTTP/1.0\n\n"
<IP proto=6 |<TCP |<Raw load='GET /index.html HTTP/1.0\n\n' |''>>>
>>> Ether()/IP()/IP()/IP()/UDP()
<Ether type=0x800 |<IP proto=0 |<IP proto=0 |<IP proto=17 |<UDP |''>>>>>
>>> IP(proto=55)/TCP()
<IP proto=55 |<TCP |''>>

Each packet can be build or dissected (note: in python _ (underscode) is the latest result) :

>>> str(IP())
'E\x00\x00\x14\x00\x01\x00\x00@\x00|\xe7\x7f\x00\x00\x01\x7f\x00\x00\x01'
>>> IP(_)
<IP frag=0 src=127.0.0.1 proto=0 tos=0x0 dst=127.0.0.1 chksum=0x7ce7 len=20 version=4 flags=0 ihl=5 ttl=64 id=1 |''>
>>> a=Ether()/IP(dst=target)/TCP()/"GET /index.html HTTP/1.0 \n\n"
>>> b=str(a)
>>> b      
'\x00\x90\x7f\x1e \xc8\x00\x03G\x88\x1d/\x08\x00E\x00\x00C\x00\x01\x00\x00@\x06
 \xcd7\xac\x10\x01\x18\xad\x1d\'e\x00P\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x0
 2\x00\x00/\xf9\x00\x00GET /index.html HTTP/1.0 \n\n'
>>> c=Ether(b)
>>> c
<Ether src=00:03:47:88:1d:2f dst=00:90:7f:1e:26:c8 type=0x800 |<IP frag=0 
 src=172.16.1.24 proto=6 tos=0x0 dst=173.29.39.101 chksum=0xcd37 len=67 options='' version=4
 flags=0 ihl=5 ttl=64 id=1 |<TCP reserved=0 seq=0L ack=0L dataofs=5 dport=80 window=0
 flags=0x2 chksum=0x2ff9 urgptr=0 sport=80 options='' | <Raw load='GET /index.html HTTP/1.0 \n\n' |''>>>>
We see that a dissected packet has all its fields filled. That's because I consider that each field has its value imposed by the original string. If this is too verbose, the method hide_defaults() will delete every field that has the same value as the default.
>>> c.hide_defaults()
>>> c
<Ether src=00:03:47:88:1d:2f dst=00:90:7f:1e:26:c8 type=0x800 |<IP src=172.16.1.24
 proto=6 dst=173.29.39.101 chksum=0xcd37 len=67 ihl=5 |<TCP dataofs=5 chksum=0x2ff9 |
 <Raw load='GET /index.html HTTP/1.0 \n\n' |''>>>>

For the moment, we have only generated one packet. Let see how to specify sets of packets as easily. Each field of the whole packet (ever layers) can be a set. This implicidely define a set of packets, generated using a kind of cartesian product between all the fields.

>>> a=IP(dst=targetnet)
>>> a
<IP dst=<Net www.target.com/30> |''>
>>> [p for p in a]
[<IP dst=173.29.39.100 |''>, <IP dst=173.29.39.101 |''>, <IP dst=173.29.39.102 |''>, <IP dst=173.29.39.103 |''>]
>>> b=IP(ttl=[1,2,(5,9)])
>>> b
<IP ttl=[1, 2, (5, 9)] |''>
>>> [p for p in b]
[<IP ttl=1 |''>, <IP ttl=2 |''>, <IP ttl=5 |''>, <IP ttl=6 |''>,
 <IP ttl=7 |''>, <IP ttl=8 |''>, <IP ttl=9 |''>]
>>> c=TCP(dport=[80,443])
>>> [p for p in a/c]
[<IP dst=173.29.39.100 proto=6 |<TCP dport=80 |''>>, <IP dst=173.29.39.100 proto=6 |<TCP dport=443 |''>>,
 <IP dst=173.29.39.101 proto=6 |<TCP dport=80 |''>>, <IP dst=173.29.39.101 proto=6 |<TCP dport=443 |''>>,
 <IP dst=173.29.39.102 proto=6 |<TCP dport=80 |''>>, <IP dst=173.29.39.102 proto=6 |<TCP dport=443 |''>>,
 <IP dst=173.29.39.103 proto=6 |<TCP dport=80 |''>>, <IP dst=173.29.39.103 proto=6 |<TCP dport=443 |''>>]
Some operations (like building the string from a packet) can't work on a set of packets. In these cases, if you forgot to unroll your set of packets, only the first element of the list you forgot to generate will be used to assemble the packet.

Now, let's try to do some fun things. The sr() function is for sending packets and receiving answers. The function returns a couple of packet and answers, and the unanswered packets. The function sr1() is a variant that only return one packet that answered the packet (or the packet set) sent. The packets must be layer 3 packets (IP, ARP, etc.). The function srp() do the same for layer 2 packets (Ethernet, 802.3, etc.).

>>> p=sr1(IP(dst="172.16.1.40")/ICMP()/"XXXXXXXXXXX")
Finished to send 1 packets.
*
Received 1 packets, got 1 answers, remaining 0 packets
>>> p
<IP frag=0 src=172.16.1.40 proto=1 tos=0x0 dst=172.16.1.24 chksum=0xd56c 
  len=39 options='' version=4 flags= ihl=5 ttl=255 id=35848 |<ICMP code=0
  type=echo-reply id=0x0 seq=0x0 chksum=0xee45 |<Raw load='XXXXXXXXXXX' |
  <Padding load='\x00\x00\x00\x00\x00\x00\x00' |>>>>
>>> p.display()
---[ IP ]---
version   = 4
ihl       = 5
tos       = 0x0
len       = 39
id        = 35848
flags     = 
frag      = 0
ttl       = 255
proto     = ICMP
chksum    = 0xd56c
src       = 172.16.1.40
dst       = 172.16.1.24
options   = ''
---[ ICMP ]---
   type      = echo-reply
   code      = 0
   chksum    = 0xee45
   id        = 0x0
   seq       = 0x0
---[ Raw ]---
      load      = 'XXXXXXXXXXX'
---[ Padding ]---
         load      = '\x00\x00\x00\x00\x00\x00\x00'

A TCP traceroute.

>>> ans,unans=sr(IP(dst=target, ttl=(4,25),id=RandShort())/TCP(flags=0x2))
*****.******.*.***..*.**Finished to send 22 packets.
***......
Received 33 packets, got 21 answers, remaining 1 packets
>>> for snd,rcv in ans:
...     print snd.ttl, rcv.src, isinstance(rcv.payload, TCP)
... 
5 194.51.159.65 0
6 194.51.159.49 0
4 194.250.107.181 0
7 193.251.126.34 0
8 193.251.126.154 0
9 193.251.241.89 0
10 193.251.241.110 0
11 193.251.241.173 0
13 208.172.251.165 0
12 193.251.241.173 0
14 208.172.251.165 0
15 206.24.226.99 0
16 206.24.238.34 0
17 173.109.66.90 0
18 173.109.88.218 0
19 173.29.39.101 1
20 173.29.39.101 1
21 173.29.39.101 1
22 173.29.39.101 1
23 173.29.39.101 1
24 173.29.39.101 1

A DNS query (rd = recursion desired).

>>> sr1(IP(dst="172.16.1.40")/UDP()/DNS(rd=1,qd=DNSQR(qname="www.target.com")))
Finished to send 1 packets.
.*
Received 2 packets, got 1 answers, remaining 0 packets
<IP frag=0 src=172.16.1.40 proto=UDP tos=0x0 dst=172.16.1.24 chksum=0xdfb8 
 len=212 options='' version=4 flags=DF ihl=5 ttl=64 id=0 |<UDP dport=80
 sport=53 len=192 chksum=0x188 |<DNS aa=0 qr=1 an=<DNSRR rdata='173.29.33.99' 
 ttl=300L rrname='www.target.com.' type=A class=IN |> ns=<DNSRR 
 rdata='ns4.target.com.' ttl=345600L rrname='target.com.' type=NS class=IN 
 |<DNSRR rdata='ns1.target.com.' ttl=345600L rrname='target.com.' type=NS 
 class=IN |<DNSRR rdata='ns2.target.com.' ttl=345600L rrname='target.com.'
 type=NS class=IN |<DNSRR rdata='ns3.target.com.' ttl=345600L 
 rrname='target.com.' type=NS class=IN |>>>> nscount=4 qdcount=1 
 tc=0 ancount=1 rd=1 arcount=4 ar=<DNSRR rdata='173.29.32.10' ttl=326818L 
 rrname='ns1.target.com.' type=A class=IN |<DNSRR rdata='173.29.34.10' 
 ttl=326818L rrname='ns2.target.com.' type=A class=IN |<DNSRR 
 rdata='173.29.36.10' ttl=326818L rrname='ns3.target.com.' type=A class=IN
 |<DNSRR rdata='173.29.38.10' ttl=326818L rrname='ns4.target.com.' type=A
 class=IN |>>>> opcode=0 ra=1 z=0 rcode=0 id=0 qd=<DNSQR 
 qclass=IN qtype=A qname='www.target.com.' |> |>>>
>>> _.an
<DNSRR rdata='173.29.33.99' ttl=300L rrname='www.target.com.' type=A class=IN |>

The process of sending packets and receiving is quite complicated. As I wanted to use the PF_PACKET interface to go through netfilter, I also needed to implement an ARP stack and ARP cache, and a LL stack. Well it seems to work, on ethernet and PPP interfaces, but I don't guarantee anything. Anyway, the fact I used a kind of super-socket for that mean that you can switch your IO layer very easily, and use PF_INET/SOCK_RAW, or use PF_PACKET at level 2 (giving the LL header (ethernet,...) and giving yourself mac addresses, ...). I've just added a super socket which use libdnet and libpcap, so that it should be portable :

>>> conf.L3socket=L3dnetSocket
>>> conf.L3listen=L3pcapListenSocket

We can easily capture some packets or even clone tcpdump or tethereal. If no interface is given, sniffing will happen on every interfaces.

>>> sniff(count=2) 
[<Ether src=00:d0:b7:88:50:f2 dst=00:03:47:88:1d:2f type=0x800 |<IP frag=0
  src=172.16.1.40 proto=1 tos=0x0 dst=172.16.1.24 chksum=0x3974 len=84 options=''
  version=4 flags=0 ihl=5 ttl=255 id=10196 |<ICMP code=0 type=0 id=0xdc0f 
  seq=0x7138 chksum=0x25e5 |<Raw load='>r\x15f\x00\x07M\xf0\x08\t\n\x0b
  \x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e
  \x1f !"#$%&\'()*+,-./01234567' |''>>>>, 
 <Ether src=00:d0:b7:88:50:f2 dst=00:03:47:88:1d:2f type=0x800 |<IP frag=0 
  src=172.16.1.40 proto=1 tos=0x0 dst=172.16.1.24 chksum=0x3973 len=84 options=''i
  version=4 flags=0 ihl=5 ttl=255 id=10197 |<ICMP code=0 type=0 id=0xdc0f 
  seq=0x7238 chksum=0x1792 |<Raw load='>r\x15f\x00\x07[C\x08\t\n\x0b\x0c\r\x0e
  \x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'
  ()*+,-./01234567' |''>>>>]
>>> sniff(iface="wifi0", prn=lambda x: x.summary())
802.11 Management 8 ff:ff:ff:ff:ff:ff / 802.11 Beacon / Info SSID / Info Rates / Info DSset / Info TIM / Info 133
802.11 Management 4 ff:ff:ff:ff:ff:ff / 802.11 Probe Request / Info SSID / Info Rates
802.11 Management 5 00:0a:41:ee:a5:50 / 802.11 Probe Response / Info SSID / Info Rates / Info DSset / Info 133
802.11 Management 4 ff:ff:ff:ff:ff:ff / 802.11 Probe Request / Info SSID / Info Rates
802.11 Management 4 ff:ff:ff:ff:ff:ff / 802.11 Probe Request / Info SSID / Info Rates
802.11 Management 8 ff:ff:ff:ff:ff:ff / 802.11 Beacon / Info SSID / Info Rates / Info DSset / Info TIM / Info 133
802.11 Management 11 00:07:50:d6:44:3f / 802.11 Authentication
802.11 Management 11 00:0a:41:ee:a5:50 / 802.11 Authentication
802.11 Management 0 00:07:50:d6:44:3f / 802.11 Association Request / Info SSID / Info Rates / Info 133 / Info 149
802.11 Management 1 00:0a:41:ee:a5:50 / 802.11 Association Response / Info Rates / Info 133 / Info 149
802.11 Management 8 ff:ff:ff:ff:ff:ff / 802.11 Beacon / Info SSID / Info Rates / Info DSset / Info TIM / Info 133
802.11 Management 8 ff:ff:ff:ff:ff:ff / 802.11 Beacon / Info SSID / Info Rates / Info DSset / Info TIM / Info 133
ARP who has 172.20.70.172 says 172.20.70.171 / Padding
ARP is at 00:0a:b7:4b:9c:dd says 172.20.70.172 / Padding
ICMP echo-request 0 / Raw
ICMP echo-reply 0 / Raw
>>> sniff(iface="lo", prn=lambda x: x.display())
---[ Ethernet ]---
dst       = 00:00:00:00:00:00
src       = 00:00:00:00:00:00
type      = 0x800
---[ IP ]---
   version   = 4
   ihl       = 5
   tos       = 0x0
   len       = 84
   id        = 0
   flags     = 2
   frag      = 0
   ttl       = 64
   proto     = 1
   chksum    = 0x3ca7
   src       = 127.0.0.1
   dst       = 127.0.0.1
   options   = ''
---[ ICMP ]---
      type      = echo-request
      code      = 0
      chksum    = 0x4f7c
      id        = 0xe10f
      seq       = 0x0
---[ Raw ]---
         load      = '>r\x15\xe0\x00\n\x88\x14\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11
\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567'
---[ Ethernet ]---
dst       = 00:00:00:00:00:00
src       = 00:00:00:00:00:00
type      = 0x800
---[ IP ]---
   version   = 4
   ihl       = 5
   tos       = 0x0
   len       = 84
   id        = 35452
   flags     = 0
   frag      = 0
   ttl       = 64
   proto     = 1
   chksum    = 0xf22a
   src       = 127.0.0.1
   dst       = 127.0.0.1
   options   = ''
---[ ICMP ]---
      type      = echo-reply
      code      = 0
      chksum    = 0x577c
      id        = 0xe10f
      seq       = 0x0
---[ Raw ]---
         load      = '>r\x15\xe0\x00\n\x88\x14\x08\t\n\x0b\x0c\r\x0e\x0f\x10
\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567'

We can sniff and do passive OS fingerprinting.

>>> p
<Ether src=00:40:33:96:7b:60 dst=00:10:4b:b3:7d:4e type=0x800 |<IP frag=0
 src=192.168.8.10 proto=6 tos=0x10 dst=192.168.8.1 chksum=0xb85e len=60
 options='' version=4 flags=2 ihl=5 ttl=64 id=61681 |<TCP reserved=0
 seq=2023566040L ack=0L dataofs=10 dport=80 window=5840 flags=SEC
 chksum=0x570c urgptr=0 sport=46511 options={'Timestamp': (342940201L, 0L),
 'MSS': 1460, 'NOP': (), 'SAckOK': '', 'WScale': 0} |''>>>
>>> p0f(p)
(1.0, ['Linux 2.4.2 - 2.4.14 (1)'])
>>> a=sniff(prn=prnp0f)
(1.0, ['Linux 2.4.2 - 2.4.14 (1)'])
(1.0, ['Linux 2.4.2 - 2.4.14 (1)'])
(0.875, ['Linux 2.4.2 - 2.4.14 (1)', 'Linux 2.4.10 (1)', 'Windows 98 (?)'])
(1.0, ['Windows 2000 (9)'])
The number before the OS guess is the accurracy of the guess.

Demo of both bpf filter and sprintf() method :

>>> a=sniff(filter="tcp and ( port 25 or port 110 )",
 prn=lambda x: x.sprintf("%IP.src%:%TCP.sport% -> %IP.dst%:%TCP.dport%  %2s,TCP.flags% : %TCP.payload%"))
192.168.8.10:47226 -> 213.228.0.14:110   S : 
213.228.0.14:110 -> 192.168.8.10:47226  SA : 
192.168.8.10:47226 -> 213.228.0.14:110   A : 
213.228.0.14:110 -> 192.168.8.10:47226  PA : +OK <13103.1048117923@pop2-1.free.fr>

192.168.8.10:47226 -> 213.228.0.14:110   A : 
192.168.8.10:47226 -> 213.228.0.14:110  PA : USER toto

213.228.0.14:110 -> 192.168.8.10:47226   A : 
213.228.0.14:110 -> 192.168.8.10:47226  PA : +OK 

192.168.8.10:47226 -> 213.228.0.14:110   A : 
192.168.8.10:47226 -> 213.228.0.14:110  PA : PASS tata

213.228.0.14:110 -> 192.168.8.10:47226  PA : -ERR authorization failed

192.168.8.10:47226 -> 213.228.0.14:110   A : 
213.228.0.14:110 -> 192.168.8.10:47226  FA : 
192.168.8.10:47226 -> 213.228.0.14:110  FA : 
213.228.0.14:110 -> 192.168.8.10:47226   A : 

Soon to come : examples for arping, scanning, arp cache poisoning, dns spoofing, etc. (they are present in the scapy presentation slides)

Bugs

Todo list

Any suggestions are welcome.