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.

  1. Run ScaPy and observe the version number announced:

    $ scapy3
    
  2. Use pip

    $ pip3 list | grep scapy
    
  3. Print __version__ of the scapy package, 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

  1. Using the provided program as example, design a program to broadcast messages to all hosts on an Ethernet.
  2. Design an experiment to test your program. Describe the experiment setup and results.
  3. 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?
  4. 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 in ether[14:2]?

  5. 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?