IPv6

Last modified by Sebastian Marsching on 2022/05/29 14:06

IPv6 with 6to4 setup on Debian / Ubuntu

If you have a static IPv4 address, the easiest way to get IPv6 working is using the 6to4 protocol. 6to4 automatically assigns a /48 IPv6 subnet to each IP address. As the minimal recommended size for a IPv6 subnet is /64, you get nearly 2^16 subnets you can use behind one IPv4 address. This makes 6to4 an excellent choice, if you have a network behind a NAT gateway, with only one static IPv4 address.

You can generate a 6to4 configuration for Debian's or Ubuntu's /etc/network/interfaces using a nice and simple to use web-based tool.

Performing a 6to4 setup on a host behind a NAT gateway

However, if you cannot or do not want to do the IPv6 routing for your network on the NAT gateway with the global IPv4 address, the setup gets a little bit more complicated:

You have to forward all IPv6 traffic (that is protocol number 41) from your NAT gateway to the machine you are performing the 6to4 setup on. On Linux you can do this with iptables using the commands

iptables -t nat -A PREROUTING -d ${EXTERNAL_IPV4_ADDRESS} -p ipv6 -j DNAT --to-destination ${TARGET_HOST}
iptables -A FORWARD -d ${TARGET_HOST} -p ipv6 -j ACCEPT

(the second rule is only needed, if you are not using policy ACCEPT for the FORWARD chain). This will forward all 6to4 related traffic that hits ${EXTERNAL_IPV4_ADDRESS} to ${TARGET_HOST}.

MTU setting

With the default MTU setting of 1480 I experienced strange problems: Sometimes, connections got "stuck". By manually setting the MTU for the tun6to4 interface to 1280 these problems could be solved. I guess, that these problem might be related to packet fragmentation when encapsulating the IPv6 packet within an IPv4 packet.

See also: Path MTU Discovery issues

IPv6 with Xen routed setup

If you want to add IPv6 support to Xen DomUs in a routed network setup, you can either use 6to4 on each individual domU (as long as each has a unique, global IPv6 address), or you can create a routed setup for IPv6 in addition to the exiting IPv4 setup.

This How To expects that you already have IPv6 running for the Xen Dom0. You might want to refer to the section above, if you have not configured IPv6 for the Dom0 yet.

The core of this setup is the following script, which should be saved in /etc/xen/scripts/vif-route-ipv6 (do not forget to chmod a+x the file):

#============================================================================
# /etc/xen/scripts/vif-route-ipv6
#
# Script for adding an IPv6 address to a routed Xen VM.
# This script is called by modified version of /etc/xen/script/vif-route.
#
# Usage:
# vif-route-ipv6 (online|offline)
#
# Environment vars:
# vif         vif interface name (required).
# XENBUS_PATH path to this device's details in the XenStore (required).
#             This path is used to extract the VM's UUID.
#
# Read from the store:
# domain      name of Xen domU
#============================================================================

command=$1

# Read name of domU from Xen Store
domu_name=`xenstore-read ${XENBUS_PATH}/domain`

# Read configuration
CONFIG_FILE="/etc/xen/ipv6.cfg"
grepstr="ipv6_gateway_addr\["${domu_name}"\]="
config_line=`grep -i ${grepstr} ${CONFIG_FILE}`
ipv6_gateway_addr=${config_line##*=}

if [ -z ${ipv6_gateway_addr} ] ; then
 exit 0
fi

case "$command" in
  online)
    ip -f inet6 addr add dev ${vif} ${ipv6_gateway_addr}
    ;;
  offline)
    ip -f inet6 addr del dev ${vif} ${ipv6_gateway_addr}
    ;;
esac

It refers to the config file /etc/xen/ipv6.cfg. This file might look like this:

ipv6_gateway_addr[mydomu1]=2001:db8:0:1::1/64
ipv6_gateway_addr[mydomu2]=2001:db8:0:2::1/64

As you can see, for each DomU, that shall be IPv6 enabled, a line with the DomU name in square brackets is written into the configuration file. The IPv6 address after the equals sign is the address that will be assigned to the virtual interface corresponding to the DomU in the Dom0.

This differs from the IPv4 routed setup, where you only specify the address of the DomU and a host route is created in order to connect the Dom0 with the DomU. For IPv6 we are using a different setup for three reasons:

  1. Configuration gets easier: We do not have to create host routes, the routes will be automatically determined by the subnet prefix for the address. In the example

above, a route for target 2002:ffff:ffff:1::/64 using the correct vif-device will be created automatically. There is also no need to manually configure a host-route to the gateway within the domU: The gateway's address (for mydomu1 in the example it is 2002:ffff:ffff:1::1) is within the subnet of the DomU.

  1. We can easily add extra IP addresses to the DomU: As the Dom0 routes the whole subnet to the DomU, we can just add any address (except the gateway address) within

the /64 subnet to the DomU, without having to change any configuration within the Dom0.

  1. The IPv6 address space is vast: If we have a /48 subnet for the whole Xen host and we use a /64 subnet for each DomU, we can create up to nearly 2^16 DomUs on one Xen host. These are more DomUs than you will ever run on a single Xen host.

In order to make this setup work, we still have to ensure that the script /etc/xen/scripts/vif-routed-ipv6 is called on the startup of a DomU. The easiest way is to patch /etc/xen/scripts/vif-routed using the following patch:

--- vif-route.dpkg-dist 2010-01-09 15:34:48.000000000 +0100
+++ vif-route   2010-01-09 15:49:17.000000000 +0100
@@ -31,11 +31,13 @@
        echo 1 >/proc/sys/net/ipv4/conf/${vif}/proxy_arp
         ipcmd='add'
         cmdprefix=''
+        XENBUS_PATH="${XENBUS_PATH}" vif="${vif}" $dir/vif-route-ipv6 online
        ;;
     offline)
         do_without_error ifdown ${vif}
         ipcmd='del'
         cmdprefix='do_without_error'
+        XENBUS_PATH="${XENBUS_PATH}" vif="${vif}" $dir/vif-route-ipv6 offline
        ;;
 esac

Finally, the setup in the domU is pretty easy: You can just use a statically configured inet6 setup on eth0. Example:

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
 address 192.0.2.31
 gateway 192.0.2.1
 netmask 255.255.255.255
 pointopoint 192.0.2.1
 post-up  /usr/sbin/ethtool -K eth0 tx off

iface eth0 inet6 static
 address 2001:db8:0:1::2
 netmask 64
 gateway 2001:db8:0:1::1

NAT with a dynamic IPv6 prefix

Some Internet service providers only provide a dynamic prefix for IPv6 via DHCPv6 prefix delegation. This prefix changes from time to time (e.g. when the connection is interrupted and reestablished).

With such a setup, there are two challenges: First, one has to delegate the prefix to the various subnets / VLANs within one's network. Second, using addresses with a non permanent prefix causes extra challenges for example when defining firewall rules between the various VLANs.

For these reasons, I am going to describe a setup in which the LAN only uses addresses from the unique local address (ULA) space. When routing into the Internet, these addresses are replaced with addresses allocated using the dynamic prefix.

For this example, it is assumed that the ISP provides a sufficiently large prefix (typically a /56) via DHCPv6 prefix delegation and that the Internet router at the edge of the LAN is capable of further delegating (parts of) this prefix via DHCPv6 prefix delegation. In my case, I am using an AVM Fritz!Box 3370 connected to a VDSL2 line from Deutsche Telekom. Deutsche Telekom provides a dynamic /56 via prefix delegation and the Fritz!Box can be configured to serve prefix delegation requests from DHCPv6 clients.

A computer that is connected to the VLAN of the Internet router (Fritz!Box) and to all other VLANS acts as a firewall and router. On this computer, I run the DHCP client from the dhcpcd5 Ubuntu package. Please note that the version of this package included before Ubuntu 16.04 LTS is too old because it lacks critical features. Luckily, the package from Ubuntu 16.04 LTS can be installed on Ubuntu 14.04 LTS (and presumably newer versions of Ubuntu) without any problems.

I use the following configuration file (/etc/dhcpcd.conf):

# A sample configuration for dhcpcd.
# See dhcpcd.conf(5) for details.

# Allow users of this group to interact with dhcpcd via the control socket.
#controlgroup wheel

# Inform the DHCP server of our hostname for DDNS.
hostname

# Use the hardware address of the interface for the Client ID.
#clientid
# or
# Use the same DUID + IAID as set in DHCPv6 for DHCPv4 ClientID as per RFC4361.
# Some non-RFC compliant DHCP servers do not reply with this set.
# In this case, comment out duid and enable clientid above.
duid

# Persist interface configuration when dhcpcd exits.
persistent

# Rapid commit support.
# Safe to enable by default because it requires the equivalent option set
# on the server to actually work.
option rapid_commit

# A list of options to request from the DHCP server.
option domain_name_servers, domain_name, domain_search, host_name
option classless_static_routes
# Most distributions have NTP support.
option ntp_servers
# Respect the network MTU.
# Some interface drivers reset when changing the MTU so disabled by default.
#option interface_mtu

# A ServerID is required by RFC2131.
require dhcp_server_identifier

# Generate Stable Private IPv6 Addresses instead of hardware based ones
slaac private

# A hook script is provided to lookup the hostname if not set by the DHCP
# server, but it should not be run by default.
nohook lookup-hostname

# Limit interfaces used by dhcpcd (comma-separated).
allowinterfaces eth0

# Configuration for eth0
interface eth0
clientid "";
persistent
option rapid_commit
nooption domain_name_servers, domain_name, domain_search, host_name
nooption classless_static_routes
nooption ntp_servers
slaac hwaddr
nohook hostname lookup-hostname mtu ntp.conf resolv.conf timezone wpa_supplicant
ia_pd 1/::/58 nosuch0/0/58
ipv6only
nogateway
noipv6rs
noauthrequired
script /etc/dhcpcd-pd-script

Everything before the "allowinterfaces" line is the default configuration. I use the allowinterfaces option because the DHCPv6 client shall only run on the interface eth0 (the interface facing the Internet router in this example). All the other interfaces use a static configuration (remember that internally this computer is a router).

We are only interested in the delegated IPv6 prefix, so I disable everything else using the nooption lines and disable all hooks that would affect the computer's network configuration.

I request a /58 prefix (so Internet router gets a /56 from the ISP so it should easily be able to provide a /58) using the ia_pd line. I have to specify a network interface that uses the assigned prefix, otherwise dhcpcd will not work. However, I do not want to use this prefix on a network interface, but only want to use it in IPtables. Therefore I specify an interface name that does not exist (nosuch0). This causes the DHCP client to log a warning, but the prefix delegation will still work.

The ipv6only options disables DHCP for IPv4 (I use a static IP address for IPv4) and the nogateway and noipv6rs options ensure that the DHCP client will not add any routes.

The noauthrequired option ensures that the IPv6 prefix is updated when the Internet router gets a new prefix from the ISP. Obviously, actually configuring authentication would be preferrable, but in my case the Internet router does not seem to support this. If there only are trusted devices in the network connecting the computer running dhcpcd with the Internet router, disabling authentication should be safe.

Finally, the script specified in the script line is called whenever a DHCP lease is obtained, renewed or released. It allows us to use the assigned prefix (we will soon see how).

In /etc/network/interfaces I use the following configuration for eth0:

iface eth0 inet6 auto
  privext 0
  dhcp 0
  post-up sysctl -w net.ipv6.conf.$IFACE.accept_ra=2 >/dev/null

I disable the privacy extension because it does not make much sense for a router. I also disable DHCPv6 because Ubuntu uses ISC's DHCPv6 client (dhclient), which unfortunately cannot handle prefix delegations correctly. The dhcpcd5 DHCPv6 client on the other hand does not have to be triggered by the network scripts explicitly but will detect the new interface and start its work automatically.

The post-up script is needed because Linux does not accept router advertisements (RAs) by default when forwarding is enabled. The rational behind this is that a computer with forwarding enabled acts as a router and typically a router should not accept RAs from other routers. In our case however, we want to accept RAs from our upstream router and setting accept_ra to 2 will override the default behavior.

Finally, we need the script that is called when a dynamic prefix is assigned so that we can create the corresponding rules for ip6tables.

I use the following script (/etc/dhcpcd-pd-script):

#!/bin/bash

set -e

ip6t="/sbin/ip6tables"
prefix_file="/var/run/ipv6-pd/current_prefix"
external_interface="eth0"
internal_prefix="fc::/58"
internal_redirect_mark="0x8aebe875"

update_iptables() {
 local external_prefix
 external_prefix="$1"
 # Ensure that the chains exist. If they already exist, creating them causes
 # an error that we have to catch.
 "${ip6t}" -t nat -N external_dnat 2>/dev/null || true
 "${ip6t}" -t nat -N external_snat 2>/dev/null || true
 # Flush the (existing) chains.
 "${ip6t}" -t nat -F external_dnat
 "${ip6t}" -t nat -F external_snat
 "${ip6t}" -t nat -A external_dnat -i "${external_interface}" \
   -d "${external_prefix}" -j NETMAP --to "${internal_prefix}"
 "${ip6t}" -t nat -A external_snat -o "${external_interface}" \
   -s "${internal_prefix}" -j NETMAP --to "${external_prefix}"
 # Internal traffic directed to the external prefix should be rerouted to the
 # internal prefix. However, the source address has to be rewritten so that
 # the response will parse through this router again and thus the address in
 # the response packet can be rewritten again. We use a mark so that we can
 # know which packets need to be touched in the POSTROUTING (external_snat)
 # chain.
 "${ip6t}" -t nat -A external_dnat ! -i "${external_interface}" \
   -s "${internal_prefix}" -d "${external_prefix}" -j MARK \
   --set-mark "${internal_redirect_mark}"
 "${ip6t}" -t nat -A external_dnat ! -i "${external_interface}" \
   -s "${internal_prefix}" -d "${external_prefix}" -j NETMAP \
   --to "${internal_prefix}"
 "${ip6t}" -t nat -A external_snat ! -i "${external_interface}" \
   -s "${internal_prefix}" -m mark --mark "${internal_redirect_mark}" \
   -j NETMAP --to "${external_prefix}"
}

# If this script is called by the firewall script, we only try to restore the
# IPTables rules.
if [ $# -ge 1 ] && [ "$1" = "restore-iptables" ]; then
 if [ -f "${prefix_file}" ]; then
   last_prefix="`cat "${prefix_file}"`"
    update_iptables "${last_prefix}"
 fi
 exit 0
fi

# If this script is called without a new prefix, there is nothing we can or have
# to do.
if [ ! -z "${new_dhcp6_ia_pd1_prefix1}" ]; then
 expected_prefix_length="`echo -n "$internal_prefix" | cut -d / -f 2`"
 # We expect a prefix of the right length because this is what we request.
 # However, as our script cannot work correctly when the length of the
 # internal and the external prefix do not match, we check this to be sure.
 if [ "${new_dhcp6_ia_pd1_prefix1_length}" -ne "${expected_prefix_length}" ]; then
   echo "Invalid prefix length: Expected ${expected_prefix_length} but got ${new_dhcp6_ia_pd1_prefix1_length}."
   exit 1
 fi
 new_prefix="${new_dhcp6_ia_pd1_prefix1}/${expected_prefix_length}"
 if [ -f "${prefix_file}" ]; then
   last_prefix="`cat "${prefix_file}"`"
 else
   last_prefix=""
 fi
 if [ "${last_prefix}" = "${new_prefix}" ]; then
   if "${ip6t}" -t mangle -L external_dnat -n -v 2>/dev/null | grep -q -F "${new_prefix}"; then
     # The prefix has not changed and the IPTables rules have already been
     # created.
     exit 0
   fi
 fi
  update_iptables "${new_prefix}"
  mkdir -p "`dirname "${prefix_file}"`"
 echo -n "${new_prefix}" >"${prefix_file}"
fi

In this script, you have to adjust your internally used prefix (when choosing a ULA prefix, you should use a random number from the range fc::/7 in order to avoid colissions when connecting different networks using addresses from the ULA space). Like in the other configuration files, you have to change the interface name from eth0 to whichever is the name of the interface that connects to the Internet router.

In order to work correctly when handling traffic that comes from the internal network and is directed at the internal network but using a destination address with the external prefix, the iptables rules use a mark. This will only work correctly if no other rules affecting the packet (in particular in the FORWARD chain) set the mark.

This script does the following: When called with the new_dhcp6_ia_pd1_prefix1 environment variable set, it uses this prefix to create iptables rules that replace the internal prefix with the dynamic external one when routing through the external interface.

These rules are created in separate chains (external_dnat and external_snat), so that we can easily replace the rules without affecting any other rules that might be present. Please note that these chains need to be called from the PREROUTING and POSTROUTING chains like this:

ip6tables -t nat -A PREROUTING -j external_dnat
ip6tables -t nat -A POSTROUTING -j external_snat

You might have noticed that there is no code that removes the rules when the prefix delegation expires. The rationale behind this is simple: Usually, we only expect a prefix to be replaced with a different prefix. The only case when we would expect no prefix at all is when our Internet connection is down. In this case, however, it does not matter if we still have a rule with the old prefix.

Using the DHCPv6 client in a fail-over setup

In my case, the actual setup is even a bit more complex: I do not want the internal router to be a single point of failure. For the DSL router on the edge of the network this is acceptable because there is no reasonable way to avoid this. A simple router box is also less likely to fail than a "real" computer and software updates requiring a reboot are less frequent, too.

I will not discuss here the details of the fail-over setup of the network interfaces. I use a HA solution involving OpenVSwitch. For the rest of this tutorial, it is assumed that fail-over is working for the network interfaces and that the network interface facing the Internet router (eth0) uses the same MAC address on all nodes of the HA cluster and is only active on a single node at once.

The remaining challenge is to ensure that the DHCPv6 client uses the same prefix when fail-over from one node to another one happens. If the prefix changed, existing connections would be interrupted.

DHCPv6 uses a DUID identifying the client when contacting the server. Typically, this ID is generated when the client runs for the first time and stored internally. This way, a client always has the same DUID when contacting a server. The DHCPv6 client from the dhcpcd5 package stores this DUID in /etc/dhcpcd.duid. We could copy this file to all nodes so that they will identify as the same DHCPv6 client, however this could be dangerous. If there happen to be other interfaces on which we want to use DHCPv6 and for one of these interfaces two nodes might be active at the same time, it could end up in the same addresses being assigned to two different clients. In addition to that, we would also have to keep the information about active leases in sync between nodes.

Luckily, there is a simpler approach to this issue: The DHCP client from dhcpcd5 has been specifically designed to work on computer where there is no permanent storage available (e.g. some embedded devices). On these devices, it generates a DUID based on the hardware address of the interface and it does not store lease information. This is exactly what we want. Typically, a DHCPv6 server will assign the same lease if the same client requests a new lease and its current lease has not expired yet (at least, this is what the DHCPv6 server in the Fritz!Box does). So if we fail-over from one node to another one, the delegated IPv6 prefix will be kept (the hardware address of the interfaces is the same as described earlier).

Unfortunately, there is no way to tell the DHCP client to operate with storing the DUID and lease information. It will simply try to store this information and fall-back to working without stored information if the write operation fails. We cannot use restrictive permissions on the relevant files because the DHCP clients runs with root privileges. However, we can set the immutable attribute on them, so that they cannot be changed any longer. We do this by running the following two commands:

chattr +i /etc/dhcpcd.duid
chattr +i /var/lib/dhcpcd5

Before doing this, ensure that etc/dhcpcd.duid is an existing empty file and that /var/lib/dhcpcd5 is an existing empty directory.

Now, the write operations will fail and the DHCP client will show the desired behavior.