Ethernet packets transmission with Ruby
Computers use network protocols to transfer data and the basic unit for this is called network packet and I will use ruby and containers to demonstrate how the packet transmission works.
What is an ethernet packet and how does it work?
An ethernet packet or frame is part of a complete network message and carries address information that helps identify its source and destination. And we are going to analyze the ethernet packets and communicate 2 hosts using the MAC addresses only.
An ethernet packet has three parts: the header, protocol data unit (PDU) and footer (trailer).
- The ethernet header includes the source and destination mac address and the protocol type.
- The ethernet payload contains the data using the underlying structure for other protocols (IP, ICMP, ARP, etc)
- The footer (trailer) is used to define the frame check sequence and this is a four-octet cyclic redundancy check.
MAC Addresses
A media access control address (MAC address) is a unique identifier assigned to a network interface controller (NIC) for use as a network address in communications in network.
Ethernet packet transmission
To send a packet from the Host A to the Host B without getting too much information about how the operating system manages the network interfaces to send the packets we are going to use the PCAP library from the Ruby programming language through the pcaprub gem.
The following piece of code will perform the following operations and that does not require root privileges to run:
- Identifies the default network interface.
- Builds the byte string that represent an ethernet packet (source and destination mac addresses, type and payload).
- Injects the ethernet packet to network interface each two seconds forever.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require 'pcaprub'
ifname = Pcap.lookupdev
stream = Pcap.open_live(ifname, 0xffff, false, 1)
eth_saddr = '02:42:ab:aa:bb:02'.split(/[:\x2d\x2e\x5f-]+/).collect {|x| x.to_i(16)}.pack('C6')
eth_daddr = '02:42:ab:aa:bb:01'.split(/[:\x2d\x2e\x5f-]+/).collect {|x| x.to_i(16)}.pack('C6')
eth_proto = [0x0800].pack('n')
count = 0
loop do
payload = "Simple payload #{count += 1}"
pkt = [eth_daddr, eth_saddr, eth_proto, payload].join.force_encoding('ASCII-8BIT')
stream.inject(pkt)
puts "Sent Packet #{count} (#{pkt.size})"
sleep(2)
end
Sending an ethernet packet
To send an ethernet packet from one host to another, I’ll implement a packet sender and an sniffer based on the great packetfu gem which is domain specific language for packet manipulation, designed for reading and writing packets to an interface or to a libpcap-formatted file.
Each script will be deployed in its own docker container running in the same ethernet network.
Packet sender
This program is designed to send ethernet packets each 2 seconds from the Host A (02:42:ab:aa:bb:02) to the Host B (02:42:ab:aa:bb:01) using the MAC addresses only.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
require 'packetfu'
eth_pkt = PacketFu::EthPacket.new
eth_pkt.eth_saddr = '02:42:ab:aa:bb:02'
eth_pkt.eth_daddr = '02:42:ab:aa:bb:01'
count = 0
loop do
eth_pkt.payload = "Ethernet payload #{count += 1}"
puts '=' * 80
puts eth_pkt.inspect
eth_pkt.to_w('eth0')
sleep(2)
end
Packet sniffer
The packet sniffer will capture the traffic in the network interface (eth0) and will print only the ethernet packets received from the Host A (02:42:ab:aa:bb:02).
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env ruby
require 'packetfu'
puts "Capturing network traffic..."
cap = PacketFu::Capture.new(iface: 'eth0', start: true)
cap.stream.each do |raw_packet|
pkt = PacketFu::Packet.parse(raw_packet)
next if pkt.is_tcp? || pkt.is_udp? || pkt.is_icmp? || pkt.is_arp?
next unless pkt.is_eth? && pkt.eth_header.eth_saddr == '02:42:ab:aa:bb:02'
puts '=' * 80
puts pkt.inspect
end
Docker Compose configuration
The docker compose configuration will be used to run multiple containers in a macvlan network within the l3 mode, which is required to allow send raw ethernet packets between containers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
version: '3'
services:
sniffer:
build: ./sniffer
container_name: sniffer
restart: unless-stopped
mac_address: 02:42:ab:aa:bb:01
networks:
- demo-network
packet_sender:
build: ./packet_sender
container_name: packet_sender
restart: unless-stopped
mac_address: 2:42:ab:aa:bb:02
networks:
- demo-network
networks:
demo-network:
driver: macvlan
driver_opts:
ipvlan_mode: l3
ipam:
driver: default
config:
- subnet: 192.168.16.0/24
gateway: 192.168.16.1
Demo
In the following video you can watch how to run the packet sender and the sniffer in two containers.
Steps to run the demo
- Download the source code to run the packet sender and the sniffer from this file .
- Uncompress the file.
- Execute the following docker commands:
NOTE: You will require docker installed in your machine.
Docker commands
- How to build and run the containers
- How to stop all the containers
- How to list the running containers
- How watch the logs from the standard output in the containers
- How to stop an specific container
- How to list the docker images
- How to remove the docker images
Conclusion
Ruby and docker containers are great tools to simulate network communications and perform packet manipulation. I hope you enjoyed this article and have great day.
Cheers