Link-local Addresses in struct sockaddr_in6 on Linux and OpenBSD

Some time ago I was porting a piece of IPv6-only network software from Linux to OpenBSD. This post is to explain the caveats of using struct sockaddr_in6 and its member sin6_scope_id. It turns out, OpenBSD does not play too well with sin6_scope_id and uses a rather odd method of populating the scope ID to user space.

Linux and sin6_scope

Let’s start with Linux and have a look at the documentation first. struct sockaddr_in6 and its members are described in manual page ipv6(7) on Linux as follows:

sin6_scope_id is an ID depending on the scope of the address. It is new in Linux 2.4. Linux supports it only for link-local addresses, in that case sin6_scope_id contains the interface index.

On Linux struct sockaddr_in6 looks like this:

struct sockaddr_in6 {
   sa_family_t     sin6_family;   /* AF_INET6 */
   in_port_t       sin6_port;     /* port number */
   uint32_t        sin6_flowinfo; /* IPv6 flow information */
   struct in6_addr sin6_addr;     /* IPv6 address */
   uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */

The scope ID is a 32 bit unsigned integer. It populates the interface index which is equivalent to the scope ID.

Example #1

For the first example let’s assume we have an interface with ID 3 and a link-local address of fe80::1. How about we fetch the link-local address of that interface? Even though it is not the best way to do it, for the sake of argument let’s say we use struct sockaddr_in6 to accomplish this.

We ask the kernel to fill the struct for us and this is what we get back in response:

No surprises here, right? Our software can safely rely on the contents of struct sockaddr_in6. Hooray!

OpenBSD and sin6_scope

On OpenBSD the same approach requires more attention from a software-porting developer. Let’s have a look at the documentation first, shall we? We find struct sockaddr_in6 on manual page inet6 of section 4 (drivers). Not quite were I expected it, but still a good place.

struct sockaddr_in6 {
    u_int8_t    sin6_len;
    sa_family_t sin6_family;
    in_port_t   sin6_port;
    u_int32_t   sin6_flowinfo;
    struct in6_addr sin6_addr;
    u_int32_t   sin6_scope_id;

First thing that pops out is that on OpenBSD, struct sockaddr_in6 has an additional member sin6_len. This make its use more versatile. It is irrelevant for the next example, though. However, despite of that the struct looks pretty much the same.

Example #2

Back to our software: Once again we ask the kernel to fill the struct for us and here is what we get back:

Wait. What? Yes, this can give you a headache when you encounter this for the first time. At least it gave me one. The OpenBSD kernel, for some odd reason, does not make use of sin6_scope_id but rather squishes the interface ID in the second group of the link-local IPv6 address. That is just ugly!

Is this a stupid bug, then?

Well, I’d say it is an ugly bug, but it is neither stupid nor doomed to fail. Technically, it is safe to store the scope ID in the address. However, developers porting software need to be aware of that and clean up the link-local address before using it. You would not want to use it in user space or present the address to a regular user without extracting the scope ID first.

Some people pointed out that this behavior does not go well with link-local addresses like fe80:aaaa:bbbb::1. In fact, IANA reserved the space of fe80::/10 for link-scoped unicast addresses, but only fe80::/64 must be used for actual link-local addresses.

Section 2.5.6 of RFC 4291 IP Version 6 Addressing Architecture clearly states this little known constraint to /64:

 Link-Local addresses are for use on a single link.  Link-Local
 addresses have the following format:

 |   10     |
 |  bits    |         54 bits         |          64 bits           |
 |1111111010|           0             |       interface ID         |