Page MenuHomeVyOS Platform

dhcp6c doesn't update pltime and vltime for dynamic ipv6 prefixes
Open, NormalPublic

Description

When a dynamic prefix is delegated, dhcp6c use invalid pltime and vltime :

dhcp6c log extract:

dhcp6_get_options: get DHCP option IA_PD, len 54
  IA_PD: ID=0, T1=86400, T2=138240
copyin_option: get DHCP option IA_PD prefix, len 25
copyin_option:   IA_PD prefix: 2001:xxx:xxxx:xxxx::/56 pltime=172800 vltime=259200
copyin_option: get DHCP option status code, len 9
  status code: success
...
update_prefix: create a prefix 2001:xxx:xxxx:xxxx::/56 pltime=94613834736384, vltime=94613834822784

Thus it cannot renew the prefix (or even renew/rebind the lease) upon lease timeout.

Also, regarding pltime and vltime, dhcp6c should honor the exact value of T1 and T2 (not T x 2 nor 94218697831552, witch seems to be a u32 overflow) according to the RFC 84115: https://www.rfc-editor.org/rfc/rfc8415#section-6.3:~:text=7.7.%20%20Representation%20of%20Time%20Values%20and%20%22Infinity%22%20as%20a%20Time%20Value

pltime and vltime are not updated radvd side either.

Details

Version
2025.09.07-0020-rolling
Is it a breaking change?
Behavior change
Issue type
Bug (incorrect behavior)

Event Timeline

nvdx created this object in space S1 VyOS Public.

Upon further investigation, dhcp6c doesn't even reset timers according to pltime !

Edited for handling T1 (renew) and T2 (rebind) value from dhcpv6 reply and set systemd timers accordingly, adjusted radvd config hints for syncing pltime and vltime

A fix would involve changing the following templates :

  • /usr/share/vyos/templates/dhcp-client/ipv6.override.conf.j2
{% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' ' if vrf is vyos_defined else '' %}
{% set no_release = '-n' if dhcpv6_options.no_release is vyos_defined else '' %}
{% set dhcp6c_options = '-D -k ' ~ dhcp6_client_dir ~ '/dhcp6c.' ~ ifname ~ '.sock -c ' ~ dhcp6_client_dir ~ '/dhcp6c.' ~ ifname ~ '.conf -p ' ~ dhcp6_client_dir ~ '/dhcp6c.' ~ ifname ~ '.pid ' ~ no_release %}

[Unit]
ConditionPathExists={{ dhcp6_client_dir }}/dhcp6c.{{ ifname }}.conf
{% if ifname.startswith('pppoe') %}
After=ppp@{{ ifname }}.service
{% endif %}

[Service]
ExecStartPre=cp /etc/wide-dhcpv6/dhcp6cctlkey {{ dhcp6_client_dir }}/dhcp6c.{{ ifname }}.sock
ExecStart=
ExecStart={{ vrf_command }}/usr/sbin/dhcp6c {{ dhcp6c_options }} {{ ifname }}
WorkingDirectory={{ dhcp6_client_dir }}
PIDFile={{ dhcp6_client_dir }}/dhcp6c.{{ ifname }}.pid
  • /usr/share/vyos/templates/dhcp-client/dhcp6c-script.j2
#!/usr/bin/env bash
# Update DNS information for DHCPv6 clients
# should be used only if vyos-hostsd is running

if /usr/bin/systemctl -q is-active vyos-hostsd; then
    hostsd_client="/usr/bin/vyos-hostsd-client"
    hostsd_changes=

    if [ -n "$new_domain_name" ]; then
        logmsg info "Deleting search domains with tag \"dhcpv6-{{ ifname }}\" via vyos-hostsd-client"
        $hostsd_client --delete-search-domains --tag "dhcpv6-{{ ifname }}"
        logmsg info "Adding domain name \"$new_domain_name\" as search domain with tag \"dhcpv6-{{ ifname }}\" via vyos-hostsd-client"
        $hostsd_client --add-search-domains "$new_domain_name" --tag "dhcpv6-{{ ifname }}"
        hostsd_changes=y
    fi

    if [ -n "$new_domain_name_servers" ]; then
        logmsg info "Deleting nameservers with tag \"dhcpv6-{{ ifname }}\" via vyos-hostsd-client"
        $hostsd_client --delete-name-servers --tag "dhcpv6-{{ ifname }}"
        logmsg info "Adding nameservers \"$new_domain_name_servers\" with tag \"dhcpv6-{{ ifname }}\" via vyos-hostsd-client"
        $hostsd_client --add-name-servers $new_domain_name_servers --tag "dhcpv6-{{ ifname }}"
        hostsd_changes=y
    fi

    if [ $hostsd_changes ]; then
        logmsg info "Applying changes via vyos-hostsd-client"
        $hostsd_client --apply
    else
        logmsg info "No changes to apply via vyos-hostsd-client"
    fi
fi

# dhcp6c hook: on each valid Reply, arm renew (T1), rebind (T2), and a guard before pltime for {{ ifname }}.

set -euo pipefail
TAG="dhcp6c-hook"
log(){ local p="${1:-info}"; shift||true; systemd-cat -t "$TAG" -p "$p" <<<"${*:-}"; }

UNIT="dhcp6c@{{ ifname }}.service"
LOOKBACK="2 minutes ago"
WAKER="/config/scripts/dhcp6c-waker.sh"

[[ -x "$WAKER" ]] || log warning "waker not found at $WAKER; create it first"

# Collect recent logs for this interface
J="$(journalctl -u "$UNIT" --since "$LOOKBACK" --no-pager 2>/dev/null || true)"

# SAFEGUARD: only proceed if this recent reply shows PD success
if ! grep -Eq 'update_ia: status code for PD-[0-9]+: success' <<<"$J"; then
  log info "no PD success found in recent reply for {{ ifname }}; skip scheduling"
  exit 0
fi

# Parse T1/T2 from IA_PD line; pltime from IA_PD prefix line
T1=""; T2=""; PLTIME=""
LIA="$(grep -E 'IA_PD: .* T1=[0-9]+, T2=[0-9]+' <<<"$J" | tail -n1 || true)"
LPD="$(grep -E 'IA_PD prefix: .* pltime=[0-9]+' <<<"$J" | tail -n1 || true)"
[[ -n "$LIA" ]] && T1="$(sed -n 's/.*T1=\([0-9]\+\).*/\1/p' <<<"$LIA")" && T2="$(sed -n 's/.*T2=\([0-9]\+\).*/\1/p' <<<"$LIA")"
[[ -n "$LPD" ]] && PLTIME="$(sed -n 's/.*pltime=\([0-9]\+\).*/\1/p' <<<"$LPD")"

# Clean any previous transient units for this iface
systemctl stop  "dhcp6c-renew@{{ ifname }}.timer"  "dhcp6c-renew@{{ ifname }}.service"  >/dev/null 2>&1 || true
systemctl stop  "dhcp6c-rebind@{{ ifname }}.timer" "dhcp6c-rebind@{{ ifname }}.service" >/dev/null 2>&1 || true
systemctl stop  "dhcp6c-guard@{{ ifname }}.timer"  "dhcp6c-guard@{{ ifname }}.service"  >/dev/null 2>&1 || true
systemctl reset-failed "dhcp6c-renew@{{ ifname }}.service" "dhcp6c-rebind@{{ ifname }}.service" "dhcp6c-guard@{{ ifname }}.service" >/dev/null 2>&1 || true

# (1) Renew (T1): exact seconds
if [[ -n "$T1" && "$T1" -gt 0 ]]; then
  systemd-run --quiet --collect \
    --unit="dhcp6c-renew@{{ ifname }}.service" \
    --property=Type=oneshot \
    --property=Description="DHCPv6 renew on {{ ifname }}" \
    --on-active="${T1}s" \
    --timer-property=AccuracySec=1s \
    --timer-property=Persistent=false \
    "$WAKER" "{{ ifname }}"
  log info "armed renew (T1=${T1}s) for {{ ifname }}"
fi

# (2) Rebind (T2): exact seconds; only fires if no Reply after T1
if [[ -n "$T2" && "$T2" -gt 0 ]]; then
  systemd-run --quiet --collect \
    --unit="dhcp6c-rebind@{{ ifname }}.service" \
    --property=Type=oneshot \
    --property=Description="DHCPv6 rebind on {{ ifname }}" \
    --on-active="${T2}s" \
    --timer-property=AccuracySec=1s \
    --timer-property=Persistent=false \
    "$WAKER" "{{ ifname }}"
  log info "armed rebind (T2=${T2}s) for {{ ifname }}"
fi

# (3) Guard: before preferred-lifetime hits 0 (offset configurable; default 60s)
GUARD_OFFSET="${GUARD_OFFSET:-60}"     # set 0 to fire exactly at pltime expiry
if [[ -n "$PLTIME" && "$PLTIME" -gt 0 ]]; then
  delay=$(( PLTIME - GUARD_OFFSET ))
  (( delay < 1 )) && delay=1
  systemd-run --quiet --collect \
    --unit="dhcp6c-guard@{{ ifname }}.service" \
    --property=Type=oneshot \
    --property=Description="Guard before pltime on {{ ifname }} (dhcp6ctl start)" \
    --on-active="${delay}s" \
    --timer-property=AccuracySec=1s \
    --timer-property=Persistent=false \
    "$WAKER" "{{ ifname }}"
  log info "armed guard: now+${delay}s (pltime=${PLTIME}s) for {{ ifname }}"
else
  log warning "no pltime found for $UNIT; guard not scheduled"
fi
  • A new script for reloading dhcp6c upon the created systemd timer dhcp6c-waker@$IFACE (e.g : /config/scripts/dhcp6c-waker.sh)
#!/usr/bin/env bash
set -euo pipefail

# Wake/renew dhcp6c for ONE iface (required arg) by issuing:
#   dhcp6ctl start interface <iface>
# Assumes per-iface control key exists at /run/dhcp6c/dhcp6c.<iface>.sock.

TAG="dhcp6c-waker"
log(){ local p="${1:-info}"; shift || true; systemd-cat -t "$TAG" -p "$p" <<<"${*:-}"; }
usage(){ echo "usage: $0 <iface>"; }

# --- arg parsing
if [[ $# -ne 1 ]]; then
  log err "missing iface argument"; usage; exit 2
fi
IFACE="$1"
UNIT="dhcp6c@${IFACE}.service"
KEY="/run/dhcp6c/dhcp6c.${IFACE}.sock"       # key file (not a socket)
FALLBACK_KEY="/etc/wide-dhcpv6/dhcp6cctlkey" # optional fallback

# --- sanity checks
if ! ip link show dev "$IFACE" &>/dev/null; then
  log err "iface $IFACE not found"; exit 1
fi
# Allow UNKNOWN (e.g., PPPoE); skip only if clearly down
if ! ip link show dev "$IFACE" 2>/dev/null | grep -Eq 'state (UP|LOWER_UP|UNKNOWN)'; then
  log info "iface $IFACE appears DOWN; skip"
  exit 0
fi

# --- ensure key (prefer per-iface; optional fallback to global)
if [[ ! -r "$KEY" ]]; then
  if [[ -r "$FALLBACK_KEY" ]]; then
    log warning "missing per-iface key $KEY; falling back to $FALLBACK_KEY"
    KEY="$FALLBACK_KEY"
  else
    log err "no control key for $IFACE ($KEY or $FALLBACK_KEY)"; exit 1
  fi
fi

# --- kick dhcp6c (prefer dhcp6ctl start; fallback to systemctl restart)
if command -v dhcp6ctl >/dev/null 2>&1; then
  if dhcp6ctl -C -k "$KEY" start interface "$IFACE" >/dev/null 2>&1 \
  || dhcp6ctl -C -k "$KEY" -p 5546 start interface "$IFACE" >/dev/null 2>&1 \
  || dhcp6ctl -C -k "$KEY" -p 5547 start interface "$IFACE" >/dev/null 2>&1
  then
    log info "dhcp6ctl start OK (iface=$IFACE)"
  else
    log warning "dhcp6ctl start failed; restarting $UNIT"
    systemctl restart "$UNIT" >/dev/null 2>&1 || { log err "systemctl restart failed"; exit 1; }
  fi
else
  log warning "dhcp6ctl not installed; restarting $UNIT"
  systemctl restart "$UNIT" >/dev/null 2>&1 || { log err "systemctl restart failed"; exit 1; }
fi

# --- re-announce via radvd (best-effort)
systemctl reload radvd >/dev/null 2>&1 || systemctl restart radvd >/dev/null 2>&1 || true
log info "radvd reloaded after waking $IFACE"
  • setting up radvd with the right lifetimes given upon dhcp6c prefix delegation query:
prefix ::/64 {
    decrement-lifetime
    deprecate-prefix
    preferred-lifetime 172800 # your ISP pltime value for prefix delegation
    valid-lifetime 259200 # your ISP vltime value for prefix delegation
}
nvdx raised the priority of this task from Low to Normal.Sep 8 2025, 1:43 PM
nvdx updated the task description. (Show Details)

dhcp6c seems very old: 20080615-24 https://manpages.debian.org/testing/wide-dhcpv6-client/dhcp6c.8.en.html

other projects seems to actively patch dhcp6c:
https://github.com/opnsense/dhcp6c

some have completely rewritten it:
https://github.com/openwrt/odhcp6c
https://wiki.archlinux.org/title/Dhcpcd

debian is also pushing for systemd-netword handling:
https://wiki.debian.org/IPv6PrefixDelegation

Is there any plan to replace dhcp6c on vyos ?

Feature request: https://vyos.dev/T7807

nvdx changed Issue type from Unspecified (please specify) to Bug (incorrect behavior).Sep 9 2025, 8:15 PM
nvdx changed Version from - to 2025.09.07-0020-rolling.
nvdx changed Is it a breaking change? from Unspecified (possibly destroys the router) to Behavior change.
nvdx updated the task description. (Show Details)
nvdx updated the task description. (Show Details)
nvdx updated the task description. (Show Details)

Is there any dhcp6c repo (used as a dhcp6c base for vyos) where the issue can be forwarded?