Socket Programming - IPv6 Extension Headers
2018-01-112447 words
Recently in my research I had to deal with the IPv6 extension headers. In particular I had to deal with the Hop-By-Hop header. Unfortunately, the process of actually setting the option and getting it on the wire is not trivial, and the documentation is so-so. Because of this I thought I’d give explaining the process a go. But firstly, let’s discuss the header itself.
IPv6 Extension Headers
The two extensions headers that are covered by this method are the Hob-by-Hop options header and the destination options header. These headers look like the diagram below[1]:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Next Header | Hdr Ext Len | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
| |
. .
. Options .
. .
| | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The first field is a 8-bit field that tells us what the next header after the current one is. Following that is another 8-bit field which tells us how long the current field is in units of 8 bytes. It should be noted, however, that this length does not include the first 8 bytes. In other words, if the header is 8 bytes long the length field will hold the value 0, while if the header is 16 bytes long it will hold the value 1.
After these two fields comes the actual options. They follow the below format, and must be a multiple of 8 bytes long. If they’re too short they’re padded to an acceptable length.
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - -
| Option Type | Opt Data Len | Option Data +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - -
The first field tells us the type of the option. The most significant bits of this value determines how the packet is treated, according to the following table:
MSB | Description |
---|---|
00 |
Skip this option and keep processing the header |
01 |
Discard the packet |
10 |
Drop the packet and return an IMCP parameter probelm packet, regardless of whether or not the destination address is multicast |
11 |
Drop the packet and return an ICMP parameter problem packet, but only if the destination address is not multicast |
The third most significant bit tells us if the option can change in en-route (1
) or not (0
). The option length field says how many bytes long the option is.
For the purposes of this guide, that is all you need to know. As you can see from the MSB meaning table, these options aren’t extremely useful if you follow the RFC, as they all either tells the receiver to ignore the option or drop the packet, but they might become useful in the future. Who knows.
Adding the extension headers to packets
And now we’re reached the reason you’re here; how do we actually make our packets that we put on the wire have these options? Unless you want to manually build the whole packet yourself, by hand (which is of course possible), the way to do it is to use struct cmsghdr
. These structs, which hold message control data, are used by struct msghdr
when using sendmsg(...)
[2] to send packets. Since building packets by hand is a bit awkward and messy, this approach is what we’ll do. So let’s get to it!
The very first step is to create a socket, regardless of method, so let’s start with that. I am going to create a raw socket that can then be used to issue ICMP messages. To create this socket we do the following:
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
void fatal(char *msg)
{
fprintf(stderr, "%s\n", msg);
exit(EXIT_FAILURE);
}
...
int sockfd;
if((sockfd = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6)) == -1)
fatal(strerror(errno));
This creates a raw IPv6 socket that is meant to send ICMPv6 messages. If something goes wrong, and error message is printed to stderr
and the program is shut down.
Now that we have our socket we’re ready to start looking at how to send this. As mentioned we’re going to use sendmsg(...)
for this, so let’s start by examining the function prototype for this:
The function first takes a socket, of course, followed by a struct msghdr *
and some flags. The socket is already created, and we’re not going to bother with flags right now, so the value of that is 0. The thing that’s interesting here is the struct msghdr
, as this structure will hold all the information about the packet that the kernel needs to know, like destination. This is also how we’re going to pass the hop-by-hop header! It looks like this[3]:
struct msghdr
{
void *msg_name;
socklen_t msg_namelen;
struct iovec *msg_iov;
size_t msg_iovlen;
void *msg_control;
size_t msg_controllen;
int msg_flags;
};
The first fields, msg_name
and msg_namelen
, are meant for the destination address and the length of it. The next two are the I/O-vectors, or buffers containing input. The msg_control
and msg_controllen
fields are the truly interesting here, since these are where we’ll put our struct cmsghdr
and its length once we’ve built that. But let’s build what we can, while we’re already looking at this. Firstly, let’s set the I/O buffers and flags, since those are trivial.
#define BUFLEN 0xff
....
char sendbuf[BUFLEN];
struct iovec iov[2];
iov[0].iov_base = sendbuf;
iov[0].iov_len = BUFLEN;
struct msghdr msg = {0};
msg.msg_iov = iov;
msg.msg_iovlen = 2;
msg.msg_flags = 0;
This piece of code creates a 256 byte input buffer, stores it in an struct iov
and stores that in the struct msghdr
. The reason the variable iov
is declared as an array with two elements is that we’re gonna need that for the options later. The next step is to set the msg_name
and msg_namelen
fields. To do this we create an struct sockaddr_in6
object and set the value of the IP address using the inet_pton(...)
function. We then set the struct msghdr
fields to point to this variable:
...
struct sockaddr_in6 *dst = malloc(sizeof(struct sockaddr_in6));
inet_pton(AF_INET6, <address>, (char *)&dst->sin6_addr);
...
msg.msg_name = dst;
msg.msg_namelen = sizeof(struct sockaddr_in6);
Exchange the <address>
above with a string containing the destination address of your packets, and you’re good to go. Up to this point we’ve covered most of the preparations that are necessary to make this work. The final step, before actually sending the packet, is to set the extension headers. The process is a bit messy, to say the least, but I think that we’ll be able to pull through.
Setting the extension header uses struct cmsghdr
. The struct is declared as follows[3]:
The cmsg_len
field contains the length of the data plus the length of the header. cmsg_level
holds the originating protocol, e.g. IPPROTO_IPV6
in our case. cmsg_type
holds the message type. Since we’re adding a hop-by-hop header the value here will be IPV6_HOPOPTS
. Now, it may seem like setting these values is all we need to do to get this up and running, but it’s quite a bit trickier than that. This is because the struct stores the data after the struct itself, so we need to initialize it carefully. For what we want to do we need four functions and three macro functions, all defined in RFC3542[4]. Let’s start with the macros:
The first macro, CMSG_LEN(len)
returns the length of the header and data, with any necessary alignment taken into account. This is the value of the cmsg_len
field. The second macro is CMSG_DATA(cmsg)
, which returns a pointer to the actual ancillary data. Finally, the CMSG_SPACE(len)
macro simply returns the size in bytes of the ancillary data and a payload of the specified length. We will use this to allocate space for the header.
The four functions needed are listed below, and since they’re a bit complicated, we’ll go through them one by one:
inet6_opt_init(void *extbuf, socklen_t extlen);
inet6_opt_append(void *extbuf, socklen_t extlen, int offset,
uint8_t type, socklen_t len, uint8_t align, void **databufp);
inet6_opt_set_val(void *databuf, int offset, void *val, socklen_t vallen);
inet6_opt_finish(void *extbuf, socklen_t extlen, offset);
inet6_opt_init(...)
is quite simple. It returns the size of an empty header. If extbuf
points to valid memory it also initializes the header’s length field. This function has to be called before any of the other functions can be called.
inet6_opt_append(...)
has two jobs. If extbuf
is NULL
the function returns how long the header would’ve been if an option was actually appended. If extbuf
is not NULL
, on the other hand, it will append the option, return the length as previously described and return a pointer to the data through the databufp
argument. len
and align
describe the length of the option, as well as the required alignment. len
is specified as the number of octets of data. align
cannot be exceed len
and must have the value 1
, 2
, 4
or 8
, which represent 0, 16, 32 or 64 bits of alignment respectively. offset
should be the returned value of either inet6_opt_init
or a previous call to inet6_opt_append
.
inet6_opt_set_val(...)
takes a pointer to the intended data, i.e. databuf
from the last function, and simply writes whatever is stored in val
to that position. As with the last function, offset
should be the returned value of the last inet6_opt_append
call.
inet6_opt_finish(...)
calculates any final padding needed to make the header a multiple of eight octets long. offset
is, as usual, the result of running inet6_opt_init
or inet6_opt_append
, whichever comes last. When extbuf
is not NULL
the padding isn’t just calculated, but also added to the header.
So now that we know what all these things do, it’s just a matter of applying this. The inet6_opt_...
functions are all specific to GNU, so to ensure that they’re actually available we need to add define _GNU_SOURCE
before including any libraries.
What should also be noted, is that in order for us to know how large our struct cmsghdr
has to be to accommodate the header we must first do a “dryrun” of the setting up. In other words, run all the functions above once with extbuf
set to NULL
, then allocate memory for the struct and then run all functions again with real values. In the end this is what it turns into:
#define _GNU_SOURCE
...
struct cmsghdr *cmsg;
void *hopbuf;
socklen_t hoplen;
void *databuf;
int curlen;
uint8_t val = 0xCC;
// Dryrun to find out size of headers
if((curlen = inet6_opt_init(NULL, 0)) == -1)
fatal(strerror(errno));
if((curlen = inet6_opt_append(NULL, 0, curlen, 0x80, 1, 1, NULL)) == -1)
fatal(strerror(errno));
if((curlen = inet6_opt_finish(NULL, 0, curlen)) == -1)
fatal(strerror(errno));
hoplen = curlen;
// Allocate memory and set known options
cmsg = (struct cmsghdr*)malloc(CMSG_SPACE(hoplen));
cmsg->cmsg_len = CMSG_LEN(hoplen);
cmsg->cmsg_level = IPPROTO_IPV6;
cmsg->cmsg_type = IPV6_HOPOPTS;
hopbuf = CMSG_DATA(cmsg);
// Build actual header
if((curlen = inet6_opt_init(hopbuf, hoplen)) == -1)
fatal(strerror(errno));
if((curlen = inet6_opt_append(hopbuf, hoplen, curlen, 0x80, 1, 1, &databuf)) == -1)
fatal(strerror(errno));
if(inet6_opt_set_val(databuf, 0, &val, sizeof(val)) == -1)
fatal(strerror(errno));
if((curlen = inet6_opt_finish(hopbuf, hoplen, curlen)) == -1)
fatal(strerror(errno));
Note the value of the type
field in the call to inet6_opt_append(...)
above: 0x80
. This value sets the type
field of the options header, and since 0x80
is 1000 0000
in binary, this tells the IPv6 stack to return an ICMP Parameter Problem
message. For the purpose of this post, there isn’t really a specific reason for choosing this - any value with at least one bit set in the upper nibble works.
The final step before we’re ready to send the message is to add the option definitions we just set up to the message itself.
iov[1].iov_base = hopbuf;
iov[1].iov_len = sizeof(hopbuf);
msg.msg_control = cmsg;
msg.msg_controllen = CMSG_SPACE(hoplen);
And that’s it, we can now send the message using sendmsg(...)
. Putting it all together, we get the following:
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUFLEN 0xff
void fatal(char *msg)
{
fprintf(stderr, "%s\n", msg);
exit(EXIT_FAILURE);
}
int main(int argc, char *argv[]) {
// Initialize the socket
int sockfd;
if((sockfd = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6)) == -1)
fatal(strerror(errno));
// Get destination address
struct sockaddr_in6 *dst = malloc(sizeof(struct sockaddr_in6));
inet_pton(AF_INET6, <address>, (char *)&dst->sin6_addr);
char sendbuf[BUFLEN];
struct iovec iov[2];
iov[0].iov_base = sendbuf;
iov[0].iov_len = BUFLEN;
struct msghdr msg = {0};
msg.msg_iov = iov;
msg.msg_iovlen = 2;
msg.msg_flags = 0;
msg.msg_name = dst;
msg.msg_namelen = sizeof(struct sockaddr_in6);
struct cmsghdr *cmsg;
void *hopbuf;
socklen_t hoplen;
void *databuf;
int curlen;
uint8_t val = 0xCC;
// Dryrun to find out size of headers
if((curlen = inet6_opt_init(NULL, 0)) == -1)
fatal(strerror(errno));
if((curlen = inet6_opt_append(NULL, 0, curlen, 0x80, 1, 1, NULL)) == -1)
fatal(strerror(errno));
if((curlen = inet6_opt_finish(NULL, 0, curlen)) == -1)
fatal(strerror(errno));
hoplen = curlen;
// Allocate memory and set known options
cmsg = (struct cmsghdr*)malloc(CMSG_SPACE(hoplen));
cmsg->cmsg_len = CMSG_LEN(hoplen);
cmsg->cmsg_level = IPPROTO_IPV6;
cmsg->cmsg_type = IPV6_HOPOPTS;
hopbuf = CMSG_DATA(cmsg);
// Build actual header
if((curlen = inet6_opt_init(hopbuf, hoplen)) == -1)
fatal(strerror(errno));
if((curlen = inet6_opt_append(hopbuf, hoplen, curlen, 0x80, 1, 1, &databuf)) == -1)
fatal(strerror(errno));
if(inet6_opt_set_val(databuf, 0, &val, sizeof(val)) == -1)
fatal(strerror(errno));
if((curlen = inet6_opt_finish(hopbuf, hoplen, curlen)) == -1)
fatal(strerror(errno));
iov[1].iov_base = hopbuf;
iov[1].iov_len = sizeof(hopbuf);
msg.msg_control = cmsg;
msg.msg_controllen = CMSG_SPACE(hoplen);
if(sendmsg(sockfd, &msg, 0) == -1)
fatal(strerror(errno));
return 0;
}
When compiled and executed, this will send a single ICMPv6 message with the Hop-by-Hop option header set. The figure below shows a screenshot of Wireshark examining such a message, headed for localhost (::1
).
Conclusion
So, there we go, that’s all there is to it. Setting up IPv6 extension headers can be a bit long-winded, and like I said in the beginning it’s not exactly intuitive. But once you wrap your head around the control structures involved, the “redundant” procedure, and the IPv6 protocol itself, it’s not too bad.
Even though this post has talked exclusively about the Hop-By-Hop header, the code above should be fairly easy to adapt to other extension headers, or modify to be more generic. If I did a lot of IPv6 network programming and I often needed to set various extension headers, I’d put a somewhat modified version of the code above in a local networking programming library that I’d link into my projects. That way, I’d only have to deal with it once.
Anyway, I hope that you found this somewhat useful, and that it saves you some headache trying to figure this out from lacklustre documentation.
Happy hacking!
References
[1] D. S. E. Deering and B. Hinden, “Internet Protocol, Version 6 (IPv6) Specification.” in Request for comments. RFC 8200; RFC Editor, Jul. 2017. doi: 10.17487/RFC8200.
[2] SEND(2) linux programmer’s manual, 5.10 ed. 2020.
[3] W. R. Stevens, UNIX network programming vol. 1 networking apis : Sockets and xti. Upper Saddle River, N.J.: Prentice Hall PTR, 1998.
[4] T. Jinmei, W. R. Stevens, and E. Nordmark, “Advanced Sockets Application Program Interface (API) for IPv6.” in Request for comments. RFC 3542; RFC Editor, Jun. 2003. doi: 10.17487/RFC3542.