Table of contents:

IPv6 Prefix Delegation

Prefix delegation is a mechanism that dynamically assigns an IPv6 host an address prefix to create one or more subnets. The host could be a router, for example, that gets a prefix dynamically assigned from an internet service provider and uses this prefix to assign IPv6 addresses to clients connected to it on a separate (local) network interface.

Since Debian release 9 (stretch), prefix delegation is supported without requiring additional packages. Support for prefix delegation relies on the packages ifupdown and isc-dhcp-client, which are part of the default installation.

Prerequisites

This how-to merely covers requesting a prefix via DHCPv6 on one network interface and assigning that prefix to another. It's assumed that you are familiar with the Debian way of configuring network interfaces (i.e. via the file /etc/network/interfaces).

Topics not covered by this document are for example how to configure packet forwarding, a firewall, router advertisements or a DHCPv6 server. If you're planning to use prefix delegation on a server that forwards packets between network interfaces (i.e. a router), you are strongly advised to implement network and server security measures, such as installing a firewall.

Requesting a prefix

Requesting a prefix can be configured with the option request_prefix in /etc/network/interfaces. For example:

iface enp1s0 inet6 dhcp
        request_prefix 1

This will cause the DHCPv6 client to request both an IPv6 address and a prefix from the network on enp1s0.

Requesting a prefix is also possible in combination with stateless autoconfiguration (SLAAC). Simply specify auto instead of dhcp as the configuration method:

iface enp1s0 inet6 auto
        dhcp 1
        request_prefix 1

With this, the address of enp1s0 will be configured via SLAAC and a prefix will be requested via DHCPv6.

Finally, some providers will send addresses via DHCPv6, but the default route via SLAAC:

iface enp1s0 inet6 dhcp
        accept_ra 2
        request_prefix 1

Assigning the prefix to another interface

Once a prefix has been obtained, it can be assigned to another interface.

For the interface that is supposed to use the requested prefix (in the following example, this is enp2s0), manual configuration is required and the related stanza in /etc/network/interfaces should look like this:

iface enp2s0 inet6 manual

Unfortunately, the ISC DHCP(v6) client can't automagically assign the obtained prefix to another interface, so you'll need a small script to perform this task. The script needs to be placed in the directory /etc/dhcp/etc/dhcp/dhclient-exit-hooks.d/ so it'll be executed by the DHCPv6 client whenever the leased prefix changes.

Here is an example script that will take care of all the necessary steps to assign a delegated prefix to another interface. It will remove an old prefix after it expired, generate an interface address with a newly learned prefix and assign that address to the interface specified in the variable IA_PD_IFACE (see line 12 of the script). In addition, it can restart or reload services after a prefix change (e.g. your firewall).

Sample script:

# This script assigns a delegated IPv6 prefix obtained via DHCPv6 to another interface
#
# Usage: This scrips is designed to be called from dhclient-script (isc-dhcp-client).
#
# LOCATION: /etc/dhcp/dhclient-exit-hooks.d/prefix_delegation
# RECOMMENDED PACKAGES: ipv6calc

# CONFIGURATION OPTIONS

# Define the interface to which a delegated prefix will be assigned
# This must not be the same interface on which the prefix is learned!
IA_PD_IFACE="enp2s0"

# Provide a space separated list of services that need to be restarted or reloaded after a prefix change
# Services must be controllable via systemd's systemctl, the default action is restart
# Service names may be followed by a colon and action name, to override the default action
# Supported actions are: restart and reload
# Example: IA_PD_SERVICES="shorewall6:reload dnsmasq"
IA_PD_SERVICES=""

# Define the location of the ipv6calc executable, if installed
# If this is empty or no executable file, no EUI-64 based IPv6 address will be calculated for the interface set in IA_PD_IFACE; instead, a static interface identifier (::1) will be appended to the prefix
# Example: IA_PD_IPV6CALC="/usr/bin/ipv6calc"
IA_PD_IPV6CALC=""

# Set to yes to make logging more verbose
IA_PD_DEBUG="no"

# END OF CONFIGURATION OPTIONS

fn_calc_ip6addr() {
        [ -z "$1" ] && return
        local ia_pd_mac
        local ia_pd_addr
        [ -e "/sys/class/net/${IA_PD_IFACE}/address" ] && ia_pd_mac="$(cat /sys/class/net/${IA_PD_IFACE}/address)"
        if [ -n "$ia_pd_mac" ] && [ -n "$IA_PD_IPV6CALC" ] && [ -x "$IA_PD_IPV6CALC" ]; then
                [ "$IA_PD_DEBUG" = "yes" ] && logger -t "dhcpv6-pd" -p daemon.debug "Debug: Determined MAC address $ia_pd_mac for interface $IA_PD_IFACE."
                ia_pd_addr="$("$IA_PD_IPV6CALC" -I prefix+mac -A prefixmac2ipv6 -O ipv6addr "$1" "$ia_pd_mac")"
        fi
        if [ -z "$ia_pd_addr" ]; then
                [ "$IA_PD_DEBUG" = "yes" ] && logger -t "dhcpv6-pd" -p daemon.debug "Debug: Failed to calculate EUI-64 based IPv6 address, using static client suffix ::1 instead."
                echo "$1" | sed 's#::/#::1/#'
        else
                echo "$ia_pd_addr"
        fi
}

fn_restart_services() {
        if [ -n "$IA_PD_SERVICES" ]; then
                local pair
                local action
                local daemon
                for pair in $IA_PD_SERVICES ; do
                        action="$(echo "$pair" | cut -d':' -f2)"
                        daemon="$(echo "$pair" | cut -d':' -f1)"
                        # Check if a valid action was provided or default to 'restart'
                        case $action in
                                reload) action="reload";;
                                *)      action="restart";;
                        esac
                        # Check if daemon is active before trying to restart or reload it (avoids non-zero exit code)
                        if ! systemctl -q is-active "${daemon}.service" > /dev/null ; then
                                logger -t "dhcpv6-pd" -p daemon.info "Info: $daemon is inactive. No $action required."
                                continue
                        fi
                        if systemctl -q "$action" "${daemon}.service" > /dev/null ; then
                                logger -t "dhcpv6-pd" -p daemon.info "Info: Performed $action of $daemon due to change of IPv6 prefix."
                        else
                                logger -t "dhcpv6-pd" -p daemon.err "Error: Failed to perform $action of $daemon after change of IPv6 prefix."
                        fi
                done
        elif [ "$IA_PD_DEBUG" = "yes" ]; then
                logger -t "dhcpv6-pd" -p daemon.debug "Debug: No list of services to restart or reload defined."
        fi
}

fn_remove_prefix() {
        [ -z "$1" ] && return
        [ "$IA_PD_DEBUG" = "yes" ] && logger -t "dhcpv6-pd" -p daemon.debug "Debug: Old prefix $1 expired."
        if [ "$(ip -6 addr show dev "$IA_PD_IFACE" scope global | wc -l)" -gt 0 ]; then
                logger -t "dhcpv6-pd" -p daemon.info "Info: Flushing global IPv6 addresses from interface $IA_PD_IFACE."
                if ! ip -6 addr flush dev "$IA_PD_IFACE" scope global ; then
                        logger -t "dhcpv6-pd" -p daemon.err "Error: Failed to flush global IPv6 addresses from interface $IA_PD_IFACE."
                        return
                fi
                # Restart services in case there is no new prefix to assign
                [ -z "$new_ip6_prefix" ] && fn_restart_services
        elif [ "$IA_PD_DEBUG" = "yes" ]; then
                logger -t "dhcpv6-pd" -p daemon.debug "Debug: No global IPv6 addresses assigned to interface $IA_PD_IFACE."
        fi 
}

fn_assign_prefix() {
        [ -z "$1" ] && return
        local new_ia_pd_addr
        new_ia_pd_addr="$(fn_calc_ip6addr "$1")"
        if [ -z "$new_ia_pd_addr" ]; then
                logger -t "dhcpv6-pd" -p daemon.err "Error: Failed to calculate address for interface $IA_PD_IFACE and prefix $1"
                return
        fi
        [ "$IA_PD_DEBUG" = "yes" ] && logger -t "dhcpv6-pd" -p daemon.debug "Debug: Received new prefix $1."
        # dhclient may return an old_ip6_prefix even after a reboot, so manually check if the address is already assigned to the interface
        if [ "$(ip -6 addr show dev "$IA_PD_IFACE" | grep -c "$new_ia_pd_addr")" -lt 1 ]; then
                logger -t "dhcpv6-pd" -p daemon.info "Info: Adding new address $new_ia_pd_addr to interface $IA_PD_IFACE."
                if ! ip -6 addr add "$new_ia_pd_addr" dev "$IA_PD_IFACE" ; then
                        logger -t "dhcpv6-pd" -p daemon.err "Error: Failed to add new address $new_ia_pd_addr to interface $IA_PD_IFACE."
                        return
                fi
                fn_restart_services
        elif [ "$IA_PD_DEBUG" = "yes" ]; then
                logger -t "dhcpv6-pd" -p daemon.debug "Debug: Address $new_ia_pd_addr already assigned to interface $IA_PD_IFACE."
        fi 
}

# Only execute on specific occasions
case $reason in
        BOUND6|EXPIRE6|REBIND6|REBOOT6|RENEW6)
                # Only execute if either an old or a new prefix is defined
                if [ -n "$old_ip6_prefix" ] || [ -n "$new_ip6_prefix" ]; then
                        # Check if interface is defined and exits
                        if [ -z "$IA_PD_IFACE" ] || [ ! -e "/sys/class/net/${IA_PD_IFACE}" ]; then
                                logger -t "dhcpv6-pd" -p daemon.err "Error: Interface ${IA_PD_IFACE:-<undefined>} not found. Cannot assign delegated prefix!"
                        else
                                # Remove old prefix if it differs from new prefix
                                [ -n "$old_ip6_prefix" ] && [ "$old_ip6_prefix" != "$new_ip6_prefix" ] && fn_remove_prefix "$old_ip6_prefix"
                                # Assign new prefix
                                [ -n "$new_ip6_prefix" ] && fn_assign_prefix "$new_ip6_prefix"
                        fi
                fi
                ;;
esac

Save this script to /etc/dhcp/dhclient-exit-hooks.d/prefix_delegation and make it executable. Don't forget to adjust the configuration options at the top, especially the name of the interface to which the prefix will be assigned to. Messages are logged to syslog with the tag dhcpv6-pd. Installation of the package ipv6calc is recommended in order to calculate IPv6 addresses based on the prefix and the MAC address of the interface, but this is optional. The script falls back to using a static interface identifier (::1) that will be appended to the prefix, in case ipv6calc isn't available.

This script is an example that should cover the basic actions required for prefix delegation. Depending on your needs, it may be simplified or extended. One limitation is that it won't split the prefix into multiple subnets to assign to more than one interface. If you need this, feel free to extend the script or check out other DHCPv6 clients.

radvd.conf

radvd (on the inside interface, to enable SLAAC and automatic router setup on the clients) can be set up such that it works regardless of the assigned prefix, by using “prefix ::/64”. This makes it unneccessary to write a new .conf file from the script. Example config:

interface vlan10 {
   AdvSendAdvert on;
   MinRtrAdvInterval 3;
   MaxRtrAdvInterval 10;
   prefix ::/64 {
     AdvOnLink on;
     AdvAutonomous on;
     AdvRouterAddr on;
     AdvValidLifetime 3600;
     AdvPreferredLifetime 3600;
   };
};

Other DHCPv6 clients

There are other DHCPv6 clients that support prefix delegation as well, such as WIDE DHCPv6 client, Dibbler or dhcpcd. The ISC DHCP(v6) client is recommended here mainly since it's already included in the default installation of Debian and because it has good scripting support in order to perform custom actions whenever the DHCPv6 lease is obtained, renewed, expired, etc.

Example /etc/wide-dhcpv6/dhcp6c.conf that sets up prefix delegation to one other interface:

profile default
{
  information-only;

  request domain-name-servers;
  request domain-name;
  request ntp-servers;

  script "/etc/wide-dhcpv6/dhcp6c-script";
};

interface vlan11 {
  send ia-na 0;
  send ia-pd 0;
};

id-assoc na 0 {
};

id-assoc pd 0 {
  prefix-interface vlan10 {
    sla-id 10;
    sla-len 8;
  };
};

In this case, due to “sla-id 10”, the subnetwork would get ...a::/64. Change it as needed.

If using wide-dhcpv6-client, the outer interface should be set as “auto” in /etc/network/interfaces, not “dhcp”.


CategoryNetwork