Programming with Ethernet with ScaPy
Programming with Ethernet using ScaPy
ScaPy is a Python tool and library for working with network packets. This tutorial is about using it to interact with Ethernet in our own programs. Complete the exercises in the last section.
Table of Content
Determining Version of ScaPy
ScaPy is an evolving software, before we consult with any documentation, we should know the version of ScaPy installed. There are a few methods to determine the version.
-
Run ScaPy and observe the version number announced:
$ scapy3
-
Use
pip
$ pip3 list | grep scapy
-
Print
__version__
of thescapy
package, e.g.,$ python -c "import scapy; print(scapy.__version__);"
Determinning Version of Python
It is also a good habit to know the version of Python interpreter you are using:
$ python --version
Experiments
We run ScaPy first:
$ scapy3
When we run ScaPy, we in fact run the Python interpreter with ScaPy loaded.
Query Network Interfaces
To list network interfaces in ScaPy, we issue the following function call:
get_if_list()
Below is an example of such an invocation:
>>> get_if_list()
['enp0s8', 'enp0s16', 'lo', 'enp0s10', 'enp0s9', 'enp0s3']
>>>
You may wonder what `get_if_list` is. You can use its online documentation:
```python
>>> help(get_if_list)
Help on function get_if_list in module scapy.arch.linux:
get_if_list()
It tells us that the function belongs to a package called scapy.arch.linux
. We can
list what this package offers:
dir(scapy.arch.linux)
The list of functions the package offers is self-explanatory, at least to an extent. For instance, if we wish to retrieve an Ethernet interface’s MAC address, we can invoke the following functions, e.g., for interface “enp0s8”:
get_if_hwaddr('enp0s8')
or
get_if_raw_hwaddr('enp0s8')
Building Ethernet Frames
To build an Ethernet frame, we use ScaPy’s Ether
class to create an object:
packet = Ether()
To obtain a human-readable representation of such a packet, we can use the repr
method:
print(repr(packet))
What can we do with this packet? You can view its documentation by
help(packet)
If you are patient, you will see that the documentation contains:
fields_desc = [<Field (Ether).dst>, <Field (Ether).src>, <Field (Ether...
Hopefully, this will refresh your memory about the Ethernet frame format we discussed. Now, let’s consider the Ethernet called “DiamondMidwoodFlatbush”. There are multiple hosts on the Ethernet. Suppose two hosts are named “midwood” and “flatbush”. Host “midwood” has an interface whose Ethernet address is “08:00:27:08:0d:a1” while host “flatbush” has an interface whose Ethernet address is “08:00:27:cb:67:1d”. For instance, we can look up the hardware addresses of the network interfaces at host “midwood” via the following:
>>> socket.gethostname()
'midwood'
>>> for dev in get_if_list():
...: print(f"{dev:8s} {get_if_hwaddr(dev):17}")
...:
...:
enp0s8 08:00:27:a1:33:b4
enp0s16 08:00:27:a3:d1:e4
lo 00:00:00:00:00:00
enp0s10 08:00:27:a2:b4:f0
enp0s9 08:00:27:08:0d:a1
enp0s3 08:00:27:5a:ff:ec
>>>
If we want to send a message “Hello, World!” from host “midwood” to host “flatbush”, we shall arrange the Ethernet packet as follows:
>>> packet = Ether()
>>> print(repr(packet))
<Ether |>
>>> packet.src = "08:00:27:08:0d:a1"
>>> packet.dst = "08:00:27:cb:67:1d"
>>> packet.payload = Raw("Hello, World!")
>>> print(repr(packet))
<Ether dst=08:00:27:cb:67:1d src=08:00:27:08:0d:a1 |'Hello, World!'>
>>>
Sending Ethernet Frames
Ethernet is a “hardware network” or a host-to-network technology. Loosely speaking, we can regard it as a layer 2 protocol. To do with the Ethernet packet we crafted, we can look up what ScaPy offers us, i.e.,
dir(scapy.layers.l2)
If you are patient, you shall see:
| ----------------------------------------------------------------------
| Static methods defined here:
|
| send_function = sendp(x, inter=0, loop=0, iface=None, iface_hint=None, count=None, verbose=None, realtime=None, return_packets=False, socket=None, *args, **kargs)
| Send packets at layer 2
... ...
Thus, we can send the Ethernet frame using
sendp(packet, iface="enp0s9")
Receiving Ethernet Frames
To receive an Ethernet frame, we simply run sniff
to capture frames. Depending on the network,
in particular, when the network is very “chatty”, we may have to apply a filter to capture only those
that are of our interest.
For instance, we anticipate that the packet we are sending to arrive at interface “enps09” at host “flatbush”, we would run as follows at host “flatbush” before we send the packet at host “midwood”:
packets = sniff(prn=lambda p: p.summary(), iface="enp0s9")
If the network is “chatty”, we may still have a difficult time to know whether the packet we sent arrived. In this case, we can apply a filter:
packets = sniff(prn=lambda p: p.summary(), iface="enp0s9", filter="ether src 08:00:27:08:0d:a1")
If there is still significant amount of packets captured, we can add a condition to the filter narrow down the packets based on the content of the payload:
packets = sniff(prn=lambda p: p.summary(), iface="enp0s9", filter="ether src 08:00:27:08:0d:a1 && ether[14:2] = 0x4865")
Examining Ethernet Frames
We can examine the crafted or the received Ethernet frames in a variety of ways. Here are a few examples:
>>> type(packets)
scapy.plist.PacketList
>>> len(packets)
1
>>> print(repr(packets[0]))
<Ether dst=08:00:27:cb:67:1d src=08:00:27:08:0d:a1 type=0x9000 |<Raw load='Hello, World!' |>>
>>> print(packets[0].summary())
08:00:27:08:0d:a1 > 08:00:27:cb:67:1d (0x9000) / Raw
>>> raw(packets[0])
b"\x08\x00'\xcbg\x1d\x08\x00'\x08\r\xa1\x90\x00Hello, World!"
>>> hexdump(packets[0])
0000 08 00 27 CB 67 1D 08 00 27 08 0D A1 90 00 48 65 ..'.g...'.....He
0010 6C 6C 6F 2C 20 57 6F 72 6C 64 21 llo, World!
>>> packets[0].show()
###[ Ethernet ]###
dst= 08:00:27:cb:67:1d
src= 08:00:27:08:0d:a1
type= 0x9000
###[ Raw ]###
load= 'Hello, World!'
>>>
Python Program using ScaPy
The sending logic can be put in a single Python program, as shown below where we name the sending program as “scapy_ether_send.py”:
$ cat scapy_ether_send.py
import scapy.layers
import scapy.layers.l2
def main():
intf = "enp0s9"
msg = input("Enter a message to send: ")
packet = scapy.layers.l2.Ether()
packet.src = "08:00:27:08:0d:a1"
packet.dst = "08:00:27:cb:67:1d"
packet.payload = scapy.packet.Raw(msg)
scapy.layers.l2.sendp(packet, iface=intf)
if __name__ == "__main__":
main()
The receiving logic can also be in a Python program, as shown below where we name the program as “scapy_ether_recv.py”:
import signal
import sys
import scapy.all as scapy
def signal_handler(sig, frame):
print('You pressed Ctrl+C!')
sys.exit(0)
def main():
signal.signal(signal.SIGINT, signal_handler)
while True:
print("Waiting for message to arrive.")
packets = scapy.sniff(
iface="enp0s9",
filter="ether src 08:00:27:08:0d:a1",
count=1
)
src = packets[0].src
msg = packets[0].payload.load.decode("utf-8")
print(f"Received from {src}: {msg}")
if __name__ == "__main__":
main()
Exercises and Questions
- We use Oracle VirtualBox to create an experiment environment. How do we know two hosts are on the same Ethernet? In another word, if we wish to place to two or more hosts on the same Ethernet, how do we configure in Oracle VirtualBox?
-
In the above, we demonstrate the example of a filter to limit the packets captured as follows:
packets = sniff( prn=lambda p: p.summary(), iface="enp0s9", filter="ether src 08:00:27:08:0d:a1 && ether[14:2] = 0x4865")
Explain the meaning of the values, such as
14
,2
,0x4865
inether[14:2]
? - For the programs listed here, we must run the programs as the root user. Why do you think we are required to run the program as the root user?
- Using the provided program as example, design a program to broadcast messages to all hosts on an Ethernet.
- Design an experiment to test your program. Describe the experiment setup and results. In your setup, you need to show that broadcasting works.