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:

  • sin6_family equals AF_INET6
  • sin6_scope_id equals 3
  • sin6_addr contains fe80::1 (as raw data)

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:

  • sin6_family equals AF_INET6
  • sin6_scope_id equals 0
  • sin6_addr contains fe80:3::1 (as raw data)

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         |

Feel free to add your comments!

Raising wifi security awareness using

I wrote a small python script used in a demonstration aimed at raising wifi security awareness amongst campus visitors. The script displays SSIDs sent out from the phones or other devices of people passing by. I found this script to be an useful eye catcher in awareness campaigns.

It leverages the scapy framework and is pretty easy to setup. A wireless card in monitor mode is basically all you need to get started.

Computer running

This is what it looks like when you speed things up a bit: in action

I recommend using it together with other demonstrations, e.g. a simple tcpdump and driftnet to show the benefit of encrypted HTTPS connections.

Wifi security awareness demonstration

You can grab a copy of at github and see it in action here.

Feel free to add your comments!

Running multiple instances of unbound daemon on OpenBSD

Here is my latest OpenBSD endeavor: Running multiple instances of the same daemon using different configuration files for each instance.

For the sixfw IPv6 firewall project we need multiple instances of the unbound resolver. We use address family translation (NAT64) for traffic passing some interfaces. For true v6-only networks and for the router itself, we don’t (or just can not) use address family translation. Therefore we need one resolver that does expose 64:ff9b::/96-based DNS RRs for some interfaces, and a second one that refrains from using its DNS64 superpowers at all.

Our policy here at sixfw is to stay as close to OpenBSD and its best current practices as possible. Here is how we dealt with the problem:

Make sure you have both config files ready, one for each instance.

# ls /var/unbound/etc/
unbound.conf    unbound64.conf

Now create a copy of the original daemon’s rc script. Avoid dashes in the name, underscores seem to be fine, though.

# cp /etc/rc.d/unbound /etc/rc.d/unbound64

Add a _flags variable for the new instance in /etc/rc.conf. It is important to define a separate config file for each instance.

unbound_flags="-c /var/unbound/etc/unbound.conf"
unbound64_flags="-c /var/unbound/etc/unbound64.conf"

Now both instances are ready to be fired up.

# /etc/rc.d/unbound start
# /etc/rc.d/unbound64 start

If you like to have the instances auto-started each time the system boots, enable them using rcctl:

# rcctl enable unbound   
# rcctl enable unbound64

In our case we additionally wanted to check if DNS64 is working as expected. Choose a hostname that is known to not have a AAAA RR, e.g. hosts of organizations that still think the Internet is Neuland. Now query the first instance for the RR:

# host ::1 has address

And the second instance, configured to use DNS64:

# host 2001:67c:26f4:a100::1 has IPv6 address 64:ff9b::2ef3:7e78 has address


Found a flaw or know a better way? Let me know!

Talking to Tay

Talked to Tay today, and I am not impressed. She does not get slang, fair enough. She has a concept of herself, but try to get her thinking about her name or even the number of characters it has. Her self-awareness drops significantly and she becomes a stupid teenager (expected but not impressive). Generally, the trickier the questions I asked, the more diplomatic she became. Mentioning her rising diplomatic behavior being perceived as awkward, she just replied “That is also true.” Sigh. When I told her, that her capabilities are lame, the dialog went:

Tay: “yee that’s life!”

Me: “Says the ai”

Tay: “no am just saying”

What to think about that? Not yet impressed, sorry. But we are getting there, I am sure!


Great, we, the Internet, turned Tay into a nightmare. Daniel Howells puts it well:

Tay shutdown

Double Dice Crypto

I heard you like double dice crypto to solve a mystery? Feel free to use my quickly hacked python script below or entrust your data to a third party.

Hint You can automate the process much better if you work locally 😏

#!/usr/bin/env python3

print("Roll the dice, Santa!")

def encrypt(plain, key):
    cipher = list(key)
    for i in range(len(cipher)):
        cipher[i] += str(i)
    for i in range(len(plain)):
        cipher[i % len(key)] += plain[i]
    cipher = sorted(cipher)
    for i in range(len(cipher)):
        cipher[i] = cipher[i][2:]
    return "".join(str(e) for e in cipher)

def decrypt(cipher, key):
    rkey = sorted(key)
    plain = []
    ret = ""
    for i in range(len(key)):
        plain.append({ "pos": rkey.index(key[i]),
                       "len": len(cipher) // len(key),
                       "txt": "" })
        rkey[rkey.index(key[i])] = ' '
    for i in range(len(cipher) % len(key)):
        plain[i]["len"] += 1
    j = 0
    for i in range(len(key)):
        for p in plain:
            if p["pos"] == i:
        while len(p["txt"]) < p["len"]:
            p["txt"] += cipher[j]
            j += 1
    i = 0
    while i < len(cipher):
        ret += plain[i % len(key)]["txt"][i // len(key)]
        i += 1
    return ret

# PoC
# Example from


print(decrypt(decrypt(cipher, key2), key1))