Prefix Delegation Hook Script
Timo Sigurdsson
Tue Jan 22 01:45:02 2019
Hi Roy,
Hey List,
I finally found some time to clean up my prefix delegation hook script and test it in different environments/setups, so I feel confident enough it might actually be useful for someone else than me ;) You can find the script attached.
Motivation:
Just as a reminder, the motivation behind my script was, that I need to track changes of a dynamically assigned IPv6 prefix and all of its derived subnets which dhcpcd assigns to downstream interfaces in order to make configuration changes and restart services that depend on the IPv6 addressing information. While dhcpcd-run-hooks provides this information, it's not "in one place" or during a single event. You'd rather need to handle multiple sequential events (BOUND6, DELEGATED6, etc.) which means that you might end up restarting services multiple times in a row (depending on the number of subnets you assign). My script tries to optimize that, i.e. avoid restarting services repeatedly, if possible.
How it works:
Basically, when a prefix is obtained and dhcpcd-run-hooks executes with the reason BOUND6, dhcpcd will have configured all the subnet interfaces already (i.e. before the DELEGATED6 event is triggered for each interface). Therefore, the hook script does the following:
1) Check if the prefix is actually new (when the prefix is simply renewed and was already configured before, it does nothing).
2) If the prefix is new, it iterates over all the other network interfaces and checks whether the interface has an IPv6 addresses that belongs to the new prefix. If yes, it will write it to a lease file to keep track of it.
3) For each new prefix and for each subnet address that was identified, it allows to run user-defined configuration actions, e.g. adding a firewall rule for the new prefix or add a new subnet to the configuration of the RA daemon, etc.
4) After the prefix has been processed it will restart or reload user-defined services, such as the firewall, etc.
5) When dhcpcd-run-hooks runs with the reason DELEGATED6, the script will check whether it has seen this subnet before by checking the lease file. In most cases, it will do nothing, because the subnet address is already in the lease file. This means the prefix and all interface addresses were processed in one go and services are restarted once only.
6) As with every "rule", there's an exception: If one of the subnet interfaces wasn't ready at the time the prefix was obtained (e.g. no cable plugged in), then dhcpcd skips the configuration of this interface and triggers the configuration of the interface at a later point when the interface comes up. So, when dhcpcd-run-hooks is executed with reason DELEGATED6 and the script finds a subnet address it hasn't seen before, it will perform the user-defined configuration action for this subnet and restart the user-defined services. So, even this corner case isn't missed.
7) When a prefix expires, the script will look at the lease files and check which subnet addresses belong to that prefix and run user-defined configuration actions for the prefix and each subnet address, e.g. removing a firewall rule, etc. Then the user-defined services are restarted/reloaded again and the prefix and subnets are removed from the lease files.
A few more remarks:
First, don't be put off by the size/length of the script. It seems long, but a lot of it is just comments. The extensive comments should also make it easier to understand and customize the script. After all, it's up to the user to decide which services need to be restarted, etc. The customizable part is all in the top, so it's not necessary to read through the actual code if you "just want to use" the hook. The code itself also deals with a few corner cases which make it larger. But I prefer reliability over simplicity and size. I also tried to avoid external dependencies wherever it seemed reasonable to me, which is why I wrote some functions that could as well be provided by external tools (e.g. a function to test if an IPv6 address is in the range of a prefix). The few requirements are listed in the header of the script.
There are a few technical limitations that are mentioned in the header of the script as well, but in real operation, they should rarely come into play or be really limiting.
@Roy: I still have two questions regarding cases where I'm not sure if the script could or should be improved:
a) The script currently checks for new prefixes during the BOUND6, RENEW6, REBIND6 and REBOOT6 run reasons. But is it actually possible to get a different prefix during RENEW6 or REBIND6? If not, these two reasons could be dropped from the case statement.
b) Currently, The scripts checks the variables new_dhcp6_ia_pd1_prefix1 to new_dhcp6_ia_pd1_prefix5. But is it possible that the part _pd1_ can also change to _pd2_ and so on? During my testing with different servers, even two DHCPv6 servers on the same network, I never managed to get anything else than _pd1_. But if that is actually variable, I'd change the script to check variations of this, too. It would be very simple to to, as it's just another loop around the current code.
I have a few more questions on issues I found during my testing, but I'll send one or two separate emails for these.
Cheers,
Timo
# This shell script handles changes of delegated DHCPv6 prefixes (IA_PD) and allows to perform custom actions whenever the prefix changes.
# It is designed to do everything "in one run", if possible, in order to avoid repeated restarts/reloads of dependent services, etc.
# USAGE:
# This script is designed to be sourced from dhcpcd-run-hooks (dhcpcd).
# Place it either inside the dhcpcd-hooks directory or save it as /etc/dhcpcd.exit-hook.
# For this script to be useful, adjust the customizable functions in the section "CUSTOMIZABLE FUNCTIONS" below.
# By default, this script only logs errors to syslog. If dhcpcd's debug option is enabled, debugging information is logged as well.
# LIMITATIONS:
# 1) This script only works with global unicast (GUA) or unique local unicast (ULA) prefixes of a length between 48-64 bits.
# 2) It only checks the first five prefixes in a DHCPv6 message.
# 3) This script checks if a prefix stored in a lease file is orphaned, i.e. its expiration went unnoticed for some reason (uncommon, yet possible, e.g. during a power outage).
# A prefix is considered orphaned if no interface has an address assigned that matches the range of the prefix and if there is no route for the prefix.
# In such a case an error message will be logged (at the log level error), but no further action is taken.
# REQUIREMENTS:
# This script avoids "bashisms" and has been tested to work with dash (Debian Almquist Shell), so it should be fairly portable.
# On top of that, a few GNU utilities are used: cp, cut, grep, mkdir, rm, sed
# As this script was written and tested on Linux, the "ip" command (iproute2) is used to retrieve the IPv6 addresses of network interfaces matching a given prefix.
# It would be possible, though, to replace the ip command in this script with e.g. ifconfig and use the function fn_address_in_prefix_range in order to check whether the address matches the prefix.
# CONFIGURATION VARIABLES:
#
# Define a directory for custom lease files (this script will put files there to keep track of prefix changes). Best not to mix this up with where dhcpcd puts its lease files.
# Using a temporary file system, ensures that a prefix that gets reassigned after a reboot is actually considered new and will trigger the function fn_custom_service_actions.
# The detection of orphaned leases, however, only works across reboots if this directory is placed on a non-temporary file system.
# Defaults to /run/dhcpcd-run-hooks if undefined. The directory will be created if it doesn't exist. Omit the trailing slash.
IA_PD_LEASE_DIR="/run/dhcpcd-run-hooks"
# Choose whether the function fn_custom_service_actions (see below) should be skipped if the interface is down when a lease expired or dhcpcd stops.
# This may be useful e.g. to avoid restarting/reloading services when there's no internet connectivity.
# Set this to 1 in order to skip fn_custom_service_actions if interface is down.
IA_PD_SKIP_SERVICE_ACTIONS_WHEN_DOWN=0
# CUSTOMIZABLE FUNCTIONS:
#
# The following five functions allow performing custom actions after a prefix or subnet changed.
# The simplest use would be to restart or reload a service that depends on the changed IPv6 address information, e.g. the firewall.
# In this case, only the first function needs to be customized and the rest can be kept as is.
# The other four functions allow configuration changes per each prefix and subnet, e.g. adding/removing a firewall rule for the changed prefix.
# The actions are only performed if the lease actually changed. When the lease is merely renewed and matches the previously configured addresses, nothing is done.
#
# Nomenclature:
# The term prefix in this context refers to the IPv6 prefix received via DHCPv6 in the IA_PD option, e.g.: 2001:db8:8bd::/48
# The term subnet refers to a subnet derived from the prefix which dhcpcd assigns to another interface, e.g.: 2001:db8:8bd:1::1/64
# fn_custom_service_actions is called after new prefixes or subnets were configured or old ones expired.
# Customize it to restart or reload any system service that depends on the prefix/subnet information.
# This function will always be called after the other four custom functions (which may alter a service's configuration)!
fn_custom_service_actions() {
# dhcpcd-run-hooks provides a built-in function "service" that can be used to control a service.
# However, any other command may be used as well.
#
# Examples (uncomment and adjust the examples or put your own commands here):
#service firewall restart
#service ipv6-ra-daemon reload
# Keep the last line, so the function doesn't fail if no customizations were made.
[ "$syslog_debug" = "true" ] && syslog debug "Triggered function: fn_custom_service_actions" || true
}
# fn_custom_config_new_prefix is called after a new prefix has been leased.
# Customize it to make configuration changes to services that depend on the prefix information.
# If multiple prefixes are obtained at the same time (e.g. GUA+ULA), the function is called once per prefix!
fn_custom_config_new_prefix() {
# The variable $1 holds the new prefix including the length, e.g.: 2001:db8:8bd::/48
# The variable $2 holds the name of the interface which requested/received the prefix, e.g.: eth0
#
# Example (put your own commands here):
#/usr/local/bin/my_firewall_helper add_rule "$1" "$2"
# Keep the last line, so the function doesn't fail if no customizations were made.
[ "$syslog_debug" = "true" ] && syslog debug "Triggered function: fn_custom_config_new_prefix $1 $2" || true
}
# fn_custom_config_expired_prefix is called after an old prefix has expired.
# Customize it to make configuration changes to services that depend on the prefix information.
# Usually, this means to undo what fn_custon_new_prefix_config did before.
# If multiple prefixes are obtained at the same time (e.g. GUA+ULA), the function is called once per prefix!
fn_custom_config_expired_prefix() {
# The variable $1 holds the expired prefix including the length, e.g.: 2001:db8:8bd::/48
# The variable $2 holds the name of the interface which previously requested/received the expired prefix, e.g.: eth0
#
# Example (put your own commands here):
#/usr/local/bin/my_firewall_helper remove_rule "$1" "$2"
# Keep the last line, so the function doesn't fail if no customizations were made.
[ "$syslog_debug" = "true" ] && syslog debug "Triggered function: fn_custom_config_expired_prefix $1 $2" || true
}
# fn_custom_config_new_subnet is called after a new subnet has been assigned to an interface.
# Customize it to make configuration changes to services that depend on the subnet information.
# If subnets are delegated to multiple interfaces, the function is called once per subnet!
fn_custom_config_new_subnet() {
# The variable $1 holds the new subnet including the length, e.g.: 2001:db8:8bd:1::1/64
# The variable $2 holds the name of the interface to which the new subnet was assigned, e.g.: eth0
#
# Example (put your own commands here):
#/usr/local/bin/my_ipv6-ra-daemon_helper add_subnet "$1" "$2"
# Keep the last line, so the function doesn't fail if no customizations were made.
[ "$syslog_debug" = "true" ] && syslog debug "Triggered function: fn_custom_config_new_subnet $1 $2" || true
}
# fn_custom_config_expired_subnet is called after an old subnet that was assigned to an interface expired.
# Customize it to make configuration changes to services that depend on the prefix information.
# Usually, this means to undo what fn_custon_new_subnet_config did before.
# If subnets are delegated to multiple interfaces, the function is called once per subnet!
fn_custom_config_expired_subnet() {
# The variable $1 holds the expired subnet including the length, e.g.: 2001:db8:8bd:1::1/64
# The variable $2 holds the name of the interface to which the expired subnet was assigned, e.g.: eth0
#
# Example (put your own commands here):
# /usr/local/bin/my_ipv6-ra-daemon_helper remove_subnet "$1" "$2"
# Keep the last line, so the function doesn't fail if no customizations were made.
[ "$syslog_debug" = "true" ] && syslog debug "Triggered function: fn_custom_config_expired_subnet $1 $2" || true
}
#
# FROM HERE ON, NO CUSTOMIZATION SHOULD BE NECESSARY
# __________________________________________________
#
fn_new_lease() {
# Test if the lease is new (it's considered new if it's not already stored in the lease file for the interface)
# If the lease is new, it's written to the lease file for the interface
# The first argument is the lease to test
# The second argument is the interface
# Returns 0 if lease is new and 1 if it's already processed (or empty)
[ -n "$1" ] && [ -n "$2" ] || return 1
local lease_file
if [ "$reason" = "DELEGATED6" ] || [ "$interface" != "$2" ]; then
lease_file="${IA_PD_LEASE_DIR}/${ia_pd_common_name}-subnet.${2}"
else
lease_file="${IA_PD_LEASE_DIR}/${ia_pd_common_name}-prefix.${2}"
fi
if [ ! -d "$IA_PD_LEASE_DIR" ] && ! mkdir -p "$IA_PD_LEASE_DIR" ; then
syslog err "Error: Failed to create directory ${IA_PD_LEASE_DIR}. This error is fatal!"
return 1
elif [ ! -w "$IA_PD_LEASE_DIR" ]; then
syslog err "Error: Directory ${IA_PD_LEASE_DIR} is not writable. This error is fatal!"
return 1
fi
if [ ! -s "$lease_file" ] || [ "$(grep -c "^${1}\$" "$lease_file")" -lt 1 ]; then
# Write new prefix to lease file
echo "$1" >> "$lease_file"
return 0
fi
return 1
}
fn_expired_lease() {
# Test if the lease is expired (it's expired if it's still stored in the lease file for the interface)
# If the lease is expired, it's removed from the lease file for the interface
# The first argument is the lease to test
# Returns 0 if lease is expired and 1 if it's already processed (or empty)
[ -n "$1" ] || return 1
local lease_file
lease_file="${IA_PD_LEASE_DIR}/${ia_pd_common_name}-prefix.${interface}"
if [ -s "$lease_file" ] && [ "$(grep -c "^${1}\$" "$lease_file")" -ne 0 ]; then
# Delete prefix from lease file
sed -i "\\#^${1}\$#d" "$lease_file"
return 0
fi
return 1
}
fn_valid_prefix() {
# Validate the prefix to be a global unicast or unique local unicast prefix with a length between 48 and 64 bits
# The first argument is the prefix and the second argument the length
# Returns 0 if the prefix is valid
[ -n "$1" ] && [ -n "$2" ] || return 1
if [ "$2" -lt "48" ] || [ "$2" -gt "64" ] || (! echo "$1" | grep -qiE '^([23][0-9a-f]|f[cd])[0-9a-f]{2}(:[0-9a-f]{1,4}){0,3}::$') ; then
return 1
fi
return 0
}
fn_normalize_ipv6() {
# Expand empty (zero) hextets in an IPv6 prefix or address, lowercase output and remove superfluous zeros in hextets
# The first argument is the prefix or address that should be expanded
# This function assumes that the input was validated before
[ -n "$1" ] || return 1
local colons omitted_zeros insert_zeros normalized_ipv6
if echo "$1" | grep -qs '::' ; then
colons="$(echo "$1" | sed 's/[^:]//g')"
omitted_zeros="$(echo '::::::::' | sed "s/${colons}//")"
insert_zeros="$(echo "$omitted_zeros" | sed 's/:/0:/g')"
# Append another zero if the prefix/address ends with ::
if echo "$1" | grep -qsE '::(/|$)' ; then
insert_zeros="${insert_zeros}0"
fi
normalized_ipv6="$(echo "$1" | sed "s/::/:${insert_zeros}/")"
else
normalized_ipv6="$1"
fi
echo "$normalized_ipv6" | sed -E 's/:0{1,3}([^:/])/:\1/g' | sed -E 's/[A-F]/\L&/g'
}
fn_address_in_prefix_range() {
# Check if an IPv6 address is in the range of a prefix
# The first argument is the address and the second argument the prefix
# Returns 0 if the address is in the range of the prefix
[ -n "$1" ] && [ -n "$2" ] || return 1
local address_length prefix_length normalized_address normalized_prefix address_value address_value_dec \
prefix_value prefix_value_dec max_subnets prefix_range_start prefix_range_end
# Compare the length of the address and prefix first
address_length="$(echo "$1" | cut -s -d'/' -f2)"
prefix_length="$(echo "$2" | cut -s -d'/' -f2)"
([ -z "$address_length" ] || [ -z "$prefix_length" ] || [ "$address_length" -lt "$prefix_length" ]) && return 1
# Normalize input to allow for a simpler comparison
normalized_address="$(fn_normalize_ipv6 "$1")"
[ -z "$normalized_address" ] && return 1
normalized_prefix="$(fn_normalize_ipv6 "$2")"
[ -z "$normalized_prefix" ] && return 1
# Compare the first three hexadecimal groups
if [ "$(echo "$normalized_address" | cut -d':' -f1-3)" = "$(echo "$normalized_prefix" | cut -d':' -f1-3)" ]; then
# Get decimal values of the fourth hexadecimal groups
address_value="$(echo "$normalized_address" | cut -d':' -f4)"
address_value_dec="$(printf '%d' "0x${address_value}")"
prefix_value="$(echo "$normalized_prefix" | cut -d':' -f4)"
prefix_value_dec="$(printf '%d' "0x${prefix_value}")"
# Calculate the decimal range of the prefix
max_subnets="$((1<<(64-prefix_length)))"
prefix_range_start="$(( (prefix_value_dec/max_subnets)*max_subnets ))"
prefix_range_end="$((prefix_range_start+max_subnets-1))"
# Return 0 if the address is in the range of the prefix
if [ "$address_value_dec" -ge "$prefix_range_start" ] && [ "$address_value_dec" -le "$prefix_range_end" ]; then
return 0
else
return 1
fi
else
return 1
fi
}
fn_determine_new_subnets() {
# Determine which interfaces got assigned subnets from the delegated prefix
# The first argument is the prefix and the second argument the length
[ -n "$1" ] && [ -n "$2" ] || return 1
if ! fn_valid_prefix "$1" "$2" ; then
# NOTE: A failure of this function isn't critical, since the subnets will be detected in the DELEGATED6 events, too,
# but it will lead to the function fn_custom_service_actions being triggered more often than necessary.
syslog err "Error: Prefix ${1}/${2} either isn't a global or unique local unicast address or has an unsuitable length."
return 1
fi
local subnet_interface subnet_address
for subnet_interface in $interface_order ; do
([ "$subnet_interface" = "$interface" ] || [ "$subnet_interface" = "lo" ]) && continue
# Test if the interface address is a subnet of the prefix
# Limit grep to one match as there should only be one address per prefix on the interface
# The regular expression doesn't need to be airtight here since ip should only return valid IPv6 addresses anyway
subnet_address="$(ip -6 -o addr show dev "$subnet_interface" scope global to "${1}/${2}" 2>/dev/null | grep -m1 -oiP '(?<=[[:blank:]])([23][0-9a-f]|f[cd])[0-9a-f]{2}(:{1,2}[0-9a-f]{0,4}){1,7}/[1-9][0-9]{1,2}(?=[[:blank:]])')"
if [ "$subnet_address" ]; then
[ "$syslog_debug" = "true" ] && syslog debug "Delegated new subnet $subnet_address to interface ${subnet_interface}."
fn_new_lease "$subnet_address" "$subnet_interface"
fn_custom_config_new_subnet "$subnet_address" "$subnet_interface"
fi
done
}
fn_determine_expired_subnets() {
# Determine which interfaces had assigned subnets from the expired delegated prefix
# The first argument is the prefix and the second argument the length
[ -n "$1" ] && [ -n "$2" ] || return 1
if ! fn_valid_prefix "$1" "$2" ; then
# NOTE: A failure of this function isn't necessarily critical, but it means that the function fn_custom_config_expired_subnet
# isn't triggered and that expired subnets possibly remain in any configuration changed by fn_custom_config_new_subnet.
syslog err "Error: Prefix ${1}/${2} either isn't a global or unique local unicast address or has an unsuitable length."
return 1
fi
local subnet_interface lease_file subnet_address
for subnet_interface in $interface_order ; do
([ "$subnet_interface" = "$interface" ] || [ "$subnet_interface" = "lo" ]) && continue
lease_file="${IA_PD_LEASE_DIR}/${ia_pd_common_name}-subnet.${subnet_interface}"
if [ -s "$lease_file" ]; then
# Read the ip addresses from the interface's subnet lease file and test if the address is a subnet of the expired prefix
# Make a temporary copy of the file in order to not read from and write to the file at the same time
cp "$lease_file" "${lease_file}.tmp"
while IFS= read -r subnet_address ; do
if fn_address_in_prefix_range "$subnet_address" "${1}/${2}" ; then
[ "$syslog_debug" = "true" ] && syslog debug "Delegated subnet $subnet_address on interface $subnet_interface expired."
# Remove the address from the lease file
sed -i "\\#^${subnet_address}\$#d" "$lease_file"
fn_custom_config_expired_subnet "$subnet_address" "$subnet_interface"
# There shouldn't be another subnet of the same prefix on this interface, so stop here
break
fi
done < "${lease_file}.tmp"
rm "${lease_file}.tmp"
fi
done
}
fn_orphaned_leases() {
# Check if the lease file contains orphaned leases
local lease_file leased_prefix
lease_file="${IA_PD_LEASE_DIR}/${ia_pd_common_name}-prefix.${interface}"
if [ -s "$lease_file" ]; then
while IFS= read -r leased_prefix ; do
# If the ip command returns nothing here, it means the prefix is not in use anymore (not assigned to any interface and no route for it)
if ! ip -6 -o addr show scope global to "$leased_prefix" 2>/dev/null | grep -q ':' && ! ip -6 -o route show to "$leased_prefix" 2>/dev/null | grep -q ':' ; then
syslog err "Error: The prefix $leased_prefix seems orphaned! No interface address or route matches the prefix! Remove the prefix and its subnets from the lease files and any service's configuration they might have been added to."
fi
done < "$lease_file"
fi
}
[ -z "$IA_PD_LEASE_DIR" ] || IA_PD_LEASE_DIR="/run/dhcpcd-run-hooks"
# Files in the lease directory will start with this string
ia_pd_common_name="dhcpv6"
ia_pd_trigger_service_actions=0
case "$reason" in
BOUND6|RENEW6|REBIND6|REBOOT6)
# Check for orphaned prefixes, before processing new ones (just in case an expiration event was somehow missed)
fn_orphaned_leases
for ia_pd_prefix_no in 1 2 3 4 5; do
eval "ia_pd_prefix=\$new_dhcp6_ia_pd1_prefix${ia_pd_prefix_no}"
eval "ia_pd_prefix_length=\$new_dhcp6_ia_pd1_prefix${ia_pd_prefix_no}_length"
# Ignore the prefix if it's all zeros (::)
if [ -n "$ia_pd_prefix" ] && [ "$ia_pd_prefix" != "::" ] && [ -n "$ia_pd_prefix_length" ] && fn_new_lease "${ia_pd_prefix}/${ia_pd_prefix_length}" "$interface" ; then
[ "$syslog_debug" = "true" ] && syslog debug "Received new prefix ${ia_pd_prefix}/${ia_pd_prefix_length}."
ia_pd_trigger_service_actions=1
fn_custom_config_new_prefix "${ia_pd_prefix}/${ia_pd_prefix_length}" "$interface"
fn_determine_new_subnets "$ia_pd_prefix" "$ia_pd_prefix_length"
fi
done
[ "$ia_pd_trigger_service_actions" -eq 1 ] && fn_custom_service_actions
;;
DELEGATED6)
# Usually, nothing is done here, since all the magic happens in the BOUND6|RENEW6|REBIND6|REBOOT6 events already
# But if an interface wasn't ready when the lease was obtained, the delegation will happen when the interface comes up later, so take care of this here
if [ -n "$new_delegated_dhcp6_prefix" ]; then
for ia_pd_delegated_subnet in $new_delegated_dhcp6_prefix ; do
if fn_new_lease "$ia_pd_delegated_subnet" "$interface" ; then
[ "$syslog_debug" = "true" ] && syslog debug "Delegated new subnet $ia_pd_delegated_subnet to interface ${interface}."
ia_pd_trigger_service_actions=1
fn_custom_config_new_subnet "$ia_pd_delegated_subnet" "$interface"
fi
done
[ "$ia_pd_trigger_service_actions" -eq 1 ] && fn_custom_service_actions
fi
;;
EXPIRE6|STOP6)
for ia_pd_prefix_no in 1 2 3 4 5; do
eval "ia_pd_prefix=\$old_dhcp6_ia_pd1_prefix${ia_pd_prefix_no}"
eval "ia_pd_prefix_length=\$old_dhcp6_ia_pd1_prefix${ia_pd_prefix_no}_length"
if [ -n "$ia_pd_prefix" ] && [ -n "$ia_pd_prefix_length" ] && fn_expired_lease "${ia_pd_prefix}/${ia_pd_prefix_length}" ; then
[ "$syslog_debug" = "true" ] && syslog debug "Old prefix ${ia_pd_prefix}/${ia_pd_prefix_length} expired."
ia_pd_trigger_service_actions=1
fn_custom_config_expired_prefix "${ia_pd_prefix}/${ia_pd_prefix_length}" "$interface"
fn_determine_expired_subnets "$ia_pd_prefix" "$ia_pd_prefix_length"
fi
done
# Trigger fn_custom_service_actions when a lease expired (that hasn't been processed before) or when dhcpcd stops
# But don't do anything if the interface is down and the option IA_PD_SKIP_SERVICE_ACTIONS_WHEN_DOWN is enabled
if ([ "$ia_pd_trigger_service_actions" -eq 1 ] || [ "$reason" = "STOP6" ]) && ([ "$if_up" = "true" ] || [ "$IA_PD_SKIP_SERVICE_ACTIONS_WHEN_DOWN" != 1 ]); then
fn_custom_service_actions
fi
;;
esac
# Always return true (the last operation may have been an if-clause with a condition that evaluated to false)
true
Archive administrator: postmaster@marples.name