Programming with Ethernet in Python using Socket API
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:
- Do we know how many bytes the server sent in the message?
- What if the message is too big to fit in a single Ethernet frame?
- 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
- In our second example, what problem/improvement is made? Did it address all the problems discussed?
- How do you make further improvement to address the rest of the problems discussed?