From 83d72e63514a5173cf5f2971b615055730f17204 Mon Sep 17 00:00:00 2001 From: Michael Tremer Date: Sat, 31 Jan 2015 13:27:23 +0000 Subject: [PATCH] dhclient: Add support for DHCPv6 and PD --- src/dhclient-script | 113 +++++++++++++- src/functions/functions.constants | 5 + src/functions/functions.dhclient | 12 -- src/functions/functions.ipv6 | 246 ++++++++++++++++++++++++++++++ 4 files changed, 363 insertions(+), 13 deletions(-) diff --git a/src/dhclient-script b/src/dhclient-script index 16e765d9..5a6e8a39 100644 --- a/src/dhclient-script +++ b/src/dhclient-script @@ -15,6 +15,14 @@ assert device_exists ${interface} basename="$(basename $0)" log DEBUG "${basename} called for interface=${interface} reason=${reason}" +# Log all information from dhclient +if enabled DEBUG; then + while read line; do + [[ ${line} =~ ^(cur|old|new)_ ]] || continue + log DEBUG " ${line}" + done <<< "$(printenv | sort)" +fi + # Main pitchfork. case "${reason}" in MEDIUM) @@ -22,6 +30,106 @@ case "${reason}" in exit 0 ;; + # IPv6 + + PREINIT6) + if ! device_is_up "${interface}"; then + log WARNING "Device '${interface}' was not brought up before starting the DHCP client" + device_set_up "${interface}" + fi + + # Flush all other aborted IP addresses + ipv6_address_flush "${interface}" + + # Wait until DAD for the link-local address has finished + for address in $(ipv6_device_get_addresses "${interface}" --scope="link"); do + ipv6_wait_for_dad "${address}" "${interface}" && exit ${EXIT_OK} + done + + log ERROR "There is no active link-local address on ${interface}" + + exit ${EXIT_ERROR} + ;; + + BOUND6) + # We will be called twice. Once for the assigned address and once for an assigned prefix. + + # Handle temporarily-assigned address + if isset new_ip6_address && isset new_ip6_prefixlen; then + ipv6_address_add "${new_ip6_address}/${new_ip6_prefixlen}" "${interface}" \ + --valid-lifetime="${new_max_life}" --preferred-lifetime="${new_preferred_life}" + + # Save configuration + routing_db_set "${interface}" "ipv6" "local-ip-address" "${new_ip6_address}/${new_ip6_prefixlen}" + routing_db_set "${interface}" "ipv6" "active" 1 + #routing_db_set "${interface}" "ipv6" "domain-name" "${new_ + + exit 0 + + # Handle Prefix Delegation + elif isset new_ip6_prefix; then + routing_db_set "${interface}" "ipv6" "delegated-prefix" "${new_ip6_prefix}" + + exit 0 + fi + ;; + + RENEW6|REBIND6) + # Will be called twice like BOUND6. + + if isset new_ip6_address && isset new_ip6_prefixlen; then + # Update nameservers if those have changed + if [[ "${old_dhcp6_name_servers}" != "${new_dhcp6_name_servers}" ]]; then + routing_db_set "${interface}" "ipv6" "domain-name-servers" "${new_dhcp6_name_servers}" + dns_generate_resolvconf + fi + + # Update the lifetime if the address has not changed + if [[ "${old_ip6_address}" = "${new_ip6_address}" ]]; then + ipv6_address_change_lifetime "${new_ip6_address}/${new_ip6_prefixlen}" "${interface}" \ + --valid-lifetime="${new_max_life}" --preferred-lifetime="${new_preferred_life}" + + exit ${EXIT_OK} + fi + + # Remove any old IP addresses + if [ -n "${old_ip6_address}" ]; then + ipv6_address_del "${old_ip6_address}/${old_ip6_prefixlen}" "${interface}" + fi + + # Add the new one + ipv6_address_add "${new_ip6_address}/${new_ip6_prefixlen}" "${interface}" \ + --valid-lifetime="${new_max_life}" --preferred-lifetime="${new_preferred_life}" + + exit ${EXIT_OK} + + # Handle Prefix Delegation + elif isset new_ip6_prefix || isset old_ip6_prefix; then + if [[ "${old_ip6_prefix}" = "${new_ip6_prefix}" ]]; then + # TODO What do we need to do if the prefix hasn't changed? + exit ${EXIT_OK} + fi + + log DEBUG "The delegated prefix has changed from ${old_ip6_prefix} to ${new_ip6_prefix}" + routing_db_set "${interface}" "ipv6" "delegated-prefix" "${new_ip6_prefix}" + + exit ${EXIT_OK} + fi + ;; + + DEPREF6) + # Check if all necessary information is there + if ! isset cur_ip6_address || ! isset cur_ip6_prefixlen; then + exit ${EXIT_ERROR} + fi + + # Set lifetime to zero + ipv6_address_change_lifetime "${cur_ip6_address}/${cur_ip6_prefixlen}" "${interface}" \ + --preferred-lifetime=0 || exit ${EXIT_ERROR} + ;; + + # IPv4 + PREINIT) # Bring up the device if it hasn't been done before. if ! device_is_up ${interface}; then @@ -63,7 +171,6 @@ case "${reason}" in exit 0 ;; - BOUND|RENEW|REBIND|REBOOT) # Check if the IP address has changed. If so, delete all routes and stuff. if [ -n "${old_ip_address}" -a "${old_ip_address}" != "${new_ip_address}" ]; then @@ -127,6 +234,10 @@ case "${reason}" in exit 0 ;; + + *) + log ERROR "Unhandled reason: ${reason}" + ;; esac exit 1 diff --git a/src/functions/functions.constants b/src/functions/functions.constants index 6c11c4ac..4233f76f 100644 --- a/src/functions/functions.constants +++ b/src/functions/functions.constants @@ -60,6 +60,11 @@ EXIT_TRUE=0 EXIT_FALSE=1 EXIT_UNKNOWN=2 +# Exit codes for IPv6 duplicate address detection (DAD) +EXIT_DAD_OK=0 +EXIT_DAD_FAILED=8 +EXIT_DAD_TENTATIVE=8 + STATUS_UP=0 STATUS_DOWN=1 STATUS_NOCARRIER=2 diff --git a/src/functions/functions.dhclient b/src/functions/functions.dhclient index 29a6d664..40cc61c5 100644 --- a/src/functions/functions.dhclient +++ b/src/functions/functions.dhclient @@ -82,7 +82,6 @@ dhclient_write_config() { assert isset file local hostname=${HOSTNAME%%.*} - local prefix_delegation="false" local vendor=$(distro_get_pretty_name) while [ $# -gt 0 ]; do @@ -90,9 +89,6 @@ dhclient_write_config() { --hostname=*) hostname=$(cli_get_val ${1}) ;; - --prefix-delegation=*) - prefix_delegation="$(cli_get_bool "${1}")" - ;; --vendor=*) vendor=$(cli_get_val ${1}) ;; @@ -131,14 +127,6 @@ dhclient_write_config() { print fi - # Prefix delegation (IPv6). - if enabled prefix_delegation; then - print " # Prefix delegation" - print " also request dhcp6.ia-pd 1;" - print " send dhcp6.ia-na 1;" - print - fi - echo "}" ) >>${file} diff --git a/src/functions/functions.ipv6 b/src/functions/functions.ipv6 index 2667ae0b..32ba81ae 100644 --- a/src/functions/functions.ipv6 +++ b/src/functions/functions.ipv6 @@ -83,6 +83,252 @@ function ipv6_split_prefix() { ip_split_prefix "$@" } +function ipv6_address_add() { + local address="${1}" + assert isset address + + local device="${2}" + assert device_exists "${device}" + shift 2 + + local scope="global" + local preferred_lft valid_lft + + # Enable to wait until DAD has finished and return + # an error if it has failed + local wait_for_dad="true" + + local arg + while read arg; do + case "${arg}" in + --preferred-lifetime=*) + preferred_lft="$(cli_get_val "${arg}")" + ;; + --valid-lifetime=*) + valid_lft="$(cli_get_val "${arg}")" + ;; + --no-wait-for-dad) + wait_for_dad="false" + ;; + esac + done <<< "$(args $@)" + + local cmd="ip addr add ${address} dev ${device} scope ${scope}" + + # Preferred lifetime + if isinteger preferred_lft; then + list_append cmd "preferred_lft ${preferred_lft}" + fi + + # Valid lifetime + if isinteger valid_lft; then + list_append cmd "valid_lft ${valid_lft}" + fi + + cmd_quiet "${cmd}" || return ${EXIT_ERROR} + + if enabled wait_for_dad; then + log DEBUG "Waiting for DAD to complete..." + + ipv6_wait_for_dad "${address}" "${device}" + local ret="${?}" + + case "${ret}" in + # DAD OK + ${EXIT_DAD_OK}) + log DEBUG "DAD successfully completed" + return ${EXIT_OK} + ;; + + # DAD failed + ${EXIT_DAD_FAILED}) + log ERROR "DAD failed" + + # Remove the IP address again + ipv6_address_del "${address}" "${device}" + + return ${EXIT_ERROR} + ;; + + # Any unknown errors + *) + log ERROR "DAD failed with unhandled error: ${ret}" + return ${EXIT_ERROR} + ;; + esac + fi + + return ${EXIT_OK} +} + +function ipv6_address_del() { + local address="${1}" + local device="${2}" + + ip_address_del "${device}" "${address}" +} + +function ipv6_address_flush() { + local device="${1}" + assert isset device + + log DEBUG "Flushing all IPv6 addresses on ${device}" + + # Remove any stale addresses from aborted clients + cmd_quiet ip -6 addr flush dev "${device}" scope global permanent +} + +function ipv6_address_change_lifetime() { + local address="${1}" + assert isset address + + local device="${2}" + assert device_exists "${device}" + shift 2 + + local preferred_lft + local valid_lft + + local arg + while read arg; do + case "${arg}" in + --preferred-lifetime=*) + preferred_lft="$(cli_get_val "${arg}")" + ;; + --valid-lifetime=*) + valid_lft="$(cli_get_val "${arg}")" + ;; + esac + done <<< "$(args $@)" + + local cmd="ip -6 addr change ${address} dev ${device} scope global" + + if isinteger preferred_lft; then + list_append cmd "preferred_lft" "${preferred_lft}" + fi + + if isinteger valid_lft; then + list_append cmd "valid_lft" "${valid_lft}" + fi + + if ! cmd_quiet "${cmd}"; then + log ERROR "Could not change lifetimes of ${address} (${device})" + return ${EXIT_ERROR} + fi + + log DEBUG "Changed lifetimes of ${address} (${device}) to:" + if isset preferred_lft; then + log DEBUG " preferred: ${preferred_lft}" + fi + + if isset valid_lft; then + log DEBUG " valid: ${valid_lft}" + fi + + return ${EXIT_OK} +} + +function ipv6_get_dad_status() { + local address="${1}" + assert isset address + + local device="${2}" + assert isset device + + # Strip prefix from address + address="$(ipv6_split_prefix "${address}")" + + local output="$(ip -o addr show dev "${device}" to "${address}")" + if ! isset output; then + return ${EXIT_ERROR} + fi + + # Abort if DAD failed + if [[ ${output} =~ "dadfailed" ]]; then + return ${EXIT_DAD_FAILED} + fi + + # Wait a little more if DAD is still in progress + if [[ ${output} =~ "tentative" ]]; then + return ${EXIT_DAD_TENTATIVE} + fi + + # DAD has successfully completed + return ${EXIT_DAD_OK} +} + +function ipv6_wait_for_dad() { + local address="${1}" + assert isset address + + local device="${2}" + assert isset device + + # Strip prefix from address + address="$(ipv6_split_prefix "${address}")" + + local i + for i in {0..10}; do + # Check DAD status + ipv6_get_dad_status "${address}" "${interface}" + local ret="${?}" + + case "${ret}" in + # DAD is still in progress. Give it a moment to settle... + ${EXIT_DAD_TENTATIVE}) + sleep 0.5 + continue + ;; + + # Raise all other error codes + ${EXIT_DAD_OK}|${EXIT_DAD_FAILED}|*) + return ${ret} + ;; + esac + done + + return ${EXIT_ERROR} +} + +function ipv6_device_get_addresses() { + local device="${1}" + assert isset device + shift + + local scope + + local arg + while read arg; do + case "${arg}" in + --scope=*) + scope="$(cli_get_val "${arg}")" + ;; + esac + done <<< "$(args $@)" + + local cmd="ip -o addr show dev ${device}" + if isset scope; then + assert isoneof scope global dynamic link + list_append cmd "scope ${scope}" + fi + + local addresses + local line args + while read line; do + args=( ${line} ) + + local i + for (( i=0; i < ${#args[@]} - 1; i++ )); do + if [ "${args[${i}]}" = "inet6" ]; then + list_append_one addresses "${args[$(( ${i} + 1 ))]}" + break + fi + done + done <<< "$(${cmd})" + + list_sort ${addresses} +} + function ipv6_implode() { local address=${1} assert isset address -- 2.39.2