How to write cross-platform packet capture from scratch in 1000 LOC.

Hi, I’m c-bata. This is my first story of Medium.

In this story, I describe how to write cross-platform packet capture from scratch. Most of the code in here will be in C but don’t worry. You can easily apply it to your preferred language.

The source code of my packet capture is available on Github. Currently it supports ARP, ICMP, IP, IPv6, TCP and UDP protocols.

How supports the both of Linux and macOS

The packet capture tool receives and analyzes all packets flowing through the network. Promiscuous mode allows a network device to intercept and read each network packets regardless of the target address. Most of NICs (Network Interface Cards) are support it.

Because software and hardware work in cooperation, the layer being handled is low and there are differences between each systems. libpcap created by the author of tcpdump absorbs differences in UNIX systems(ex: Linux, BSD and macOS). It has also been ported to Windows, and it called WinPcap. But xpcap supports Linux and macOS without having to depends on libpcap for my study purpose.

RAW Socket

When reading ethernet frames on Linux, we need to use RAW Socket.

  1. Open socket descriptor with `PF_PACKET` as a protocol family, `SOCK_RAW` as a socket type and `htons(ETH_P_ALL)` as a protocol.
    int soc = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)))
  2. Retrieving information about network interface from the interface name.
    ioctl(soc, SIOCGIFINDEX, &if_req)
  3. Binding the socket descriptor to the interface.
    bind(soc, (struct sockaddr *) &sa, sizeof(sa))
  4. Get flags of the interface and enable promiscuous mode and set the interface up.
    ioctl(soc, SIOCGIFFLAGS, &if_req);
    if_req.ifr_flags = if_req.ifr_flags|IFF_PROMISC|IFF_UP;
    ioctl(soc, SIOCSIFFLAGS, &if_req);

After that, you reading ethernet frames via `recv(2)` when socket descriptor is ready.
If using `select(2)` to watch socket descriptor, the source code is below:

struct timeval timeout;
fd_set mask;
int width, len, ready;
while (g_gotsig == 0) {
FD_ZERO(&mask);
FD_SET(soc, &mask);
width = doc + 1;
    timeout.tv_sec = 8;
timeout.tv_usec = 0;
ready = select(width, &mask, NULL, NULL, &timeout);
if (ready == -1) {
perror("select");
break;
} else if (ready == 0) {
fprintf(stderr, "select timeout");
break;
}
    if (FD_ISSET(sniffer->fd, &mask)){
if ((len = recv(soc, buffer, >buf_len, 0)) == -1){
perror("recv:");
return -1;
}
}
}

There are many materials and texts which describes how to capture packets in Linux environment using RAW Socket. But BPF(Berkeley Packet Filters) which is the only way to read ethernet frames on BSD environment is not.

Berkeley Packet Filters

We need to use BPF(Berkeley Packet Filter) at BSD systems including macOS.
BPF provides virtual machine to filter packets in kernel space. And BPF devices are used to read data.

BPF devices are exists in `/dev/` directory. You need to find available BPF devices by checking sequentially from bpf0 to bpfxxx.

$ ls /dev/bpf?
/dev/bpf0 /dev/bpf1 /dev/bpf2 /dev/bpf3 /dev/bpf4 /dev/bpf5 /dev/bpf6 /dev/bpf7 /dev/bpf8 /dev/bpf9

Although it exists up to about bpf255 in my macbook, google/gopacke seems to check until bpf99. Maybe it’s enough in almost situations because of the number of NIC (See google/gopacket bsdbpf package).

After found free bpf device, following operations are required to read ethernet frames.

  1. Open a bpf device.
    fd = open(params.device, O_RDWR)
  2. Set buffer length or get buffer length.
    ioctl(fd, BIOCSBLEN, &params.buf_len) : set buffer length
    ioctl(fd, BIOCGBLEN, &params.buf_len) : get buffer length
  3. Bind a BPF device into the interface.
    ioctl(fd, BIOCSETIF, &if_req)
  4. Enable promiscuous mode.
    ioctl(fd, BIOCPROMISC, NULL)

After that you need to use `read(2)` because this is a device file, not a socket descriptor. Returned value of `read(2)` is not Ethernet frame binaries. Ethernet frame is wrapped by a BPF packet.
When parsing the header of BPF, since the data length is on, we will repeat the parsing by finding the position of the next BPF packet by using it.

typedef struct {
int fd;
char device[11];
unsigned int buf_len;
char *buffer;
unsigned int last_read_len;
unsigned int read_bytes_consumed;
} Sniffer;
int
parse_bpf_packets(Sniffer *sniffer, CapturedInfo *info)
{
if (sniffer->read_bytes_consumed + sizeof(sniffer->buffer)
>= sniffer->last_read_len) {
return 0;
}
    info->bpf_hdr = (struct bpf_hdr*)((long)sniffer->buffer +
(long)sniffer->read_bytes_consumed);
info->data = sniffer->buffer + \
(long)sniffer->read_bytes_consumed + \
info->bpf_hdr->bh_hdrlen;
sniffer->read_bytes_consumed += BPF_WORDALIGN(
info->bpf_hdr->bh_hdrlen + info->bpf_hdr->bh_caplen);
return info->bpf_hdr->bh_datalen;
}

After that it’s ok you just parsing ethernet frames and extract `ARP`, `ICMP`, `IP`, `IPv6`, `TCP` and `UDP` protocols.

References

Implementations I referred are below:

  • https://github.com/bpk-t/packet_capture
  • https://github.com/google/gopacket/blob/master/bsdbpf/bsd_bpf_sniffer.go
  • https://www.freebsd.org/cgi/man.cgi?bpf(4)