diff --git a/board/common/busybox_defconfig b/board/common/busybox_defconfig index bd6f7bdc5..1d10811be 100644 --- a/board/common/busybox_defconfig +++ b/board/common/busybox_defconfig @@ -1016,12 +1016,7 @@ CONFIG_UDHCPC=y CONFIG_FEATURE_UDHCPC_ARPING=y CONFIG_FEATURE_UDHCPC_SANITIZEOPT=y CONFIG_UDHCPC_DEFAULT_SCRIPT="/usr/share/udhcpc/default.script" -CONFIG_UDHCPC6_DEFAULT_SCRIPT="/usr/share/udhcpc/default6.script" -CONFIG_UDHCPC6=y -CONFIG_FEATURE_UDHCPC6_RFC3646=y -CONFIG_FEATURE_UDHCPC6_RFC4704=y -CONFIG_FEATURE_UDHCPC6_RFC4833=y -CONFIG_FEATURE_UDHCPC6_RFC5970=y +# CONFIG_UDHCPC6 is not set # # Common options for DHCP applets diff --git a/board/common/rootfs/usr/libexec/odhcp6c.sh b/board/common/rootfs/usr/libexec/odhcp6c.sh new file mode 100755 index 000000000..21ca00464 --- /dev/null +++ b/board/common/rootfs/usr/libexec/odhcp6c.sh @@ -0,0 +1,242 @@ +#!/bin/sh +# DHCPv6 client state update script for odhcp6c +# This script expects a system with resolvconf (openresolv) and iproute2 + +[ -z "$1" ] && echo "Error: should be called from odhcp6c" && exit 1 + +interface="$1" +state="$2" +RESOLV_CONF="/run/resolvconf/interfaces/${interface}-ipv6.conf" +NTPFILE="/run/chrony/dhcp-sources.d/${interface}-ipv6.sources" + +[ -n "$metric" ] || metric=5 + +log() +{ + logger -I $$ -t odhcp6c -p user.notice "${interface}: $*" +} + +dbg() +{ + logger -I $$ -t odhcp6c -p user.debug "${interface}: $*" +} + +err() +{ + logger -I $$ -t odhcp6c -p user.err "${interface}: $*" +} + +teardown_interface() +{ + ip -6 route flush dev "$interface" + ip -6 address flush dev "$interface" scope global +} + +setup_interface() +{ + # Merge RA addresses with DHCP addresses + for entry in $RA_ADDRESSES; do + duplicate=0 + addr="${entry%%/*}" + for dentry in $ADDRESSES; do + daddr="${dentry%%/*}" + [ "$addr" = "$daddr" ] && duplicate=1 + done + [ "$duplicate" = "0" ] && ADDRESSES="$ADDRESSES $entry" + done + + # Add addresses + for entry in $ADDRESSES; do + addr="${entry%%,*}" + entry="${entry#*,}" + preferred="${entry%%,*}" + entry="${entry#*,}" + valid="${entry%%,*}" + + ip -6 address add "$addr" dev "$interface" preferred_lft "$preferred" valid_lft "$valid" proto dhcp + log "assigned address $addr (preferred=$preferred, valid=$valid)" + done + + # Add routes from RA + for entry in $RA_ROUTES; do + addr="${entry%%,*}" + entry="${entry#*,}" + gw="${entry%%,*}" + entry="${entry#*,}" + valid="${entry%%,*}" + entry="${entry#*,}" + metric="${entry%%,*}" + + if [ -n "$gw" ]; then + ip -6 route add "$addr" via "$gw" metric "$metric" dev "$interface" from "::/128" + else + ip -6 route add "$addr" metric "$metric" dev "$interface" + fi + + # Add routes for delegated prefixes + for prefix in $PREFIXES; do + paddr="${prefix%%,*}" + [ -n "$gw" ] && ip -6 route add "$addr" via "$gw" metric "$metric" dev "$interface" from "$paddr" + done + done +} + +handle_prefixes() +{ + # $PREFIXES format: "prefix/len,preferred,valid[,class=N][,excluded=...] ..." + for entry in $PREFIXES; do + addr="${entry%%,*}" + entry="${entry#*,}" + preferred="${entry%%,*}" + entry="${entry#*,}" + valid="${entry%%,*}" + + log "received delegated prefix $addr (preferred=$preferred, valid=$valid)" + + # Add unreachable route to prevent routing loops + ip -6 route add unreachable "$addr" 2>/dev/null + + # Future: Distribute to downstream interfaces + done +} + +handle_dns() +{ + truncate -s 0 "$RESOLV_CONF" + + # Combine DHCPv6 DNS ($RDNSS) and RA DNS ($RA_DNS), deduplicating + all_dns="" + for server in $RDNSS $RA_DNS; do + # Simple deduplication: only add if not already in list + case " $all_dns " in + *" $server "*) ;; + *) all_dns="$all_dns $server" ;; + esac + done + + # Domain search list (DHCPv6 option 24) + if [ -n "$DOMAINS" ]; then + dbg "adding search domains: $DOMAINS" + echo "search $DOMAINS # $interface" >> "$RESOLV_CONF" + fi + + # DNS servers + for server in $all_dns; do + [ -z "$server" ] && continue + dbg "adding dns $server" + echo "nameserver $server # $interface" >> "$RESOLV_CONF" + done + + if [ -n "$all_dns" ]; then + resolvconf -u + fi +} + +handle_ntp() +{ + # DHCPv6 option 56 (NTP server) is provided as $OPTION_56 in hex format + # Format: sub-option-code (2 bytes) + length (2 bytes) + data + # Sub-option 1 = NTP server address (16 bytes IPv6) + # + # This is complex to parse in shell. For now, we attempt basic parsing + # and fall back to logging a warning if the format is unexpected. + + if [ -n "$OPTION_56" ]; then + # Remove all non-hex characters (spaces, colons, etc.) and convert to lowercase + hex=$(echo "$OPTION_56" | tr -d '[:space:]:-' | tr '[:upper:]' '[:lower:]') + + truncate -s 0 "$NTPFILE" + ntp_found=0 + + # Parse option 56: iterate through sub-options + # Each sub-option: 2 bytes code + 2 bytes length + data + pos=0 + while [ $pos -lt ${#hex} ]; do + # Need at least 4 hex chars (2 bytes) for sub-option code + [ $((${#hex} - pos)) -lt 4 ] && break + + # Extract sub-option code (2 bytes = 4 hex chars) + subopt_code=$(echo "$hex" | cut -c $((pos+1))-$((pos+4))) + pos=$((pos + 4)) + + # Need 4 more hex chars for length + [ $((${#hex} - pos)) -lt 4 ] && break + + # Extract length (2 bytes = 4 hex chars) + subopt_len_hex=$(echo "$hex" | cut -c $((pos+1))-$((pos+4))) + subopt_len=$(printf "%d" "0x$subopt_len_hex") + pos=$((pos + 4)) + + # Sub-option 1 = NTP server address (should be 16 bytes for IPv6) + if [ "$subopt_code" = "0001" ] && [ "$subopt_len" -eq 16 ]; then + # Extract 16 bytes (32 hex chars) for IPv6 address + addr_hex=$(echo "$hex" | cut -c $((pos+1))-$((pos+32))) + + # Convert hex to IPv6 address format + # Format: 0123456789abcdef0123456789abcdef -> 0123:4567:89ab:cdef:0123:4567:89ab:cdef + ipv6=$(echo "$addr_hex" | sed 's/\(....\)\(....\)\(....\)\(....\)\(....\)\(....\)\(....\)\(....\)/\1:\2:\3:\4:\5:\6:\7:\8/') + + dbg "got NTP server $ipv6" + echo "server $ipv6 iburst" >> "$NTPFILE" + ntp_found=1 + fi + + # Skip this sub-option's data + pos=$((pos + subopt_len * 2)) + done + + if [ "$ntp_found" -eq 1 ]; then + chronyc reload sources >/dev/null 2>&1 + else + dbg "option 56 received but no NTP server addresses found (consider using option 31/SNTP)" + fi + fi +} + +log "state: $state" + +( + flock 9 + case "$state" in + started) + # Initial state - clean up any stale config + teardown_interface + ;; + + bound) + # Fresh lease - tear down and set up from scratch + teardown_interface + setup_interface + handle_prefixes + handle_dns + handle_ntp + ;; + + informed|updated|rebound|ra-updated) + # Update existing configuration + setup_interface + [ -n "$PREFIXES" ] && handle_prefixes + handle_dns + handle_ntp + ;; + + unbound|stopped) + # Lost server or client stopped + teardown_interface + rm -f "$RESOLV_CONF" + rm -f "$NTPFILE" + resolvconf -u + chronyc reload sources >/dev/null 2>&1 + ;; + esac +) 9>/tmp/odhcp6c.lock.${interface} +rm -f /tmp/odhcp6c.lock.${interface} + +# Run hooks +HOOK_DIR="/usr/libexec/odhcp6c.d" +for hook in "${HOOK_DIR}/"*; do + [ -f "${hook}" -a -x "${hook}" ] || continue + "${hook}" "$interface" "$state" +done + +exit 0 diff --git a/board/common/rootfs/usr/share/udhcpc/default6.script b/board/common/rootfs/usr/share/udhcpc/default6.script deleted file mode 100755 index c588bdbde..000000000 --- a/board/common/rootfs/usr/share/udhcpc/default6.script +++ /dev/null @@ -1,111 +0,0 @@ -#!/bin/sh -# This script expects a system with resolvconf (openresolv) and iproute2 - -[ -z "$1" ] && echo "Error: should be called from udhcpc6" && exit 1 - -ACTION="$1" -RESOLV_CONF="/run/resolvconf/interfaces/${interface}-ipv6.conf" -NTPFILE="/run/chrony/dhcp-sources.d/${interface}-ipv6.sources" - -[ -n "$metric" ] || metric=5 - -if [ -z "${IF_WAIT_DELAY}" ]; then - IF_WAIT_DELAY=10 -fi - -log() -{ - logger -I $$ -t udhcpc6 -p user.notice "${interface}: $*" -} - -dbg() -{ - logger -I $$ -t udhcpc6 -p user.debug "${interface}: $*" -} - -err() -{ - logger -I $$ -t udhcpc6 -p user.err "${interface}: $*" -} - -clr_dhcpv6_addresses() -{ - addrs=$(ip -j addr show dev $interface \ - | jq -c '.[0].addr_info[] | select(.family == "inet6") | select(.protocol == "dhcp")') - - for addr in $addrs; do - ip="$(echo "$addr" | jq -r '."local"')" - prefix="$(echo "$addr" | jq -r '."prefixlen"')" - log "removing $ip/$prefix" - ip addr del "$ip/$prefix" dev "$interface" - done -} - -log "action $ACTION" -case "$ACTION" in - deconfig) - clr_dhcpv6_addresses - - # drop info from this interface - rm -f "$RESOLV_CONF" - rm -f "$NTPFILE" - ;; - - leasefail|nak) - # DHCPv6 lease failed - log it - err "DHCPv6 lease failed" - ;; - - renew|bound) - # Add IPv6 address if provided (stateful DHCPv6) - if [ -n "$ipv6" ]; then - if /bin/ip addr add dev $interface $ipv6 proto dhcp; then - log "assigned address $ipv6" - fi - fi - - # Handle delegated prefix (prefix delegation) - if [ -n "$ipv6prefix" ]; then - log "received delegated prefix $ipv6prefix" - # Prefix delegation handling could be added here - # For now, just log it - actual routing/distribution - # would need additional configuration - fi - - # drop info from this interface - truncate -s 0 "$RESOLV_CONF" - - # DHCPv6 domain search list (option 24) - if [ -n "$search" ]; then - dbg "adding search $search" - echo "search $search # $interface" >> $RESOLV_CONF - fi - - # DHCPv6 DNS servers (option 23) - if [ -n "$dns" ]; then - for i in $dns ; do - dbg "adding dns $i" - echo "nameserver $i # $interface" >> $RESOLV_CONF - done - resolvconf -u - fi - - # NTP servers (option 56) - if [ -n "$ntpsrv" ]; then - truncate -s 0 "$NTPFILE" - for srv in $ntpsrv; do - dbg "got NTP server $srv" - echo "server $srv iburst" >> "$NTPFILE" - done - chronyc reload sources >/dev/null - fi - ;; -esac - -HOOK_DIR="$0.d" -for hook in "${HOOK_DIR}/"*; do - [ -f "${hook}" -a -x "${hook}" ] || continue - "${hook}" "$ACTION" -done - -exit 0 diff --git a/configs/aarch32_defconfig b/configs/aarch32_defconfig index 029e15f44..f8f6566c8 100644 --- a/configs/aarch32_defconfig +++ b/configs/aarch32_defconfig @@ -35,6 +35,7 @@ BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/aarch32/li BR2_LINUX_KERNEL_INSTALL_TARGET=y BR2_LINUX_KERNEL_NEEDS_HOST_OPENSSL=y BR2_PACKAGE_BUSYBOX_CONFIG="${BR2_EXTERNAL_INFIX_PATH}/board/common/busybox_defconfig" +BR2_PACKAGE_ODHCP6C=y BR2_PACKAGE_STRACE=y BR2_PACKAGE_STRESS_NG=y BR2_PACKAGE_JQ=y diff --git a/configs/aarch32_minimal_defconfig b/configs/aarch32_minimal_defconfig index 4410a9137..fbebc005b 100644 --- a/configs/aarch32_minimal_defconfig +++ b/configs/aarch32_minimal_defconfig @@ -35,6 +35,7 @@ BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/aarch32/li BR2_LINUX_KERNEL_INSTALL_TARGET=y BR2_LINUX_KERNEL_NEEDS_HOST_OPENSSL=y BR2_PACKAGE_BUSYBOX_CONFIG="${BR2_EXTERNAL_INFIX_PATH}/board/common/busybox_defconfig" +BR2_PACKAGE_ODHCP6C=y BR2_PACKAGE_STRACE=y BR2_PACKAGE_STRESS_NG=y BR2_PACKAGE_JQ=y diff --git a/configs/aarch64_defconfig b/configs/aarch64_defconfig index e83ba3bfe..e4cdf77c4 100644 --- a/configs/aarch64_defconfig +++ b/configs/aarch64_defconfig @@ -32,6 +32,7 @@ BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/aarch64/linux_defconfig" BR2_LINUX_KERNEL_INSTALL_TARGET=y BR2_PACKAGE_BUSYBOX_CONFIG="${BR2_EXTERNAL_INFIX_PATH}/board/common/busybox_defconfig" +BR2_PACKAGE_ODHCP6C=y BR2_PACKAGE_STRACE=y BR2_PACKAGE_STRESS_NG=y BR2_PACKAGE_JQ=y diff --git a/configs/aarch64_minimal_defconfig b/configs/aarch64_minimal_defconfig index b9d83f4ef..953f4e758 100644 --- a/configs/aarch64_minimal_defconfig +++ b/configs/aarch64_minimal_defconfig @@ -31,6 +31,7 @@ BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/aarch64/linux_defconfig" BR2_LINUX_KERNEL_INSTALL_TARGET=y BR2_PACKAGE_BUSYBOX_CONFIG="${BR2_EXTERNAL_INFIX_PATH}/board/common/busybox_defconfig" +BR2_PACKAGE_ODHCP6C=y BR2_PACKAGE_STRACE=y BR2_PACKAGE_STRESS_NG=y BR2_PACKAGE_SYSREPO_GROUP="sys-cli" diff --git a/configs/riscv64_defconfig b/configs/riscv64_defconfig index 61db925c0..660c0f425 100644 --- a/configs/riscv64_defconfig +++ b/configs/riscv64_defconfig @@ -37,6 +37,7 @@ BR2_LINUX_KERNEL_DTB_KEEP_DIRNAME=y BR2_LINUX_KERNEL_INSTALL_TARGET=y BR2_LINUX_KERNEL_NEEDS_HOST_OPENSSL=y BR2_PACKAGE_BUSYBOX_CONFIG="$(BR2_EXTERNAL_INFIX_PATH)/board/common/busybox_defconfig" +BR2_PACKAGE_ODHCP6C=y BR2_PACKAGE_STRACE=y BR2_PACKAGE_STRESS_NG=y BR2_PACKAGE_JQ=y diff --git a/configs/x86_64_defconfig b/configs/x86_64_defconfig index ab35d43ae..5ee156201 100644 --- a/configs/x86_64_defconfig +++ b/configs/x86_64_defconfig @@ -32,6 +32,7 @@ BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/x86_64/lin BR2_LINUX_KERNEL_INSTALL_TARGET=y BR2_LINUX_KERNEL_NEEDS_HOST_LIBELF=y BR2_PACKAGE_BUSYBOX_CONFIG="${BR2_EXTERNAL_INFIX_PATH}/board/common/busybox_defconfig" +BR2_PACKAGE_ODHCP6C=y BR2_PACKAGE_STRACE=y BR2_PACKAGE_STRESS_NG=y BR2_PACKAGE_JQ=y diff --git a/configs/x86_64_minimal_defconfig b/configs/x86_64_minimal_defconfig index d97366a04..e0ffa68ac 100644 --- a/configs/x86_64_minimal_defconfig +++ b/configs/x86_64_minimal_defconfig @@ -32,6 +32,7 @@ BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="${BR2_EXTERNAL_INFIX_PATH}/board/x86_64/lin BR2_LINUX_KERNEL_INSTALL_TARGET=y BR2_LINUX_KERNEL_NEEDS_HOST_LIBELF=y BR2_PACKAGE_BUSYBOX_CONFIG="${BR2_EXTERNAL_INFIX_PATH}/board/common/busybox_defconfig" +BR2_PACKAGE_ODHCP6C=y BR2_PACKAGE_STRACE=y BR2_PACKAGE_STRESS_NG=y BR2_PACKAGE_SYSREPO_GROUP="sys-cli" diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index db71a077e..85de87ef2 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -28,6 +28,9 @@ All notable changes to the project are documented in this file. - Upgrade netopeer2 (NETCONF) to 2.7.0 - Add RIPv2 routing support, issue #582 - Add NTP server support, issue #904 +- Migrate DHCPv6 client to odhcp6c for improved Router Advertisement + integration. Adds support for hybrid RA+DHCPv6 deployments where SLAAC + assigns addresses and DHCPv6 provides DNS (common ISP scenario) - Add support for configurable OSPF debug logging, issue #1281. Debug options can now be enabled per category (bfd, packet, ism, nsm, default-information, nssa). All debug options are disabled by default to prevent log flooding in diff --git a/src/confd/src/dhcp-common.c b/src/confd/src/dhcp-common.c index d364ae596..49ba3d983 100644 --- a/src/confd/src/dhcp-common.c +++ b/src/confd/src/dhcp-common.c @@ -212,16 +212,15 @@ static void infer_options_v4(sr_session_ctx_t *session, const char *xpath) /* * Default DHCPv6 options - * Note: udhcpc6 only supports dns-server (dns) and domain-search (search) with string names. - * Other options use numeric codes, which dhcp_compose_option() handles automatically. + * Note: odhcp6c uses numeric option codes for all options. */ static void infer_options_v6(sr_session_ctx_t *session, const char *xpath) { const char *opt[] = { - "dns-server", /* option 23 - udhcpc6: -O dns */ - "domain-search", /* option 24 - udhcpc6: -O search */ - "client-fqdn", /* option 39 - udhcpc6: -O 39 (equivalent to DHCPv4 hostname) */ - "ntp-server" /* option 56 - udhcpc6: -O 56 */ + "dns-server", /* option 23 */ + "domain-search", /* option 24 */ + "client-fqdn", /* option 39 */ + "ntp-server" /* option 56 */ }; size_t i; diff --git a/src/confd/src/dhcpv6-client.c b/src/confd/src/dhcpv6-client.c index e7bbebdd1..d29426277 100644 --- a/src/confd/src/dhcpv6-client.c +++ b/src/confd/src/dhcpv6-client.c @@ -20,7 +20,8 @@ static char *fallback_options_v6(const char *ifname) { /* dns-server, domain-search, client-fqdn, ntp-server */ - const char *defaults = "-O 23 -O 24 -O 39 -O 56 "; + /* odhcp6c format: -R -r */ + const char *defaults = "-R -r 23,24,39,56 "; return strdup(defaults); } @@ -28,7 +29,9 @@ static char *fallback_options_v6(const char *ifname) static char *dhcp_options_v6(const char *ifname, struct lyd_node *cfg, bool *request_pd) { struct lyd_node *option; + char opt_list[512] = { 0 }; char *options = NULL; + bool has_options = false; *request_pd = false; @@ -39,13 +42,34 @@ static char *dhcp_options_v6(const char *ifname, struct lyd_node *cfg, bool *req const char *name = lyd_get_value(id); int num = dhcp_option_lookup(id); - /* ia-pd option requires -d flag, not -O 25 */ + /* ia-pd option requires -P flag, not -r 25 */ if (num == 25 || (name && !strcmp(name, "ia-pd"))) { *request_pd = true; continue; /* Don't add to options string */ } - options = dhcp_compose_options(cfg, ifname, &options, id, val, hex, NULL); + /* Options with values not yet supported with odhcp6c */ + if (val || hex) { + WARN("DHCPv6 client %s: option values not yet supported with odhcp6c, ignoring %s", + ifname, name); + continue; + } + + /* Build comma-separated list for -r flag */ + if (has_options) + strlcat(opt_list, ",", sizeof(opt_list)); + + char num_str[8]; + snprintf(num_str, sizeof(num_str), "%d", num); + strlcat(opt_list, num_str, sizeof(opt_list)); + has_options = true; + } + + if (has_options) { + size_t len = strlen(opt_list) + 16; + options = malloc(len); + if (options) + snprintf(options, len, "-R -r %s ", opt_list); } return options ?: fallback_options_v6(ifname); @@ -57,27 +81,31 @@ static void add_v6(const char *ifname, struct lyd_node *cfg) const char *duid = lydx_get_cattr(cfg, "duid"); char *client_duid = NULL, *options = NULL; const char *action = "disable"; - char info_only[4] = { 0 }; - char prefix_del[4] = { 0 }; + const char *addr_mode = "-N try"; /* Default: stateful mode */ + char prefix_del[16] = { 0 }; bool request_pd = false; FILE *fp; + /* Information-only mode (stateless DHCPv6) */ if (lydx_is_enabled(cfg, "information-only")) - strlcpy(info_only, "-l ", sizeof(info_only)); + addr_mode = "-N none"; + /* Custom DUID: odhcp6c uses -c instead of -x 1: */ if (duid && duid[0]) { - size_t len = strlen(duid) + 32; + size_t len = strlen(duid) + 16; client_duid = malloc(len); if (!client_duid) goto generr; - snprintf(client_duid, len, "-x 1:%s ", duid); + snprintf(client_duid, len, "-c %s ", duid); } options = dhcp_options_v6(ifname, cfg, &request_pd); + + /* Prefix delegation: use -P 0 for auto */ if (request_pd) - strlcpy(prefix_del, "-d ", sizeof(prefix_del)); + strlcpy(prefix_del, "-P 0 ", sizeof(prefix_del)); fp = fopenf("w", "/etc/finit.d/available/dhcpv6-client-%s.conf", ifname); if (!fp) { @@ -89,14 +117,15 @@ static void add_v6(const char *ifname, struct lyd_node *cfg) fprintf(fp, "# Generated by Infix confd\n"); fprintf(fp, "metric=%s\n", metric); fprintf(fp, "service name:dhcpv6-client :%s \\\n" - " [2345] udhcpc6 -f -p /run/dhcpv6-client-%s.pid -t 3 -T 5 -A 30 -S -R \\\n" - " %s%s%s%s \\\n" - " -i %s %s \\\n" + " [2345] odhcp6c -e -p /run/dhcpv6-client-%s.pid \\\n" + " -s /usr/libexec/odhcp6c.sh \\\n" + " %s %s%s%s \\\n" + " %s \\\n" " -- DHCPv6 client @%s\n", ifname, ifname, ifname, - info_only, prefix_del, - options ? "-o " : "", options ?: "", - ifname, client_duid ?: "", ifname); + addr_mode, prefix_del, + options ?: "", client_duid ?: "", + ifname, ifname); fclose(fp); action = "enable"; err: diff --git a/test/case/dhcp/Readme.adoc b/test/case/dhcp/Readme.adoc index 8f21546a1..a128d72eb 100644 --- a/test/case/dhcp/Readme.adoc +++ b/test/case/dhcp/Readme.adoc @@ -9,6 +9,7 @@ Tests verifying DHCPv4/DHCPv6 client and server functionality: - DHCPv4 client hostname management and priority - Basic DHCPv6 client operation with address assignment - DHCPv6 client with prefix delegation (IA_PD) + - DHCPv6 SLAAC/RA (Stateless) - Basic DHCPv4 server operation and lease assignment - DHCPv4 server with host-specific IP reservations - DHCPv4 server with multiple subnet configurations @@ -37,6 +38,10 @@ include::client6_prefix_delegation/Readme.adoc[] <<< +include::client6_slaac_ra/Readme.adoc[] + +<<< + include::server_basic/Readme.adoc[] <<< diff --git a/test/case/dhcp/client6_basic/test.adoc b/test/case/dhcp/client6_basic/test.adoc index 2fcc3a183..f325230aa 100644 --- a/test/case/dhcp/client6_basic/test.adoc +++ b/test/case/dhcp/client6_basic/test.adoc @@ -15,6 +15,7 @@ image::topology.svg[DHCPv6 Basic topology, align=center, scaledwidth=75%] . Set up topology and attach to target DUT . Configure DHCPv6 client +. Verify DHCPv6 client is running . Verify client lease for {CLIENT} . Verify client default route ::/0 . Verify client domain name resolution diff --git a/test/case/dhcp/client6_basic/test.py b/test/case/dhcp/client6_basic/test.py index ab28f9da8..6a9a041a4 100755 --- a/test/case/dhcp/client6_basic/test.py +++ b/test/case/dhcp/client6_basic/test.py @@ -12,6 +12,19 @@ import infamy.route as route from infamy.util import until + +def checkrun(): + """Check DHCPv6 client is running""" + res = tgtssh.runsh(f"pgrep -f 'odhcp6c.*{port}'") + return res.returncode == 0 + + +def check_dns_resolution(): + """Check if DNS resolution works by pinging FQDN""" + rc = tgtssh.runsh(f"ping -6 -c1 -w5 {VERIFY}") + return rc.returncode == 0 + + with infamy.Test() as test: SERVER = '2001:db8::1' CLIENT = '2001:db8::42' @@ -53,14 +66,17 @@ } client.put_config_dict("ietf-interfaces", config) + with test.step("Verify DHCPv6 client is running"): + until(checkrun, attempts=10) + with test.step(f"Verify client lease for {CLIENT}"): - until(lambda: iface.address_exist(client, port, CLIENT, prefix_length=128)) + until(lambda: iface.address_exist(client, port, CLIENT, prefix_length=128), attempts=30) with test.step("Verify client default route ::/0"): until(lambda: route.ipv6_route_exist(client, "::/0"), attempts=20) with test.step("Verify client domain name resolution"): - rc = tgtssh.runsh(f"ping -6 -c1 -w20 {VERIFY}") - assert rc.returncode == 0, f"Client failed: ping {VERIFY}" + # DNS configuration may take a moment, especially on ARM hardware + until(check_dns_resolution, attempts=20) test.succeed() diff --git a/test/case/dhcp/client6_prefix_delegation/test.py b/test/case/dhcp/client6_prefix_delegation/test.py index 11600689e..242adf0f3 100755 --- a/test/case/dhcp/client6_prefix_delegation/test.py +++ b/test/case/dhcp/client6_prefix_delegation/test.py @@ -15,8 +15,8 @@ def checkrun(dut): """Check DUT is running DHCPv6 client""" - res = dut.runsh(f"pgrep -f 'udhcpc6.*{port}'") - # print(f"Checking for udhcpc6: {res.stdout}") + res = dut.runsh(f"pgrep -f 'odhcp6c.*{port}'") + # print(f"Checking for odhcp6c: {res.stdout}") if res.stdout.strip() != "": return True return False @@ -24,8 +24,8 @@ def checkrun(dut): def checklog(dut): """Check syslog for prefix delegation message""" - rc = dut.runsh("tail -10 /log/syslog | grep 'received delegated prefix'") - # print(f"DHCPv6 client logs:\n{res.stdout}") + rc = dut.runsh("tail -50 /log/syslog | grep 'received delegated prefix'") + # print(f"DHCPv6 client logs:\n{rc.stdout}") if rc.stdout.strip() != "": return True return False @@ -74,9 +74,10 @@ def checklog(dut): client.put_config_dict("ietf-interfaces", config) with test.step("Verify DHCPv6 client is running"): - until(lambda: checkrun(tgtssh)) + until(lambda: checkrun(tgtssh), attempts=20) with test.step("Verify prefix delegation in logs"): - until(lambda: checklog(tgtssh)) + # Prefix delegation may take longer on ARM hardware + until(lambda: checklog(tgtssh), attempts=30) test.succeed() diff --git a/test/case/dhcp/client6_slaac_ra/Readme.adoc b/test/case/dhcp/client6_slaac_ra/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/dhcp/client6_slaac_ra/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/dhcp/client6_slaac_ra/test.adoc b/test/case/dhcp/client6_slaac_ra/test.adoc new file mode 100644 index 000000000..01bf3eb51 --- /dev/null +++ b/test/case/dhcp/client6_slaac_ra/test.adoc @@ -0,0 +1,30 @@ +=== DHCPv6 SLAAC/RA (Stateless) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/dhcp/client6_slaac_ra] + +==== Description + +Verify DHCPv6 client works in stateless mode (information-only) where: +- Router Advertisements (RA) provide the IPv6 address via SLAAC +- DHCPv6 provides DNS servers and domain search options only + +This is a common ISP deployment scenario where the router sends RAs for +address autoconfiguration, and DHCPv6 is only used for providing additional +configuration like DNS servers. + +The test verifies that odhcp6c correctly integrates both RA and DHCPv6 +information, which is something the old udhcpc6 client could not do. + +==== Topology + +image::topology.svg[DHCPv6 SLAAC/RA (Stateless) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Configure DHCPv6 client in information-only mode +. Verify DHCPv6 client is running +. Verify client got SLAAC address from RA +. Verify client domain name resolution + + diff --git a/test/case/dhcp/client6_slaac_ra/test.py b/test/case/dhcp/client6_slaac_ra/test.py new file mode 100755 index 000000000..b34d6dadf --- /dev/null +++ b/test/case/dhcp/client6_slaac_ra/test.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""DHCPv6 with SLAAC/RA (Stateless DHCPv6) + +Verify DHCPv6 client works in stateless mode (information-only) where: +- Router Advertisements (RA) provide the IPv6 address via SLAAC +- DHCPv6 provides DNS servers and domain search options only + +This is a common ISP deployment scenario where the router sends RAs for +address autoconfiguration, and DHCPv6 is only used for providing additional +configuration like DNS servers. + +The test verifies that odhcp6c correctly integrates both RA and DHCPv6 +information, which is something the old udhcpc6 client could not do. + +""" + +import infamy +import infamy.dhcp +import infamy.iface as iface +from infamy.util import until + + +def checkrun(dut): + """Check DUT is running DHCPv6 client""" + rc = dut.runsh(f"pgrep -f 'odhcp6c.*{port}'") + if rc.stdout.strip() != "": + return True + return False + + +def check_dns_resolution(): + """Check if DNS resolution works by pinging FQDN""" + rc = tgtssh.runsh(f"ping -6 -c1 -w5 {VERIFY}") + return rc.returncode == 0 + + +def check_slaac_address(): + """Check if SLAAC address was assigned""" + addrs = iface.get_ipv6_address(client, port) + return addrs is not None and len(addrs) > 0 + + +with infamy.Test() as test: + SERVER = '2001:db8::1' + DOMAIN = 'example.com' + VERIFY = 'server.' + DOMAIN + + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + client = env.attach("client", "mgmt") + tgtssh = env.attach("client", "mgmt", "ssh") + _, host = env.ltop.xlate("host", "data") + _, port = env.ltop.xlate("client", "data") + + with infamy.IsolatedMacVlan(host) as netns: + netns.addip(SERVER, prefix_length=64, proto="ipv6") + # Stateless DHCPv6: RA provides address, DHCPv6 provides DNS + with infamy.dhcp.Server6Dnsmasq(netns, + start=None, # No DHCPv6 addresses + end=None, # Stateless mode + dns=SERVER, + domain=DOMAIN, + address=SERVER): + + with test.step("Configure DHCPv6 client in information-only mode"): + config = { + "interfaces": { + "interface": [{ + "name": f"{port}", + "ipv6": { + "enabled": True, + "infix-dhcpv6-client:dhcp": { + "information-only": True, # Stateless DHCPv6 + "option": [ + {"id": "dns-server"}, + {"id": "domain-search"} + ] + } + } + }] + } + } + client.put_config_dict("ietf-interfaces", config) + + with test.step("Verify DHCPv6 client is running"): + until(lambda: checkrun(tgtssh), attempts=20) + + with test.step("Verify client got SLAAC address from RA"): + # Should get address from RA (SLAAC), not DHCPv6 + # Address will be in 2001:db8::/64 range + until(check_slaac_address, attempts=30) + + with test.step("Verify client domain name resolution"): + # This proves DNS came from DHCPv6 (information-only) + until(check_dns_resolution, attempts=30) + + test.succeed() diff --git a/test/case/dhcp/client6_slaac_ra/topology.dot b/test/case/dhcp/client6_slaac_ra/topology.dot new file mode 100644 index 000000000..730ae6d10 --- /dev/null +++ b/test/case/dhcp/client6_slaac_ra/topology.dot @@ -0,0 +1,23 @@ +graph "1x2" { + layout="neato"; + overlap="false"; + esep="+100"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt | data }", + pos="0,20!", + requires="controller", + ]; + + client [ + label="{ mgmt | data } | client", + pos="200,20!", + requires="infix", + ]; + + host:mgmt -- client:mgmt [requires="mgmt", color=lightgrey] + host:data -- client:data [color=black, taillabel="2001:db8::1/64\nRA+DHCPv6", headlabel="SLAAC addr\nDNS via DHCPv6"] +} diff --git a/test/case/dhcp/client6_slaac_ra/topology.svg b/test/case/dhcp/client6_slaac_ra/topology.svg new file mode 100644 index 000000000..6132fa6ed --- /dev/null +++ b/test/case/dhcp/client6_slaac_ra/topology.svg @@ -0,0 +1,46 @@ + + + + + + +1x2 + + + +host + +host + +mgmt + +data + + + +client + +mgmt + +data + +client + + + +host:mgmt--client:mgmt + + + + +host:data--client:data + +SLAAC addr +DNS via DHCPv6 +2001:db8::1/64 +RA+DHCPv6 + + + diff --git a/test/case/dhcp/dhcp_client.yaml b/test/case/dhcp/dhcp_client.yaml index b6ac309db..db91983c2 100644 --- a/test/case/dhcp/dhcp_client.yaml +++ b/test/case/dhcp/dhcp_client.yaml @@ -19,3 +19,6 @@ - name: DHCPv6 Prefix Delegation case: client6_prefix_delegation/test.py + +- name: DHCPv6 SLAAC/RA (Stateless) + case: client6_slaac_ra/test.py diff --git a/test/case/ntp/client_stratum_selection/test.adoc b/test/case/ntp/client_stratum_selection/test.adoc index 9c458894f..56f5ebc52 100644 --- a/test/case/ntp/client_stratum_selection/test.adoc +++ b/test/case/ntp/client_stratum_selection/test.adoc @@ -29,6 +29,7 @@ image::topology.svg[NTP client stratum selection topology, align=center, scaledw . Wait for srv2 to sync from srv1 . Configure client to sync from both servers . Wait for client to see both servers +. Wait for srv2 stratum to stabilize . Verify client selects srv1 (lower stratum)