Karmatr0n

Programming and Infosec for fun!

Ethernet packets transmission with Ruby

14 Nov 2022

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).

ethernet encapsulation

  • 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.

Ethernet packet transmission

The following piece of code will perform the following operations and that does not require root privileges to run:

  1. Identifies the default network interface.
  2. Builds the byte string that represent an ethernet packet (source and destination mac addresses, type and payload).
  3. 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

  1. Download the source code to run the packet sender and the sniffer from this file .
  2. Uncompress the file.
  3. Execute the following docker commands:
docker compose -f docker-compose.yml up -d
docker ps
docker logs -f <container_id>

NOTE: You will require docker installed in your machine.

Docker commands

  • How to build and run the containers
docker compose -f docker-compose.yml up -d 
  • How to stop all the containers
docker compose -f docker-compose.yml stop
  • How to list the running containers
docker ps
  • How watch the logs from the standard output in the containers
docker logs -f <container_id>
  • How to stop an specific container
docker kill <container_id>
  • How to list the docker images
docker image ls
  • How to remove the docker images
docker rmi <image_id>

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

References