Send an arbitrary Ethernet frame using an AF_PACKET
socket in C
Tested on |
Debian (Lenny) |
Ubuntu (Lucid, Trusty) |
Objective
To send an arbitrary Ethernet frame using an AF_PACKET
socket
Background
Ethernet is a link layer protocol. Most networking programs interact with the network stack at the transport layer or above, so have no need to deal with Ethernet frames directly, but there are some circumstances where interaction at a lower level may be necessary. These include:
- implementation of Ethernet-based protocols that are not built in to the network stack, and
- production of malformed or otherwise non-standard frames for testing purposes.
Scenario
Suppose that you wish to send an ARP request for the IP address 192.168.0.83. The request is to be sent from interface eth0
to the broadcast MAC adddress.
(ARP is the Address Resolution Protocol. It is used when a host needs to send a datagram to a given IP address, but does not know which MAC address corresponds to that IP address.)
Method
Overview
The method described here has five steps:
- Select the required EtherType.
- Create the
AF_PACKET
socket. - Determine the index number of the Ethernet interface to be used.
- Construct the destination address.
- Send the Ethernet frame.
The following header files are used:
Header | Used by |
---|---|
<errno.h> |
errno |
<string.h> |
memcpy , strerror , strlen
|
<arpa/inet.h> |
in_addr_t , htons
|
<net/ethernet.h> |
ETHER_ADDR_LEN , ETH_P_*
|
<net/if.h> |
struct ifreq |
<netinet/if_ether.h> |
struct ether_arp |
<netpacket/packet.h> |
struct sockaddr_ll |
<sys/ioctl.h> |
SIOCGIFINDEX , ioctl
|
<sys/socket.h> |
struct sockaddr , struct iovec , struct msghdr , AF_PACKET , SOCK_DGRAM , socket , sendto , sendmsg
|
AF_PACKET
sockets are specific to Linux. Programs that make use of them need elevated privileges in order to run.
Setting SO_BROADCAST
does not appear to be necessary when sending broadcast frames using an AF_PACKET
socket. Some programs do so anyway, which is unlikely to be harmful, and could be considered a worthwhile hedge against any future change in behaviour.
Select the required EtherType
The EtherType of an Ethernet frame specifies the type of payload that it contains. There are several sources from which EtherTypes can be obtained:
- The header file
<linux/if_ether.h>
provides constants for most commonly-used EtherTypes. Examples includeETH_P_IP
for the Internet Protocol (0x8000
),ETH_P_ARP
for the Address Resolution Protocol (0x0806
) andETH_P_8021Q
for IEEE 802.1Q VLAN tags (0x8100
). - The IEEE maintains the definitive list of registered EtherTypes.
- A semi-official list is maintained by IANA.
The wildcard value ETH_P_ALL
allows any EtherType to be received without using multiple sockets. This includes EtherTypes that are handled by the kernel, such as IP and ARP.
If you need an EtherType for experimental or private use then the values 0x88b5
and 0x88b6
have been reserved for that purpose.
Create the AF_PACKET socket
The socket that will be used to send the Ethernet frame should be created using the socket
function. This takes three arguments:
- the domain (
AF_PACKET
for a packet socket); - the socket type (
SOCK_DGRAM
if you want the Ethernet header to be constructed for you orSOCK_RAW
if you want to construct it yourself); and - the protocol (equal to the Ethertype chosen above, converted to network byte order), which is used for filtering inbound packets.
In this instance the socket will be used for sending (and presumably also receiving) ARP requests, therefore the third argument should be set to htons(ETH_P_ARP)
(or equivalently, htons(0x0806)
). There is no need to construct a custom Ethernet header so the second argument should be set to SOCK_DGRAM
:
int fd=socket(AF_PACKET,SOCK_DGRAM,htons(ETH_P_ARP)); if (fd==-1) { die("%s",strerror(errno)); }
Determine the index number of the Ethernet interface to be used
Network interfaces are usually identified by name in user-facing contexts, but for some low-level APIs like the one used here a number is used instead. You can obtain the index from the name by means of the ioctl
command SIOCGIFINDEX
:
struct ifreq ifr; size_t if_name_len=strlen(if_name); if (if_name_len<sizeof(ifr.ifr_name)) { memcpy(ifr.ifr_name,if_name,if_name_len); ifr.ifr_name[if_name_len]=0; } else { die("interface name is too long"); } if (ioctl(fd,SIOCGIFINDEX,&ifr)==-1) { die("%s",strerror(errno)); } int ifindex=ifr.ifr_ifindex;
For further details of this method see the microHOWTO Get the index number of a Linux network interface in C using SIOCGIFINDEX
.
Construct the destination address
To send a frame using an AF_PACKET
socket its destination must be given in the form of a sockaddr_ll
structure. The fields that you need to specify are sll_family
, sll_addr
, sll_halen
, sll_ifindex
and sll_protocol
. The remainder should be zeroed:
const unsigned char ether_broadcast_addr[]= {0xff,0xff,0xff,0xff,0xff,0xff}; struct sockaddr_ll addr={0}; addr.sll_family=AF_PACKET; addr.sll_ifindex=ifindex; addr.sll_halen=ETHER_ADDR_LEN; addr.sll_protocol=htons(ETH_P_ARP); memcpy(addr.sll_addr,ether_broadcast_addr,ETHER_ADDR_LEN);
(At the time of writing, the manpage packet(7) stated that only sll_family
, sll_addr
, sll_halen
and sll_ifindex
need be provided when sending. This is incorrect. The EtherType specified when opening the socket is used for filtering inbound packets but not for constructing outbound ones.)
Send the Ethernet frame
Frames can in principle be sent using any function that is capable of writing to a file descriptor, however if you have opted for the link-layer header to be constructed automatically then it will be necessary to use either sendto
or sendmsg
so that a destination address can be specified. Of these sendmsg
is the more flexible option, but at the cost of a significantly more complex interface. Details of each function are given below.
Regardless of which function you choose, each function call will result in a separate datagram being sent. For this reason you must either compose each datagram payload as a single, contiguous block of memory, or make use of the scatter/gather capability provided by sendmsg
.
In this particular scenario the payload to be sent is an ARP request. For completeness, here is an example of how such a payload might be constructed:
struct ether_arp req; req.arp_hrd=htons(ARPHRD_ETHER); req.arp_pro=htons(ETH_P_IP); req.arp_hln=ETHER_ADDR_LEN; req.arp_pln=sizeof(in_addr_t); req.arp_op=htons(ARPOP_REQUEST); memset(&req.arp_tha,0,sizeof(req.arp_tha));
You will need to set req.arp_tpa
to contain the IP address (in network byte order) for which you want to find the corresponding MAC
address. For example, starting from a string in dotted quad format:
const char* target_ip_string="192.168.0.83"; struct in_addr target_ip_addr={0}; if (!inet_aton(target_ip_string,&target_ip_addr)) { die("%s is not a valid IP address",target_ip_string); } memcpy(&req.arp_tpa,&target_ip_addr.s_addr,sizeof(req.arp_tpa));
You will also need to set source_ip_addr
and source_hw_addr
to contain the IP and MAC addresses of the interface from which the request will be sent (in network byte order). See the microHOWTOs Get the IP address of a network interface in C using SIOCGIFADDR and Get the MAC address of an Ethernet interface in C using SIOCGIFHWADDR for details of how to obtain these given the interface name.
Send the frame (using sendto)
To call sendto
you must supply the content of the frame and the remote address to which it should be sent:
if (sendto(fd,&req,sizeof(req),0,(struct sockaddr*)&addr,sizeof(addr))==-1) { die("%s",strerror(errno)); }
The fourth argument is for specifying flags which modify the behaviour of sendto
, none of which are needed in this example.
The value returned by sendto
is the number of bytes sent, or -1 if there was an error. AF_PACKET
frames are sent atomically, so unlike when writing to a TCP socket there is no need to wrap the function call in a loop to handle partially-sent data.
Send the frame (using sendmsg)
To call sendmsg
, in addition to the datagram content and remote address you must also construct an
iovec
array and a msghdr
structure:
struct iovec iov[1]; iov[0].iov_base=&req; iov[0].iov_len=sizeof(req); struct msghdr message; message.msg_name=&addr; message.msg_namelen=sizeof(addr); message.msg_iov=iov; message.msg_iovlen=1; message.msg_control=0; message.msg_controllen=0; if (sendmsg(fd,&message,0)==-1) { die("%s",strerror(errno)); }
The purpose of the iovec
array is to provide a scatter/gather capability so that the datagram payload need not be stored in a contiguous region of memory. In this example the entire payload is stored in a single buffer, therefore only one array element is needed.
The msghdr
structure exists to bring the number of arguments to recvmsg
and sendmsg
down to a managable number. On entry to sendmsg
it specifies where the destination address, the datagram payload and any ancillary data are stored. In this example no ancillary data has been provided.
If you wish to pass any flags into sendmsg
then this cannot be done using msg_flags
, which is ignored on entry. Instead you must pass them using the third argument to sendmsg
(which is zero in this example).
Alternatives
Using libpcap
See: | Send an arbitrary Ethernet frame using libpcap |
libpcap is a cross-platform library for capturing traffic from network interfaces. It also has the ability to send, so provides broadly the same functionality as a packet socket (and on Linux, is implemented using a packet socket).
The main advantage of using libpcap is that it abstracts away differences between the operating systems that it supports, thereby allowing relatively portable code to be written. This involves some loss of functionality, and that may make libpcap unsuitable for use in some circumstances, but otherwise it is recommended in preference to AF_PACKET
sockets on the grounds of portability.
Using a raw socket
See: | Send an arbitrary IPv4 datagram using a raw socket in C |
Raw sockets differ from packet sockets in that they operate at the network layer as opposed to the link layer. For this reason they are limited to network protocols for which raw socket support has been explicitly built into the network stack, but they also have a number of advantages which result from operating at a higher level of abstraction:
- You can write code that will work with any suitable type of network interface.
- Routing and link-layer address resolution are handled for you.
- The network layer header is constructed for you unless you request otherwise.
- The raw socket API has been partially standardised by POSIX, whereas
AF_PACKET
sockets are specific to Linux.
For these reasons, use of a raw socket is recommended unless you specifically need the extra functionality provided by working at the link layer.
Further reading
- packet(7) (Linux manpage)