# Title : ipv6-pd-br0 # Last modified date : 26.03.2023 # Author : Martin Tonusoo # Description : Script finds the /64 IPv6 network by combining the # /56 prefix delegated by Telia(from 2001:7d0::/32 # allocation) and value of "ip6_net" variable. First # address of this /64 network is added to a port # specified by "ia_pd_iface" variable or removed # from this interface depending on the DHCPv6 # event. In addition, radvd configuration file # is updated accordingly. # Options : # Notes : Script is meant to be run as a dhclient exit hook. # # Script expects a single dhclient instance # in DHCPv6 mode, i.e it is not tested for # possible race conditions which might occur # if multiple "dhclient -6 ..." instances # are running at once. # # Script is POSIX sh compliant except the variables # declared to be local to functions. Coding style # is based on /usr/sbin/dhclient-script. # # Initially based on script seen here: # https://wiki.debian.org/IPv6PrefixDelegation ia_pd_iface="br0" # Values from 00 to ff. ip6_net="00" find_ia_pd_addr() { local h h1 h2 h3 h4 ip6_prefix # According to technical service description, # Telia delegates a /56 prefix from their # 2001:7d0::/32 allocation. This function takes # the delegated /56 prefix as an input, combines # it with the value of ip6_net variable and # returns a RFC5952(A Recommendation for IPv6 # Address Text Representation) compatible /64 prefix. # Explode the possibly compressed IPv6 prefix. IFS=: read -r h1 h2 h3 h4 _ <<-EOF ${1%/56} EOF ip6_prefix="" for h in "$h1" "$h2" "${h3:-0}" "${h4:-0}"; do ip6_prefix="$ip6_prefix"$(printf "%04x:" "0x$h") done ip6_prefix="${ip6_prefix%[[:xdigit:]][[:xdigit:]]:}$ip6_net::/64" # Convert the exploded IPv6 prefix back to RFC5952 # compatible compressed format. IFS=: read -r h1 h2 h3 h4 _ <<-EOF $ip6_prefix EOF # In case both the third and fourth hextet # is all zeros, then simply do not show those # in the final output. if [ "$h3" = "0000" ] && [ "$h4" = "0000" ]; then h3="" h4="" fi ip6_prefix="" for h in "$h1" "$h2" "$h3" "$h4"; do [ -z "$h" ] && continue ip6_prefix="$ip6_prefix"$(printf "%x:" "0x$h") done echo "$ip6_prefix:/64" } get_prefix_rm_time() { local valid_lft="$1" local unix_time rm_unix_time unix_time=$(date +%s) # Sanity check. Ensure that $valid_lft is an int. case "$valid_lft" in ""|*[!0-9]*) rm_unix_time="$unix_time" ;; *) rm_unix_time=$(( unix_time + valid_lft )) ;; esac date --iso-8601="seconds" -d @"$rm_unix_time" } get_ip6_ll_addr() { local iface="$1" local mac_addr ll_prefix # Pick the first link local IPv6 address. read -r _ _ ll_prefix _ <<-EOF $(ip --brief -6 addr show dev "$iface" scope link 2>/dev/null) EOF if [ -n "$ll_prefix" ]; then echo "${ll_prefix%/*}" elif [ -x "$(PATH=$PATH:/usr/local/bin/ command -v mac_to_ip6_ll_addr)" ]; then # As it's possible that the interface is for example in the administratively # disabled state and thus does not have the automatically found link local # address set, then use the external script which is able to calculate the # IPv6 link local address based on the MAC address. read -r _ _ mac_addr _ <<-EOF $(ip --brief link show dev "$iface" 2>/dev/null) EOF PATH="$PATH:/usr/local/bin/" mac_to_ip6_ll_addr "$mac_addr" fi } # Function creates an initial Router Advertisement # Daemon(radvd) configuration file /etc/radvd.conf # if it does not exist or updates the prefix and # RDNSS(Recursive DNS Server) statements based on # DHCPv6 updates in case the /etc/radvd.conf is # present. Preferred and valid lifetime of the prefix # set by Telia's DHCPv6 server(s) are propagated # towards LAN by radvd. # # It's needed to keep advertising the prefixes with # no valid or preferred lifetime left towards LAN at # least as long as the last non-zero valid lifetime # of the prefix in order to ensure that all the # hosts in LAN pick up the prefix deprecation. Such # setup ensures that the prefix deprecation is seen # even by devices which were for example in suspended # to RAM state at the time of the delegated prefix change. # # For each "prefix" statement in radvd.conf file the # function finds a timestamp in the future when this # prefix could be removed from the radvd.conf file # in order to avoid stale entries. Prefixes removal # from radvd.conf are handled by another script # running in cron as this should not be coupled with # dhclient. # # RDNSS definition is overwritten with a link-local # address of the interface where the /64 address is # added. The link-local address is found automatically # because this interface could be a bridge and depending # on the kernel version the bridge interface MAC address # changes depending on its member interfaces. This in # turn can cause the IPv6 link local address to change. # # According to RFC8106 section 5.1 the addresses for # RDNSSes in the RDNSS option MAY be link-local addresses. # Tested with Debian 11 host running rdnssd ver 1.0.4. # # Function is based on make_resolv_conf() in # /sbin/dhclient-script. make_radvd_conf() { local prefix="$1" local preferred_lft="$2" local valid_lft="$3" local ll_addr radvd_conf new_radvd_conf line option value prefix_rm_time prefix_found ll_addr=$(get_ip6_ll_addr "$ia_pd_iface") radvd_conf=$(readlink -f "/etc/radvd.conf" 2>/dev/null) || radvd_conf="/etc/radvd.conf" prefix_rm_time=$(get_prefix_rm_time "$valid_lft") if [ -f "$radvd_conf" ]; then new_radvd_conf="${radvd_conf}.dhclient-new.$$" wait_for_rw "$new_radvd_conf" rm -f "$new_radvd_conf" prefix_found="" while IFS= read -r line; do case "$line" in " RDNSS "*) printf " RDNSS %s { };\n" "$ll_addr" ;; " prefix $prefix {"*) if [ "$valid_lft" = "0" ]; then IFS=" ;" read -r _ _ _ option value _ <<-EOF $line EOF if [ "$option" = "AdvValidLifetime" ]; then # If the value of AdvValidLifetime was already 0, then # keep using the old expiration field. if [ "$value" = "0" ]; then prefix_rm_time="${line##* }" else prefix_rm_time=$(get_prefix_rm_time "$value") fi fi fi printf " prefix %s { AdvValidLifetime %s; AdvPreferredLifetime %s; }; # expires at %s\n" \ "$prefix" \ "$valid_lft" \ "$preferred_lft" \ "$prefix_rm_time" prefix_found="yes" ;; "};") if [ -z "$prefix_found" ]; then # Last line of the conf file AND prefix was not found. Add the new prefix. printf " prefix %s { AdvValidLifetime %s; AdvPreferredLifetime %s; }; # expires at %s\n%s\n" \ "$prefix" \ "$valid_lft" \ "$preferred_lft" \ "$prefix_rm_time" \ "};" else echo "};" fi ;; *) echo "$line" ;; esac done < "$radvd_conf" >>"$new_radvd_conf" chown --reference="$radvd_conf" "$new_radvd_conf" chmod --reference="$radvd_conf" "$new_radvd_conf" mv -f "$new_radvd_conf" "$radvd_conf" else # Build the initial radvd configuration. cat <<-EOF > "$radvd_conf" # Generated by ipv6-pd-br0 dhclient exit hook. interface $ia_pd_iface { # Send RA messages. AdvSendAdvert on; MinRtrAdvInterval 60; MaxRtrAdvInterval 180; RDNSS $ll_addr { }; prefix $prefix { AdvValidLifetime $valid_lft; AdvPreferredLifetime $preferred_lft; }; # expires at $prefix_rm_time }; EOF fi # Reload the radvd if it is already running. # This will immediately send a Router Advertisement # to hosts in LAN. if systemctl is-active --quiet radvd.service; then systemctl reload radvd.service 2>/dev/null fi } # Only execute on specific occasions case "$reason" in BOUND6|EXPIRE6|REBIND6|REBOOT6|RENEW6|RELEASE6|STOP6|DEPREF6) # Only execute if either an old or a new or a current prefix is defined if [ -n "$old_ip6_prefix" ] || [ -n "$new_ip6_prefix" ] || [ -n "$cur_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 "ipv6-pd-br0" -p daemon.err \ "$reason - ERROR: Interface ${ia_pd_iface:-} not found." else # Remove old prefix if it differs from new prefix or the valid # lifetime of the new prefix is 0. if { [ -n "$old_ip6_prefix" ] && [ "$old_ip6_prefix" != "$new_ip6_prefix" ]; } || { [ -n "$new_max_life" ] && [ "$new_max_life" -eq 0 ]; }; then old_ia_pd_addr=$(find_ia_pd_addr "$old_ip6_prefix") while read -r error_message; do case "$error_message" in "") logger -t "ipv6-pd-br0" -p daemon.info \ "$reason - INFO: Deleted old address $old_ia_pd_addr from interface $ia_pd_iface." ;; "RTNETLINK answers: Cannot assign requested address") logger -t "ipv6-pd-br0" -p daemon.info \ "$reason - INFO: Address $old_ia_pd_addr already deleted from interface $ia_pd_iface." ;; *) logger -t "ipv6-pd-br0" -p daemon.err \ "$reason - ERROR: Failed to delete old address $old_ia_pd_addr from interface $ia_pd_iface." esac done <<-EOF $(ip -6 addr del "$old_ia_pd_addr" dev "$ia_pd_iface" scope global 2>&1) EOF # At this point, the address should be removed from the LAN-facing interface # and the hosts in LAN will soon receive an RA with prefix AdvValidLifetime and # AdvPreferredLifetime both set to zero. # # While the hosts will adjust the preferred lifetime accordingly, then the # valid lifetime update will be ignored(RFC 4862 Section 5.5.3 point e) and # this means that hosts can still be using addresses from the old prefix # for existing connections. # # While at least in newer kernels deleting an IPv6 address with a # non-forever valid lifetime does not remove the route from # the routing table, then it is likely that LAN hosts valid lifetime # is higher than the lifetime of the connected route in the router # because radvd messages are not in sync with dhclient events. # # So regardless whether the ISP keeps routing the packets destined to # addresses from the old prefix, then make sure, that this router is # not dropping those packets. Thus, update the connected route expiration # counter with the value of the old valid lifetime of the address. This # ensures that the connected route is present and it will expire after the # addresses on LAN hosts. if [ -n "$old_max_life" ] && [ "$old_max_life" -gt 0 ]; then ip -6 route replace "$old_ia_pd_addr" dev "$ia_pd_iface" \ expires "$old_max_life" >/dev/null 2>&1 fi # Inform the downstream clients, that the prefix is no longer valid. make_radvd_conf "$old_ia_pd_addr" 0 0 fi # Assign new prefix if [ -n "$new_ip6_prefix" ] && [ "$new_max_life" -ne 0 ]; then new_ia_pd_addr=$(find_ia_pd_addr "$new_ip6_prefix") # "ip -6 addr replace ..." adds the address if it's # missing or updates the valid and preferred lifetime # counters of the address if the address is already # configured. # Adding an IPv6 address with a non-forever valid lifetime # creates a temporary connected route which has "expires" # counter with a value equal to valid lifetime of the address. # At least in newer kernels deleting IPv6 address with a # non-forever valid lifetime does not remove the route from # the routing table. The route will expire automatically when # its expiration counter reaches 0. if ip -6 addr replace "$new_ia_pd_addr" \ ${new_max_life:+valid_lft "$new_max_life"} \ ${new_preferred_life:+preferred_lft "$new_preferred_life"} \ dev "$ia_pd_iface" >/dev/null 2>&1; then logger -t "ipv6-pd-br0" -p daemon.info \ "$reason - INFO: Added/updated address $new_ia_pd_addr on interface $ia_pd_iface." else logger -t "ipv6-pd-br0" -p daemon.err \ "$reason - ERROR: Failed to add/update address $new_ia_pd_addr on interface $ia_pd_iface." fi make_radvd_conf "$new_ia_pd_addr" "$new_preferred_life" "$new_max_life" fi # Handle DEPREF6 similarly to dhclient-script by configuring # the preferred lifetime of the IP from leased prefix to 0. # Do not change the valid lifetime. if [ "$reason" = "DEPREF6" ] && [ -n "$cur_ip6_prefix" ]; then cur_ia_pd_addr=$(find_ia_pd_addr "$cur_ip6_prefix") # Find the value of valid_lft. Unit of valid_lft is always "sec". cur_valid_life=$(ip -oneline -6 addr show dev "$ia_pd_iface" to "$cur_ia_pd_addr" 2>/dev/null) cur_valid_life="${cur_valid_life##*valid_lft }" cur_valid_life="${cur_valid_life%%sec *}" if [ -n "$cur_valid_life" ] && [ "$cur_valid_life" -gt 0 ]; then # If one does not specify the value for "valid_lft", then # it defaults to "forever". if ip -6 addr change "$cur_ia_pd_addr" \ dev "$ia_pd_iface" scope global \ valid_lft "$cur_valid_life" \ preferred_lft 0 >/dev/null 2>&1; then logger -t "ipv6-pd-br0" -p daemon.info \ "$reason - INFO: Changed the preferred lifetime of $cur_ia_pd_addr on interface $ia_pd_iface to 0." else logger -t "ipv6-pd-br0" -p daemon.err \ "$reason - ERROR: Failed to change the preferred lifetime of $cur_ia_pd_addr on interface $ia_pd_iface to 0." fi make_radvd_conf "$cur_ia_pd_addr" 0 "$cur_valid_life" else # Address was already removed or the valid lifetime was 0. # Ensure, that the radvd gets configured accordingly. make_radvd_conf "$cur_ia_pd_addr" 0 0 fi # Ensure that the connected route is present and it will expire after the # addresses on LAN hosts. if [ -n "$cur_max_life" ] && [ "$cur_max_life" -gt 0 ]; then ip -6 route replace "$cur_ia_pd_addr" dev "$ia_pd_iface" \ expires "$cur_max_life" >/dev/null 2>&1 fi fi fi fi ;; esac