We assume the environment introduced in the previous lecture. In this hands-on session, we will create data link raw sockets using Linux and Python 3 and try to send/receive data using sockets.

Hands-On with Sample Code

Step 1. Preparation

Here is the sample code. (This session is based on this Python sample code. If you prefer C language, please refer to the C version.)

Open the terminal and type:

$ git clone https://github.com/y-sira/pyng.git

You can find network interface names and MAC addresses on your virtual machine using ip command as follows:

$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:dd:d7:43 brd ff:ff:ff:ff:ff:ff
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:b0:d6:ff brd ff:ff:ff:ff:ff:ff
4: enp0s9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:e6:4d:39 brd ff:ff:ff:ff:ff:ff
5: enp0s10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:8e:75:44 brd ff:ff:ff:ff:ff:ff

Please remember interface names and corresponding MAC addresses.

Step 2. Start Packet Capture Using Wireshark

Open the terminal and type:

$ sudo wireshark

Wireshark screen will be appeared.

Wireshark start screen

Double click the interface name you want to monitor, then you can see packets sent to the interface.

Wireshark list packets

Step 3. Send/Receive Frames Using Layer 2 Raw Sockets

Move to pyng directory and run the server script:

$ cd /path/to/pyng
$ sudo ./pyngd $SERVER_INTERFACE_NAME

Open another terminal, move to pyng directory, and run the client script to send the frame to the server:

$ cd /path/to/pyng
$ sudo ./pyng $DESTINATION_MAC_ADDRESS $CLIENT_INTERFACE_NAME
Sent!

Make sure that the data client script sent is appeared in the server side terminal.

pyng outputs

Step 4. Check Frames with Wireshark

Stop packet capture. You can filter the packet by protocol, source address, destination address, …, etc. Please enter eth.type == 0x88b5 in the filter text box to check frames you sent.

wireshark filter packets

Verify the Ethernet header and the payload.

Step 5. Considerations

Please consider below.

  • What data is sent?
    • Why does the payload contain many zero?
  • How do we send/receive data in the sample code?
    • What is struct.pack('!6s6sH', ...) in the server side script?
  • What does the magic number 0x88b5 mean?
  • What kinds of packets are displayed in Wireshark?
  • What does the MAC address ff:ff:ff:ff:ff:ff mean?
  • What is select in the server side sample code?
    • Why do not use recv() directly?
    • How can we handle massive requests?

For Beginners

Socket APIs

The socket APIs are APIs for interprocess communication and network communication from applications. It is available on UNIX and Linux. In Linux, it is easy to handle the data link layer by using the socket API. Let’s explain Linux sockets later.

In order to send and receive data using socket APIs, we have to generate a socket descriptor. The socket descriptor can be generated using int socket (int family, int type, int protocol) defined in sys/socket.h. The first argument family specifies the address family. For the data link layer, specify AF_PACKET defined in sys/socket.h. The second argument type specifies the socket type. For the data link layer, specify SOCK_RAW defined in sys/socket.h. The third argument protocol specifies the protocol. Specify ETH_P_ALL to retrieve any Ethernet frames, and specify ETH_P_IP to retrieve Ethernet frames containing a IP packet. The protocol types of the Ethernet frame is defined in linux/if_ether.h.

In Python, there is a standard library that wraps the OS level socket APIs, so we will use it to create a data link raw socket. Please be careful to close() after using the socket.

import socket
ETH_P_ALL = 3
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_ALL))
s.close()

We use bind() to bind the network interface to the socket. You can check the network interface available on your computer with the following command.

$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:dd:d7:43 brd ff:ff:ff:ff:ff:ff
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:b0:d6:ff brd ff:ff:ff:ff:ff:ff
4: enp0s9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:e6:4d:39 brd ff:ff:ff:ff:ff:ff
5: enp0s10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:8e:75:44 brd ff:ff:ff:ff:ff:ff

For example, to bind the network interface enp0s3 to the socket, write as follows.

import socket
ETH_P_ALL = 3
interface = 'enp0s3'
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_ALL))
s.bind((interface, 0))
# do something
s.close()

Now you are ready to send and receive raw data on the data link layer using the network interface enp0s3.

Receive Data Using Raw Sockets

Use recv() to receive data using a socket. Specify the buffer size in first argument of recv(). recv() will block until ready to receive data from the socket. The return value of recv() is the received byte sequence. This byte string consists of the header and the payload of the Ethernet frame.

When receiving data sent to enp0s3, the script server.py is as follows:

import socket
ETH_P_ALL = 3
ETH_FRAME_LEN = 1514  # Max. octets in frame sans FCS
interface = 'enp0s3'
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_ALL))
s.bind((interface, 0))
data = s.recv(ETH_FRAME_LEN)
print(data)           # => b'\x08\x00\x27\xdd\xd7\x43\x08\x00\x27\x8e\x75\x44\x88\xb5Hi'
s.close()

Note that the data link raw sockets require a root permission. Please run the above script as root.

When the data is sent to enp0s3, you can get the following output:

$ sudo python3 server.py
b"\x08\x00'\xdd\xd7C\x08\x00'\x8euD\x88\xb5Hi"

Send Data Using Raw Sockets

To send data using a socket, use sendall(). The data to be sent must be a byte sequence containing the header and payload of the Ethernet frame.

When sending data “Hi” from enp0s10 (08:00:27:8e:75:44) to enp0s3 (08:00:27:dd:d7:43), the script client.py is as follows:

import socket
ETH_P_ALL = 3
interface = 'enp0s10'
dst = b'\x08\x00\x27\xdd\xd7\x43'  # destination MAC address
src = b'\x08\x00\x27\x8e\x75\x44'  # source MAC address
proto = b'\x88\xb5'                # ethernet frame type
payload = 'Hi'.encode()            # payload
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_ALL))
s.bind((interface, 0))
s.sendall(dst + src + proto + payload)
s.close()

Then,

$ sudo python3 client.py
Sent!

Make sure that the server side terminal outputs the data you sent.

See Also