Programming with Ethernet in Python using Socket API

Python provides a wrapper for Socket API, with which we can directly access service provided by Ethernet.

Table of Content

Client-Server: Sending/Receiving Messages

When you develop a networking application, you often develop two programs, one acts as the server program, and the other the client. The server program will run first, and passively wait for the client to initiate a “session”. The client initiates the session by sending a packet. For the server and the client to work in tandem, they will need to observe a set of rules, including the format of the packet, and the timing of the packet, as well as the meaning of the packets.

In the following example, we design two simple Python programs, ethersend.py and etherrecv.py where the former acts as the client, and the later the server. The client expects the server to send to it a message (a string) as the payload of an Ethernet frame and prints out the message when the message arrives. The sender is of course to send the message as the payload of an Ethernet frame.

First, let’s look at etherrecv.py:

"""etherrecv.py"""
import socket

def main():
  ETH_P_ALL = 3
  sock = socket.socket(
    socket.AF_PACKET,
    socket.SOCK_RAW,
    socket.htons(ETH_P_ALL))
  sock.bind(("enp0s9", 0))

  packet = sock.recv(3000)
  sock.close()

  print(f"{repr(packet):s}")

  dst_addr = packet[:6]
  src_addr = packet[6:12]
  type_value = packet[12:14]
  payload = packet[14:]
  print(dst_addr.hex())
  print(src_addr.hex())
  print(type_value.hex())
  print(payload)
          

if __name__ == "__main__":
  main()

Next, let’s examine ethersend.c:

"""Send a packet via Ethernet

   Example
     python ethersend.py enp0s9 \
       "08:00:27:cb:67:1d" \
       "08:00:27:08:0d:a1" \
       "Hello, World!"
"""
import socket
import sys
import struct

def help():
  print("Usage: ethersend interface src_addr dst_addr payload")

def send_msg(intf, src_addr, dst_addr, payload):
  sock = socket.socket(
    socket.AF_PACKET,
    socket.SOCK_RAW,
    0)
  sock.bind((intf, 0))

  packet = struct.pack(
          "!6s6s2s", 
          bytes.fromhex(dst_addr.replace(":", "")),
          bytes.fromhex(src_addr.replace(":", "")),
          b"\x00\x00")

  sock.send(packet + payload.encode("utf-8"))
  sock.close()

def main():
    if len(sys.argv) != 5:
        help()
        return
    intf, src_addr, dst_addr, payload = sys.argv[1:]
    send_msg(intf, src_addr, dst_addr, payload)

if __name__ == "__main__":
  main()

Client-Server: A Simple Protocol

Observing the previous example, we can conclude that we need additional information to receive the message correctly. To see this, consider:

  1. Do we know how many bytes the server sent in the message?
  2. What if the message is too big to fit in a single Ethernet frame?
  3. What if the message is corrupted during transmission?

Let’s examine the improved version of the sender and receiver:

"""ethermsgrecv.py"""
import socket

PROTOCOL_NUM = socket.htons(0x4321)

def main():
  sock = socket.socket(
    socket.AF_PACKET,
    socket.SOCK_RAW,
    PROTOCOL_NUM)
  sock.bind(("enp0s9", 0))

  packet = sock.recv(3000)
  sock.close()

  dst_addr = packet[:6]
  src_addr = packet[6:12]
  type_value = packet[12:14]
  protocol_packet = packet[14:]
  length = int.from_bytes(protocol_packet[0:2], byteorder="big")
  print(f"length={length}")
  payload = protocol_packet[2:2+length]
  print(f"{dst_addr.hex()}")
  print(f"{src_addr.hex()}")
  print(f"{type_value.hex()}")
  print(f"{payload}")
          

if __name__ == "__main__":
  main()
"""Send a packet via Ethernet

   Example
     python ethermsgsend.py enp0s9 \
       "08:00:27:cb:67:1d" \
       "08:00:27:08:0d:a1" \
       "Hello, World!"
"""
import socket
import sys
import struct

PROTOCOL_NUM = 0x4321

def help():
  print("Usage: ethersend interface src_addr dst_addr payload")

def protocol_packet(msg):
    return len(msg).to_bytes(length=2, byteorder='big') + msg.encode("utf-8")

def send_msg(intf, src_addr, dst_addr, payload):
  sock = socket.socket(
    socket.AF_PACKET,
    socket.SOCK_RAW,
    PROTOCOL_NUM)
  sock.bind((intf, 0))

  packet = struct.pack(
          "!6s6sh", 
          bytes.fromhex(dst_addr.replace(":", "")),
          bytes.fromhex(src_addr.replace(":", "")),
          PROTOCOL_NUM)

  payload = protocol_packet(payload)
  sock.send(packet + payload)
  sock.close()

def main():
    if len(sys.argv) != 5:
        help()
        return
    intf, src_addr, dst_addr, payload = sys.argv[1:]
    send_msg(intf, src_addr, dst_addr, payload)

if __name__ == "__main__":
  main()

Exercises and Questions

  1. In our second example, what problem/improvement is made? Did it address all the problems discussed?
  2. How do you make further improvement to address the rest of the problems discussed?