IPv6 and home servers: the varying prefix dilemma
Sun 21 August 2022Introduction
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:
The second is about sending actual network packets to the target machine:
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
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:
- 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,
- 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:
- create a new DNS zone
my-zone
- 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 theMAC
address of the server you want on the Internet (on the interface that will be connected to the router). This is the conveniencedynv6
is providing us with. - finally (optional) create a
CNAME
pointing tomyserver.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:
- open your router admin interface
- select "port forwarding" on the left menu
- go to "basic IPv6"
- 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
. - click save: the configuration is now in the
nvram
and is persisted when the router is rebooted
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:
- open the settings, select "Network and Internet"
- check the names of the access points and then the access point itself
- open the settings and enable IPv6 for the APNs
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
AndroDNS
for any DNS issue: this should work irrelevant from your IPv4/IPv6 configuration on your phone- if you receive a timeout, it is likely that the firewall is not letting the traffic through
- 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:
- 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
- DNS update: this will be an API call to specific
dynDNS
endpoints - 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
- those are not persisted after a reboot, it would mean we have to do some manual work ourselves
- 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
: the rule is active (1) or inactive (0)1
: tcp (1), udp (2) or both (3)2001:9e8:97c:bcff:dea6:32ff:feb7:cac6
: destination IP80,443
: ports (coma separated)>
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:
- attach an USB pen drive to the router: most routers do have such a slot
- format that pen drive with eg.
ext2
(you can even have several partitions) - configure the router to mount the drive at boot time (see picture)
- copy the script to the pen drive (using
scp
)
(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:
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!