如何使用Windows C++发送以太网(Ethernet)帧

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.

Scenerio

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 eth0to 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:

  1. Select the required EtherType.
  2. Create the AF_PACKET socket.
  3. Determine the index number of the Ethernet interface to be used.
  4. Construct the destination address.
  5. Send the Ethernet frame.

The following header files are used:

Header Used by
<errno.h> errno
<string.h> memcpystrerrorstrlen
<arpa/inet.h> in_addr_thtons
<net/ethernet.h> ETHER_ADDR_LENETH_P_*
<net/if.h> struct ifreq
<netinet/if_ether.h> struct ether_arp
<netpacket/packet.h> struct sockaddr_ll
<sys/ioctl.h> SIOCGIFINDEXioctl
<sys/socket.h> struct sockaddrstruct iovecstruct msghdrAF_PACKETSOCK_DGRAMsocket,sendtosendmsg

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 Ethernet Type

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 <netinet/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 or SOCK_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));
}

Create the AF_Packet socket

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

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_familysll_addrsll_halensll_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_familysll_addrsll_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 amsghdr 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)

你可能感兴趣的:(C++,windows,socket,struct,NetWork,interface)