Programming with Ethernet with ScaPy
Programing with Ethernet using ScaPy
ScaPy is a Python tool and library for working with network packets. This tutorial is to use it to program with Ethernet.
Determining Version of ScaPy
ScaPy is an evolving software, before we consult 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 thescapypackage, e.g.,$ python -c "import scapy; print(scapy.__version__);"
Determinning Version of Python
It is also a good habbit 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 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 extend. 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 a 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 with this packet? You can view its documentation by
help(packet)
If you are patient, you will see that the documenation 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 there is an 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 particuar, when the network is very “chatty”, we may have to apply to filter to capture those
are of our interest.
For instance, we ancipate the packet we are sending to arrivate 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 will have 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 signicant amount packets captured, we can add a condition to the filter to filter the packet 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 another Python program, as shown below were 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
- 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.
- For the programs listed here, we must run the programs as the root user. Why do think we are required to run the programa as the root user?
-
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,0x4865inether[14:2]? - 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?