Merge pull request #1350 from kernelkit/change-dhcpv6-client

Migrate to odhcp6c for dhpcv6 client support
This commit is contained in:
Joachim Wiberg
2026-01-11 09:26:51 +01:00
committed by GitHub
23 changed files with 534 additions and 147 deletions
+1 -6
View File
@@ -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
+242
View File
@@ -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
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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"
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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"
+3
View File
@@ -29,6 +29,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
+5 -6
View File
@@ -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;
+44 -15
View File
@@ -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 <comma-separated list> */
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 <net/%s/running> \\\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:
+5
View File
@@ -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[]
<<<
+1
View File
@@ -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
+19 -3
View File
@@ -12,6 +12,19 @@ import infamy.iface as iface
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 @@ with infamy.Test() as test:
}
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()
@@ -15,8 +15,8 @@ import time
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 @@ with infamy.Test() as test:
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()
+1
View File
@@ -0,0 +1 @@
test.adoc
+30
View File
@@ -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
+97
View File
@@ -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()
@@ -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> mgmt | <data> data }",
pos="0,20!",
requires="controller",
];
client [
label="{ <mgmt> mgmt | <data> 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"]
}
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 1x2 Pages: 1 -->
<svg width="474pt" height="55pt"
viewBox="0.00 0.00 474.04 55.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 51)">
<title>1x2</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-51 470.04,-51 470.04,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-46.5 100,-46.5 100,-0.5 0,-0.5"/>
<text text-anchor="middle" x="25" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-0.5 50,-46.5 "/>
<text text-anchor="middle" x="75" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="50,-23.5 100,-23.5 "/>
<text text-anchor="middle" x="75" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
</g>
<!-- client -->
<g id="node2" class="node">
<title>client</title>
<polygon fill="none" stroke="black" points="350.04,-0.5 350.04,-46.5 466.04,-46.5 466.04,-0.5 350.04,-0.5"/>
<text text-anchor="middle" x="375.04" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="350.04,-23.5 400.04,-23.5 "/>
<text text-anchor="middle" x="375.04" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="400.04,-0.5 400.04,-46.5 "/>
<text text-anchor="middle" x="433.04" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">client</text>
</g>
<!-- host&#45;&#45;client -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;client:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M100,-35.5C100,-35.5 350.04,-35.5 350.04,-35.5"/>
</g>
<!-- host&#45;&#45;client -->
<g id="edge2" class="edge">
<title>host:data&#45;&#45;client:data</title>
<path fill="none" stroke="black" stroke-width="2" d="M100,-11.5C100,-11.5 350.04,-11.5 350.04,-11.5"/>
<text text-anchor="middle" x="288.04" y="-30.3" font-family="DejaVu Serif, Book" font-size="14.00">SLAAC addr</text>
<text text-anchor="middle" x="288.04" y="-15.3" font-family="DejaVu Serif, Book" font-size="14.00">DNS via DHCPv6</text>
<text text-anchor="middle" x="154.5" y="-30.3" font-family="DejaVu Serif, Book" font-size="14.00">2001:db8::1/64</text>
<text text-anchor="middle" x="154.5" y="-15.3" font-family="DejaVu Serif, Book" font-size="14.00">RA+DHCPv6</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+3
View File
@@ -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