yayi C++, python, image processing, hacking, etc

IPv6 and home servers: the varying prefix dilemma

Introduction

IPv6 has been around for a long time, and the transition from IPv4 to IPv6, even though still on-going, has accelerated those last years. If you have eg. a DSL connection at home, it is highly likely that you don't have a public IPv4 address any more and the best you can get is a Dual-Stack Lite: the IPv4 stack stays within your ISP and your home network, but the packets you are sending to the Internet are proxied through your Internet Service Provider (ISP) facilities. In that case, nobody can access your home network from the Internet using IPv4 only as the only public IPv4 address that is visible is the one of your ISP's proxy.

Enters IPv6 which comes with the promise that there are sufficient addresses in the global (public) namespace that you can easily and freely get one. In turn, once you have a publicly visible IPv6 address, well you can access your home network from anywhere in the world, and this is what we want.

This was at least the tale I was thinking of: IPv6 address is given and fixed, then you configure your network once (your server(s) and your DNS entries), and you are good until you change ISP... until I started switching myself to IPv6. The truth is, ISPs can implement IPv6 the way they want, and some mimic a similar behavior to what we had with IPv4 such as the dynamically attributed address. In that case, part of your router/home network address can change over time when your ISP implements varying IPv6 prefixes, and the frequency at of those changes is up to your ISP, ie. external factors.

First thought is "this should not be a big deal" as we had that for a long time with IPv4. This is indeed True, but it seems that in the IPv6 world, it was assumed that we don't need such complexities and everyone would have a fixed IPv6 address, simply because address exhaustion is decades away.

But the varying prefix is a how it is for me and I had to cope for it. It unfolds a chain of problems, from router implementation, to DNS registrar/owner, to IPv6 privacy rules, and this article will try to cover the elements involved in this stack to get a successful home server running and accessible.

We will be focusing on a solution using a router that implements Tomato firmware, but the article is not bound to it and the implementation should be easily adaptable to any other router letting you run scripts. In any case, this is an implementation detail and the other part of this article should be useful to people interested in IPv6.

Breaking down the problem

Let's recap all the chain of events and actions that happens when a client wants to access a server by its name (say myserver.example.org). This is basically done in 2 steps.

The first is about address resolution via a DNS query, in this case the query should reply an IPv6 address:

clientDNSregistrarDNSqueryIPv6address

The second is about sending actual network packets to the target machine:

homeserver,,).'.clienthomerouterinternet'.

Making IPv6 work for your home server implies working on almost all the parts above (except of course the Internet, supposed to be IPv6 ready and compliant).

Varying IPv6 prefix

What is a varying IPv6 prefix? An IPv6 address is (in a very high view) made of 3 parts:

  • the prefix
  • the subnet
  • the interface

Taking the example of the address 2001:0db8:0000:85a3:0000:0000:ac1f:8001 with a prefix length of 56 bits, the address would be like this

prefix56bitssubnetinterface64bits2001:0db8:0000:85a3:0000:0000:ac1f:8001

The length of the prefix is usually given by your ISP, the interface on the other hand is always 64 bits long, and the subnet length is the remainder of the interface and the prefix such that the total bit length is 128. We consider only unicast addresses. Concerning the subnet: until now I don't know if its value follows any particular rule and the a3 part above seems to be completely arbitrary to the router to me. We'll discuss this later.

A varying prefix means that the prefix will change over time and you cannot really do anything about it but adapting your network to that changing parameter. In particular:

  1. our home router has an embedded firewall and we want to keep this safety element. In case a change of prefix happens, we want to modify the firewall rules such that it lets the packets only for the target home server through. Since the unicast address of home server has changed (the change of prefix), the firewall rules should be rewritten accordingly,
  2. the DNS entry myserver.example.org should now point to another IPv6 address once a change of prefix has been detected. This seems to be simple since we had this issue already with dyndns and IPv4, with the exception being that IPv6 ecosystem is not really ready for changing IPv6 addresses.

Proposed solution

We don't want to sacrifice the firewall, even if we have billions of possible addresses for the machines that are behind our router and this induces a good level of privacy. There is however a difference between relying on a mechanism that should work most of the time, and explicitly allowing one specific traffic. We chose the latter: we keep control on what we do and we'll see this does not add complexity to our overall setup.

We will configure our router such that it detects a change of prefix, and configures the firewall and the DNS entries when this happens. We first need to configure our network, then configure the DNS, and finally automate the prefix changes actions.

An alternative solution would be to automate the prefix change actions on the server itself (the machine hosting the service we want to expose on the Internet). However this makes it hard to control and rewrite the firewall rules accordingly. There are some routers doing that based on eg. the MAC address of the target server though, this is not covered here.

So... what do we need to do now?

First we need to register to a DNS service/registrar which 1) is compatible with IPv6 and 2) we can update remotely.

The second step is to have a predictable IPv6 interface address for the server: the router can assign IPv6 interface addresses but this makes everything complicated and unneeded. IPv6 comes with various solutions, and usually the default one is making such that the interface address can change over time or as a function of the prefix.

Finally, after detecting a change on the IPv6 prefix, we need to propagate the new global address of our server to our DNS registrar and rewrite the firewall rules.

DynDNS for IPv6 with API access

The current domain is hosted on OVH (a French company). Unfortunately as of today my provider OVH does not offer the ability to update a IPv6 DNS entry (AAAA record) from an API call, at least not on the tier I am subscribed to.

I wanted something as cheap as possible (possibly free and without ads) offering a good level of service. After a quick search, I've found dynv6.org. The service offers much more interesting service and API that I would initially have excepted.

Register to dynv6 and then execute the following steps:

  1. create a new DNS zone my-zone
  2. then add a new record in that zone: this record will be in the form myserver.my-zone.dynv6.net. However you have to create this record by adding the MAC address of the server you want on the Internet (on the interface that will be connected to the router). This is the convenience dynv6 is providing us with.
  3. finally (optional) create a CNAME pointing to myserver.my-zone.dynv6.net in the DNS zone you own.

What will happen afterwards is that our router will update the IPv6 prefix and only that information of our DNS zone and all records within will be updated as well. The trick with the MAC address is that it makes the IPv6 address of the router predictable without the burden of setting up DHCPv6 or manual assignment of the addresses (see Wikipedia § Address formats).

Instructions are shown for a specific zone to remotely update the configuration: we will .

Server configuration

We now need to configure our server such that it generates the same type of IPv6 addresses as the one we just configured for dynv6. I will not enter into the details of address attributions in IPv6, but "roughly speaking", you have 2 mechanisms: stateless address autoconfiguration (SLAAC) and DHCPv6. DHCPv6 requires the configuration of another service/computer (usually the router that contains a DHCP server) but does not provide any added value in the case of IPv6. We focus on SLAAC in the following.

Let's also not forget that our software stack may need a slight update to accept IPv6 packets.

Fixed IPv6 interface section

Even for the case of SLAAC, several modes exists. They differ mostly on privacy issues: in short MAC addresses can be used to track your devices. In our case however, we have a server that needs to be reached and its address needs to be publicly known: privacy does not apply much in our case and it is ok to derive the IPv6 interface section from the MAC address.

This configuration is operating system dependant.

Windows

I don't run servers on Windows so I can't talk about that OS.

Ubuntu

If you have Ubuntu (available on Raspberry Pi's as well!) you can skip this part: the default configuration I have played with so far produces stable interface identifiers and the SLAAC configuration yields the desired results. ip address show eth0 will give you what you need.

Raspbian

On Raspbian (Raspberry Pi), SLAAC by default uses enforced privacy, which can be changed easily by updating the file /etc/dhcpcd.conf (the DHCP configuration...) like this:

# Generate SLAAC address using the Hardware Address of the interface
slaac hwaddr        # <- uncommented line

# OR generate Stable Private IPv6 Addresses based from the DUID
# slaac private     # <- commented line

You can see if this worked by comparing the IPv6 address of that interface to its MAC address: if the last 4 last letters are the same if it likely that it worked (restart your computer if this is not the case).

Configuring the service

After establishing the IPv6 stack, some setup would certainly be needed to bring your software stack (your actual service) to IPv6. This is usually much simpler than what we have above. In the case of NGinx for instance, this is done by adding a listening line specific to IPv6, straightforward like this:

server {
    listen                  80
    listen                  [::]:80; # <- new line here
    server_name             myserver.my-zone.org;

    return 301 https://$host$request_uri;
}

Save, restart the service, done.

Opening the IPv6 firewall on a Tomato router

We quickly describe how to open the firewall on a Tomato router manually, this is only for testing/debugging purposes and the settings described here will be full automated by the script that comes later in the article. The version of Tomato used below is the excellent Fresh Tomato, but other firmware should be similar. Some routers do not let you specify the IPv6 address (like for a Fritzbox) and they instead rely on the MAC address of the device to route to. We do not deal with those here.

To create the right firewall rules for your device, run the following steps:

  1. open your router admin interface
  2. select "port forwarding" on the left menu
  3. go to "basic IPv6"
  4. enter the full IPv6 address of your internal server, the desired ports, and describe it if necessary. To print the address on Ubuntu: ip address show eth0.
  5. click save: the configuration is now in the nvram and is persisted when the router is rebooted

Tomato router

This setting is under "port forwarding" on my router, but this is technically misleading: we are not mapping an exposed port to an internal address, we are letting packets for specific destination and ports through.

Everything is now ready on your server and home network to receive traffic from the Internet. Let's play with some clients.

Configuring clients

Before going any further into automation, I would recommend configuring a client first: not only we will learn about the client side (and gain debugging skills), but this also would let us check the above configuration.

Recall that all we did so far was to have a DNS entry, fixed the IPv6 address of our server and opened a firewall rule on our router. If our IPv6 address does not change over time, this is just enough and we don't need any automation. Usually even for varying IPv6 prefix, you have several hours before a change occurs, which is plenty of time to check the configuration.

One easy and ubiquitous client is a simple smartphone with Internet access: the network is (usually) totally different when Internet is used with 4/5/XG (remember to disable your WiFi access) and this type of access is illustrative on how other people would access your server.

We can set those up all manually and bring automation later, but it is important to check we are on the right track and we understand what we do. Another relevant property this would bring us is to debug client issues while accessing our server.

From the diagram above, we are now on the /client/ side.

Using an Android phone

Surprisingly enough, the default settings did not enable IPv6 on my Android phone. Here is how to enable it:

  1. open the settings, select "Network and Internet"
  2. check the names of the access points and then the access point itself
  3. open the settings and enable IPv6 for the APNs

access point settings

Using an iPhone

I don't have an iPhone so ...

Other networks

Remember that in order to access IPv6 addresses, the client should have IPv6 enabled. In particular, if your client is a friend with a DSL connection, the router of that DSL connection should have IPv6 enabled in order for this to work.

Tools for debugging

For debugging various issues, we have to check the settings of 3 components:

  • the DNS record for your server
  • the firewall configuration, letting traffic through
  • and the server

For checking the DNS on my phone, I used AndroDNS (on f-Droid): make sure to ask for AAAA records. For checking the DNS from a terminal, use

dig AAAA myserver.my-zone.dynv6.net

or

nslookup -query=AAAA myserver.my-zone.dynv6.net

You may also replace myserver.my-zone.dynv6.net by the CNAME record pointing to it. Check that the returned address is the one of your server (ip address show eth0, see above).

Optionally, you can start a simple HTTP service on your server with python.

python -m http.server

This is handy if you want to untangle the network issues from your eg. NGinx configuration, in particular if you have virtual hosts on your web server.

First check that this works from your internal network over IPv6 by opening the browser with http://[IPv6-address]:8080 (for the test web server, fix the port for the real server). The brackets here are important as they indicate an IPv6 address. That IPv6 address should be the same as the one returned by the nslookup / dig commands above.

The final step is to check the firewall settings: if the above steps are working fine and the firewall is configured properly, then you should be able to open the same address from your phone. Note however that if you stick to the test web server started above, you should open the port 8080 on your firewall in addition to eg. 80 and 443.

If something happens, use

  1. AndroDNS for any DNS issue: this should work irrelevant from your IPv4/IPv6 configuration on your phone
  2. if you receive a timeout, it is likely that the firewall is not letting the traffic through
  3. if you see any activity on the web server, it means the packets are going through and we are good.

FreshTomato router automation for IPv6 prefix change

After this rather long introduction to IPv6 and the manual configuration, some elements for automating the IPv6 prefix change should seem clearer now, but for completion, an IPv6 prefix change should trigger the following actions:

  1. server's IPv6 prefix change: this is automatically done as this is the purpose of the router. Note that the interface part of the server IPv6 address does not change
  2. DNS update: this will be an API call to specific dynDNS endpoints
  3. firewall rules updates

Point 2/ above can be done directly on our server, but performing 3/ is challenging when done outside the router. As mentioned earlier, the proposed solution is to have a cron job doing the necessary task directly on the router.

FreshTomato

Our router runs an operating system, FreshTomato, that provides us with the tools for the automation above to work. FreshTomato is a fork of Tomato (and later Shibby Tomato). It is based around an old Linux kernel (old because some drivers' source code are not available and cannot be compiled with newer versions), which means that the tools we are used to are also available. Welcome then a terminal, remote ssh access, sh, cuRL, vi (!), USB mounts, file systems, init facilities ... plus some other tools specific to the router. Even an old version of python2 is available from some unmaintained package repository!

In order to use FreshTomato, you have to flash the router firmware with a compatible version. All routers are not compatible with FreshTomato (list of supported routers on the website). Flashing the firmware may be a delicate operation, and more or less easy to perform depending on the original firmware of the router. I would suggest watching videos and resources on the net before jumping into the void.

Note that other OSes exists such as DD-WRT or OpenWRT. My router though does not work with OpenWRT (I bricked it several times) and have not checked DD-WRT.

For the rest of the article, it would be handy to enable the ssh access on the router.

Update of the DynDNSv6 record

This is straightforward: you need the API token (see here) together with the DNS zone and the new IPv6 prefix, and then call a simple cURL command like this:

curl -fsS "https://dynv6.com/api/update?zone=$zone&ipv6prefix=$CURRENT&token=$token"

No need for any other tool like ddclient, DDNS Updater 2 or anything hard to install that would in any case not be available on our router.

The zone is your DNS zone as configured on dynv6 (myserver.my-zone.dynv6.net in our examples). The ipv6prefix has the form 2001:16b8:4655:3bff::/64 and the token is the secret token. The command to run for your specific zone is available on the dynv6 website when you are logged in.

Recall that in the case we have several records in the zone, updating the prefix once will update all the records as we configured our records (and servers) to use an IPv6 interface derived from the MAC address, and created the DNS records from those MAC addresses.

Script for Tomato routers

Several scripts are available online, but we need to adapt those to our specific needs:

  • updating the prefix of the DNS zone only
  • caching values as much as possible, hitting the dynv6 service only when needed
  • retrieving various information directly from the router itself
  • updating the firewall rules

The script can be found at the bottom of the article. Instructions for "deploying" this script can be found on this section.

Most of the sections of the script can be used on other OSes; the retrieval of the IPv6 prefix, firewall and the NVRAM updates are however specific to Tomato/FreshTomato. For those who do not know: NVRAM is the Non Volatile RAM of the router; a very tiny storage that persists over reboots. This usually holds the configuration of the router.

Retrieving the prefix

This is done by the two commands nvram get ipv6_prefix and nvram get ipv6_prefix_length. Nothing magic here: I just used nvram show | grep ipv6 on the router.

Updating the firewall rules

This is the tricky part, and we will take a complete different approach as the ones you may find on the net: we will save the configuration but as usual, a bit of explanation on the design choice. It would be possible to update the ip6tables directly, but

  1. those are not persisted after a reboot, it would mean we have to do some manual work ourselves
  2. it is not clear how those rules affect the GUI of the router. As a user, I expect that any change on the firewall rules can be seen on the corresponding GUI entry. It seems to me this is not the case and this makes life really hard when we need to troubleshoot the accesses.

On the other hand, all the information entered in the GUI is saved to the NVRAM, so it should be possible to tweak the corresponding GUI entry from some information in the NVRAM.

After some digging and a bit of reverse engineering, the variable ipv6_portforward seems to contain what I need. I am showing below one single firewall rule (firewall rules are all concatenated):

# > nvram get ipv6_portforward
 1<1<<2001:9e8:97c:bcff:dea6:32ff:feb7:cac6<80,443<Forward r-pi4-8 auto dynv6>
 | |  |                                     |                                |
(1)|  |                                     |                                |
  (2) |                                     |                                |
     (3)                                   (4)                               |
                                                                            (5)

Here is the meaning of each sections:

  1. 1: the rule is active (1) or inactive (0)
  2. 1: tcp (1), udp (2) or both (3)
  3. 2001:9e8:97c:bcff:dea6:32ff:feb7:cac6: destination IP
  4. 80,443: ports (coma separated)
  5. > marks the end of that rule

The part between (2) and (3) (the << above) should receive the source IP, but I have not checked and this is not needed here.

Once the update has been done, the firewall service needs to be restarted:

service firewall restart

Deployment

It is possible to paste the script directly in the cron job, but I would not do so: it would then be saved to the NVRAM and eat too much space. Instead I suggest:

  1. attach an USB pen drive to the router: most routers do have such a slot
  2. format that pen drive with eg. ext2 (you can even have several partitions)
  3. configure the router to mount the drive at boot time (see picture)
  4. copy the script to the pen drive (using scp)

mount configuration

(sorry for the crazy arrows, loosing a bit of patience...)

We then configure the CRON ("schedule") to call the script directly from the pen drive. Here we check for a prefix change every 5 minutes:

scheduling configuration

Script

Here is the script... you can imagine how unskilled I am in sh by looking at it. I would have loved having it in python but as mentioned earlier, installing newer versions is impossible from current package systems and it would require me some cross-compilation exploration I cannot afford right now.

But this does the job: I am using this since 10 months now, it works. The resilience to errors on the API calls can be improved (when eg. dynv6 is not reachable), as well as the "atomic" property of updating both the dynv6 and the firewall rules (they should happen hand in hand), but nothing that cannot be fixed. A cached version of the prefix is stored to some /tmp file after an update and compared against the current IPv6 prefix. In case of mismatch, then the update happens: API calls, firewall rules updates, and firewall restart.

Some additional steps are taken during the startup/boot of the router as the cached IPv6 prefix does not exist.

Voilà, the script is largely commented (especially the string transformations using sed), feel free to drop me a message with your improvements!

#!/bin/sh -e

CACHE_REMOTE_PREFIX="/tmp/remote_prefix.txt"
CURRENT_PREFIX=`nvram get ipv6_prefix`
CURRENT_PREFIX_LEN=`nvram get ipv6_prefix_length`

zone="my-zone.dynv6.net"
token='<place here your token>'

if [ -z $CURRENT_PREFIX ]; then
   logger -p ERROR "No IPv6 prefix available."
   exit 1
fi

if [ -z $CURRENT_PREFIX_LEN ]; then
   logger -p ERROR "No IPv6 prefix length available."
   exit 1
fi

CURRENT="${CURRENT_PREFIX}/${CURRENT_PREFIX_LEN}"

CURRENT_REMOTE_PREFIX=""
if [ -f $CACHE_REMOTE_PREFIX ]; then
    CURRENT_REMOTE_PREFIX=`cat ${CACHE_REMOTE_PREFIX}`
else
    # this may happen at startup
    echo "Fetching the remote prefix from remote because current one is empty..."
    payload=$(curl -fsS -H "Authorization: Bearer $token" https://dynv6.com/api/v2/zones/by-name/$zone)

    if [ "$payload" == "" ]; then
        logger -p ERROR "Returned payload empty."
        exit 1
    fi

    # this is the prefix without its length, taken from the json output
    CURRENT_REMOTE_PREFIX=$(echo $payload | sed -E 's/.*"ipv6prefix"\s*:\s*"([:0-9a-fA-F]*)".*/\1/')

    if [ "${CURRENT_REMOTE_PREFIX}" == "${payload}" ]; then
        logger -p ERROR "Cannot find the ipv6prefix in the returned payload: ${payload}"
        exit 1
    fi

    # ipv6prefix string without ':' length * 4 = prefix length
    remove_colums=$(echo $CURRENT_REMOTE_PREFIX | sed -E 's/://g')
    calculated_prefix=$(( ${#remove_colums}*4 ))
    CURRENT_REMOTE_PREFIX="${CURRENT_REMOTE_PREFIX}/${calculated_prefix}"

    # committing to the file for the next round in case the update below fails
    echo $CURRENT_REMOTE_PREFIX > $CACHE_REMOTE_PREFIX
fi


if [ "${CURRENT_REMOTE_PREFIX}" == "${CURRENT}" ]; then
    echo "Same ipv6 configuration: remote(cached) ${CURRENT_REMOTE_PREFIX} / current ${CURRENT}: noop"
    exit 0
fi

echo "Updating the remote dyndns entry: new prefix $CURRENT"
logger -p WARN "Updating the remote dyndns entry: new prefix $CURRENT"

out=$(curl -fsS "https://dynv6.com/api/update?zone=$zone&ipv6prefix=$CURRENT&token=$token")
echo $out

echo "Committing to the file"
logger -p WARN "committing the prefix ${CURRENT} to the file ${CACHE_REMOTE_PREFIX}"
echo $CURRENT > $CACHE_REMOTE_PREFIX
logger -p WARN "updated the ipv6 dyndns successfully"

# updating the port forwarding rules
# 1. remove all from CURRENT_REMOTE_PREFIX
# 2. update the rules for specific addresses

# we strip out the /
# 2001:16b8:4655:3bff::/64 -> 2001:16b8:4655:3bff::
CACHED_PREFIX=$(echo $CURRENT_REMOTE_PREFIX | cut -d'/' -f1)

# ${CACHED_PREFIX%?}
# 2001:16b8:4655:3bff:: -> 2001:16b8:4655:3bff:

previous=`nvram get ipv6_portforward`
reg_ex="1<([123])<<(${CACHED_PREFIX%?})(.*)<"
reg_ex_out="1<\1<<${CURRENT_PREFIX%?}\3<"
rewritten=$(echo $previous | sed -E "s/$reg_ex/$reg_ex_out/gI")

logger -p INFO "Rewritting the IPv6 forwarding rules: ${rewritten} / initial ${previous}"
nvram set ipv6_portforward="${rewritten}"
nvram commit

logger -p INFO "Restarting the firewall services"
service firewall restart

The output in the logs of the router look like this:

Jan 26 06:42:00 my-router user.info sched[23174]: Performing scheduled custom #1...
Jan 26 06:42:01 my-router user.warn root: Updating the remote dyndns entry: new prefix 2001:16b8:46b4:7eff::/64
Jan 26 06:42:04 my-router user.warn root: committing the prefix 2001:16b8:46b4:7eff::/64 to the file /tmp/remote_prefix.txt
Jan 26 06:42:04 my-router user.warn root: updated the ipv6 dyndns successfully
Jan 26 06:42:04 my-router user.info root: Rewritting the IPv6 forwarding rules: 1<3<<2001:16b8:46b4:7eff:ba27:ebff:fe34:c20<80,443<Forward 1>
Jan 26 06:42:06 lolig-router user.info root: Restarting the firewall services

Conclusion

IPv6 was pretty new to me before digging into it, pushed by my provider and my needs. It was a nice experience to break down the problem related to the varying IPv6 prefix and to solve it by a simple script and a basic configuration.

At the end of the day, this is Internet and all of us should be able to host public services when needed. On top of that, having a publicly accessible IP one of the core reasons why IPv6 exists. It may be complicated at first, and your provider may make that even more complicated (and there are many parts of IPv6 I don't need to understand and explore), but nothing we can't overcome.

Figuring out where to execute that script, retaining the firewall property, reverse engineering FreshTomato ... are of my own sauce: I hope you find it useful.

References

This blog post was inspirational and really helpful https://blog.hansenpartnership.com/creating-a-home-ipv6-network/, many thanks to his author!