From a568f31309fbb8f6ffdd85e8e9b012e0ade7757f Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 9 Nov 2025 16:18:57 +0100 Subject: [PATCH] confd: add support for dhcpv6 client Basic DHCPv6 support as a per-interface ipv6 setting, not in line with the IETF YANG model, which has this as a root container. Note, this also fixes DHCPv4 option inference after the relocation of /dhcp-client to /interfaces, in 764bd8e. Fixes #1110 Signed-off-by: Joachim Wiberg --- board/common/rootfs/etc/udhcpc/default.script | 1 + .../common/rootfs/etc/udhcpc/default6.script | 1 + .../rootfs/usr/share/udhcpc/default6.script | 112 ++++++++ src/confd/src/Makefile.am | 1 + src/confd/src/core.c | 8 +- src/confd/src/core.h | 7 +- src/confd/src/ietf-interfaces.c | 4 + src/confd/src/ietf-interfaces.h | 3 + src/confd/src/infix-dhcp-client.c | 226 +--------------- src/confd/src/infix-dhcp-common.c | 242 ++++++++++++++++++ src/confd/src/infix-dhcp-common.h | 20 ++ src/confd/src/infix-dhcp-server.c | 3 +- src/confd/src/infix-dhcpv6-client.c | 167 ++++++++++++ src/confd/yang/confd.inc | 5 +- src/confd/yang/confd/infix-dhcp-client.yang | 6 +- ...yang => infix-dhcp-client@2025-11-09.yang} | 0 src/confd/yang/confd/infix-dhcp-common.yang | 73 ++++++ ...yang => infix-dhcp-common@2025-11-09.yang} | 0 src/confd/yang/confd/infix-dhcpv6-client.yang | 140 ++++++++++ .../confd/infix-dhcpv6-client@2025-11-09.yang | 1 + 20 files changed, 786 insertions(+), 234 deletions(-) create mode 120000 board/common/rootfs/etc/udhcpc/default.script create mode 120000 board/common/rootfs/etc/udhcpc/default6.script create mode 100755 board/common/rootfs/usr/share/udhcpc/default6.script create mode 100644 src/confd/src/infix-dhcp-common.h create mode 100644 src/confd/src/infix-dhcpv6-client.c rename src/confd/yang/confd/{infix-dhcp-client@2025-11-02.yang => infix-dhcp-client@2025-11-09.yang} (100%) rename src/confd/yang/confd/{infix-dhcp-common@2025-01-29.yang => infix-dhcp-common@2025-11-09.yang} (100%) create mode 100644 src/confd/yang/confd/infix-dhcpv6-client.yang create mode 120000 src/confd/yang/confd/infix-dhcpv6-client@2025-11-09.yang diff --git a/board/common/rootfs/etc/udhcpc/default.script b/board/common/rootfs/etc/udhcpc/default.script new file mode 120000 index 00000000..b633b25c --- /dev/null +++ b/board/common/rootfs/etc/udhcpc/default.script @@ -0,0 +1 @@ +/usr/share/udhcpc/default.script \ No newline at end of file diff --git a/board/common/rootfs/etc/udhcpc/default6.script b/board/common/rootfs/etc/udhcpc/default6.script new file mode 120000 index 00000000..85411a05 --- /dev/null +++ b/board/common/rootfs/etc/udhcpc/default6.script @@ -0,0 +1 @@ +/usr/share/udhcpc/default6.script \ No newline at end of file diff --git a/board/common/rootfs/usr/share/udhcpc/default6.script b/board/common/rootfs/usr/share/udhcpc/default6.script new file mode 100755 index 00000000..219d6a3e --- /dev/null +++ b/board/common/rootfs/usr/share/udhcpc/default6.script @@ -0,0 +1,112 @@ +#!/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 + /bin/ip link set dev $interface up + + # 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/src/confd/src/Makefile.am b/src/confd/src/Makefile.am index 29466e99..3692de39 100644 --- a/src/confd/src/Makefile.am +++ b/src/confd/src/Makefile.am @@ -45,6 +45,7 @@ confd_plugin_la_SOURCES = \ ietf-routing.c \ infix-dhcp-common.c \ infix-dhcp-client.c \ + infix-dhcpv6-client.c \ infix-dhcp-server.c \ infix-factory.c \ infix-firewall.c \ diff --git a/src/confd/src/core.c b/src/confd/src/core.c index f452df35..dc3f270d 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -198,6 +198,10 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod if ((rc = infix_dhcp_client_change(session, config, diff, event, confd))) goto free_diff; + /* infix-dhcpv6-client*/ + if ((rc = infix_dhcpv6_client_change(session, config, diff, event, confd))) + goto free_diff; + /* ietf-keystore */ if ((rc = ietf_keystore_change(session, config, diff, event, confd))) goto free_diff; @@ -428,10 +432,6 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv) goto err; rc = infix_dhcp_server_candidate_init(&confd); - if (rc) - goto err; - - rc = infix_dhcp_client_candidate_init(&confd); if (rc) goto err; /* YOUR_INIT GOES HERE */ diff --git a/src/confd/src/core.h b/src/confd/src/core.h index 87f59849..fc0e62d8 100644 --- a/src/confd/src/core.h +++ b/src/confd/src/core.h @@ -204,12 +204,11 @@ static inline int infix_containers_rpc_init(struct confd *confd) { return 0; } static inline int infix_containers_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd) { return 0; } #endif -/* infix-dhcp-common.c */ -int dhcp_option_lookup(const struct lyd_node *id); - /* infix-dhcp-client.c */ int infix_dhcp_client_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd); -int infix_dhcp_client_candidate_init(struct confd *confd); + +/* infix-dhcpv6-client.c */ +int infix_dhcpv6_client_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd); /* infix-dhcp-server.c */ int infix_dhcp_server_candidate_init(struct confd *confd); diff --git a/src/confd/src/ietf-interfaces.c b/src/confd/src/ietf-interfaces.c index db10e3f7..ee42a6a7 100644 --- a/src/confd/src/ietf-interfaces.c +++ b/src/confd/src/ietf-interfaces.c @@ -160,6 +160,10 @@ static int ifchange_cand(sr_session_ctx_t *session, uint32_t sub_id, const char if (err) break; + err = ifchange_cand_infer_dhcp(session, new->xpath); + if (err) + break; + err = cni_ifchange_cand_infer_type(session, new->xpath); if (err) break; diff --git a/src/confd/src/ietf-interfaces.h b/src/confd/src/ietf-interfaces.h index 655f6a83..a34accad 100644 --- a/src/confd/src/ietf-interfaces.h +++ b/src/confd/src/ietf-interfaces.h @@ -144,6 +144,9 @@ int ifchange_cand_infer_vlan(sr_session_ctx_t *session, const char *path); int vlan_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip); int vlan_add_deps(struct lyd_node *cif); +/* infix-dhcp-common.c */ +int ifchange_cand_infer_dhcp(sr_session_ctx_t *session, const char *path); + /* infix-if-vxlan.c */ int vxlan_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip); diff --git a/src/confd/src/infix-dhcp-client.c b/src/confd/src/infix-dhcp-client.c index c2da13b8..74a82dd6 100644 --- a/src/confd/src/infix-dhcp-client.c +++ b/src/confd/src/infix-dhcp-client.c @@ -12,6 +12,8 @@ #include #include "core.h" +#include "infix-dhcp-common.h" + #define ARPING_MSEC 1000 #define MODULE "infix-dhcp-client" #define XPATH "/ietf-interfaces:interfaces/interface/ietf-ip:ipv4/infix-dhcp-client:dhcp" @@ -48,140 +50,6 @@ static char *ip_cache(const char *ifname, char *str, size_t len) return str; } -static char *hostname(struct lyd_node *cfg, char *str, size_t len) -{ - struct lyd_node *node; - const char *hostname; - char *ptr; - - node = lydx_get_xpathf(cfg, "/ietf-system:system/hostname"); - if (!node) - return NULL; - - hostname = lyd_get_value(node); - if (!hostname || hostname[0] == 0) - return NULL; - - ptr = strchr(hostname, '.'); - if (ptr) - *ptr = 0; - - snprintf(str, len, "-x hostname:%s ", hostname); - - return str; -} - -static char *fqdn(const char *val, char *str, size_t len) -{ - snprintf(str, len, "-F \"%s\" ", val); - return str; -} - -static char *os_name_version(char *str, size_t len) -{ - char *val; - - if (!str || !len) - return NULL; - - str[0] = 0; - - val = fgetkey("/etc/os-release", "NAME"); - if (val) - snprintf(str, len, "-V \"%.32s ", val); - - val = fgetkey("/etc/os-release", "VERSION"); - if (val) { - strlcat(str, val, len); - strlcat(str, "\"", len); - } - - if (strlen(str) > 0 && str[strlen(str) - 1] != '"') { - str[0] = 0; - return NULL; - } - - return str; -} - -static char *compose_option(struct lyd_node *cfg, const char *ifname, struct lyd_node *id, - const char *val, const char *hex, char *option, size_t len) -{ - const char *name = lyd_get_value(id); - int num = dhcp_option_lookup(id); - - if (num == -1) { - ERROR("Failed looking up DHCP client %s option %s, skipping.", ifname, name); - return NULL; - } - - if (val || hex) { - switch (num) { - case 81: /* fqdn */ - if (!val) - return NULL; - return fqdn(val, option, len); - case 12: /* hostname */ - if (val && !strcmp(val, "auto")) - return hostname(cfg, option, len); - /* fallthrough */ - default: - if (hex) { - snprintf(option, len, "-x %d:", num); - strlcat(option, hex, len); - strlcat(option, " ", len); - } else { - /* string value */ - snprintf(option, len, "-x %d:'\"%s\"' ", num, val); - } - break; - } - } else { - struct { int num; char *(*cb)(const char *, char *, size_t); } opt[] = { - { 50, ip_cache }, /* address */ - { 81, NULL }, /* fqdn */ - }; - - for (size_t i = 0; i < NELEMS(opt); i++) { - if (num != opt[i].num) - continue; - - if (!opt[i].cb || !opt[i].cb(ifname, option, len)) - return NULL; - - return option; - } - - snprintf(option, len, "-O %d ", num); - } - - return option; -} - -static char *compose_options(struct lyd_node *cfg, const char *ifname, char **options, - struct lyd_node *id, const char *val, const char *hex) -{ - char opt[300]; - - if (!compose_option(cfg, ifname, id, val, hex, opt, sizeof(opt))) - return *options; - - if (*options) { - char *opts; - - opts = realloc(*options, strlen(*options) + strlen(opt) + 1); - if (!opts) { - ERROR("failed reallocating options: %s", strerror(errno)); - free(*options); - return NULL; - } - - *options = strcat(opts, opt); - } else - *options = strdup(opt); - - return *options; -} static char *fallback_options(const char *ifname) { @@ -206,7 +74,7 @@ static char *dhcp_options(const char *ifname, struct lyd_node *cfg) const char *val = lydx_get_cattr(option, "value"); const char *hex = lydx_get_cattr(option, "hex"); - options = compose_options(cfg, ifname, &options, id, val, hex); + options = dhcp_compose_options(cfg, ifname, &options, id, val, hex, ip_cache); } return options ?: fallback_options(ifname); @@ -244,7 +112,7 @@ static void add(const char *ifname, struct lyd_node *cfg) options = dhcp_options(ifname, cfg); - os_name_version(vendor, sizeof(vendor)); + dhcp_os_name_version(vendor, sizeof(vendor)); fp = fopenf("w", "/etc/finit.d/available/dhcp-client-%s.conf", ifname); if (!fp) { @@ -331,89 +199,3 @@ int infix_dhcp_client_change(sr_session_ctx_t *session, struct lyd_node *config, return err; } - -/* - * Default DHCP options for udhcpc, from networking/udhcp/common.c OPTION_REQ - */ -static void infer_options(sr_session_ctx_t *session, const char *xpath) -{ - const char *opt[] = { - "netmask", - "broadcast", - "router", - "domain", - "hostname", /* server may use this to register our current name */ - "dns-server", - "ntp-server" /* will not be activated unless ietf-system also is */ - }; - size_t i; - - for (i = 0; i < NELEMS(opt); i++) - srx_set_item(session, NULL, 0, "%s/option[id='%s']", xpath, opt[i]); -} - -static int cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module, - const char *xpath, sr_event_t event, unsigned request_id, void *priv) -{ - sr_change_iter_t *iter; - sr_change_oper_t op; - sr_val_t *old, *new; - sr_error_t err; - - switch (event) { - case SR_EV_UPDATE: - case SR_EV_CHANGE: - break; - default: - return SR_ERR_OK; - } - - err = sr_dup_changes_iter(session, XPATH "//*", &iter); - if (err) - return err; - - while (sr_get_change_next(session, iter, &op, &old, &new) == SR_ERR_OK) { - char *xpath, *ptr; - size_t cnt = 0; - - switch (op) { - case SR_OP_CREATED: - case SR_OP_MODIFIED: - break; - default: - continue; - } - - xpath = strdupa(new->xpath); - if (!xpath) { - ERRNO("Failed strdupa()"); - return SR_ERR_SYS; - } - - if ((ptr = strstr(xpath, "]/")) == NULL) - continue; - ptr[1] = 0; - - err = srx_nitems(session, &cnt, "%s/option", xpath); - if (err || cnt) { - continue; - } - - infer_options(session, xpath); - } - - sr_free_change_iter(iter); - return SR_ERR_OK; -} - -int infix_dhcp_client_candidate_init(struct confd *confd) -{ - int rc; - - REGISTER_CHANGE(confd->cand, MODULE, XPATH"//.", SR_SUBSCR_UPDATE, cand, confd, &confd->sub); - - return SR_ERR_OK; -fail: - ERROR("init failed: %s", sr_strerror(rc)); - return rc; -} diff --git a/src/confd/src/infix-dhcp-common.c b/src/confd/src/infix-dhcp-common.c index 841ff3e4..6782094b 100644 --- a/src/confd/src/infix-dhcp-common.c +++ b/src/confd/src/infix-dhcp-common.c @@ -1,6 +1,18 @@ /* SPDX-License-Identifier: BSD-3-Clause */ +#include +#include +#include +#include +#include + +#include +#include +#include + #include +#include "core.h" +#include "infix-dhcp-common.h" int dhcp_option_lookup(const struct lyd_node *id) { @@ -43,8 +55,238 @@ int dhcp_option_lookup(const struct lyd_node *id) val = strtol(name, &endptr, 10); if (*endptr == 0 && val > 0 && val < 255) return (int)val; + } else if (type->basetype == LY_TYPE_UINT16) { + char *endptr; + long val; + + val = strtol(name, &endptr, 10); + if (*endptr == 0 && val > 0 && val < 65536) + return (int)val; } } return -1; } + +char *dhcp_hostname(struct lyd_node *cfg, char *str, size_t len) +{ + struct lyd_node *node; + const char *hostname; + char *ptr; + + node = lydx_get_xpathf(cfg, "/ietf-system:system/hostname"); + if (!node) + return NULL; + + hostname = lyd_get_value(node); + if (!hostname || hostname[0] == 0) + return NULL; + + ptr = strchr(hostname, '.'); + if (ptr) + *ptr = 0; + + snprintf(str, len, "-x hostname:%s ", hostname); + + return str; +} + +char *dhcp_fqdn(const char *val, char *str, size_t len) +{ + snprintf(str, len, "-F \"%s\" ", val); + return str; +} + +char *dhcp_os_name_version(char *str, size_t len) +{ + char *val; + + if (!str || !len) + return NULL; + + str[0] = 0; + + val = fgetkey("/etc/os-release", "NAME"); + if (val) + snprintf(str, len, "-V \"%.32s ", val); + + val = fgetkey("/etc/os-release", "VERSION"); + if (val) { + strlcat(str, val, len); + strlcat(str, "\"", len); + } + + if (strlen(str) > 0 && str[strlen(str) - 1] != '"') { + str[0] = 0; + return NULL; + } + + return str; +} + +char *dhcp_compose_option(struct lyd_node *cfg, const char *ifname, struct lyd_node *id, + const char *val, const char *hex, char *option, size_t len, + char *(*ip_cache_cb)(const char *, char *, size_t)) +{ + const char *name = lyd_get_value(id); + int num = dhcp_option_lookup(id); + + if (num == -1) { + ERROR("Failed looking up DHCP client %s option %s, skipping.", ifname, name); + return NULL; + } + + if (val || hex) { + switch (num) { + case 81: /* fqdn */ + if (!val) + return NULL; + return dhcp_fqdn(val, option, len); + case 12: /* hostname */ + if (val && !strcmp(val, "auto")) + return dhcp_hostname(cfg, option, len); + /* fallthrough */ + default: + if (hex) { + snprintf(option, len, "-x %d:", num); + strlcat(option, hex, len); + strlcat(option, " ", len); + } else { + /* string value */ + snprintf(option, len, "-x %d:'\"%s\"' ", num, val); + } + break; + } + } else { + struct { int num; char *(*cb)(const char *, char *, size_t); } opt[] = { + { 50, ip_cache_cb }, /* address */ + { 81, NULL }, /* fqdn */ + }; + + for (size_t i = 0; i < NELEMS(opt); i++) { + if (num != opt[i].num) + continue; + + if (!opt[i].cb || !opt[i].cb(ifname, option, len)) + return NULL; + + return option; + } + + snprintf(option, len, "-O %d ", num); + } + + return option; +} + +char *dhcp_compose_options(struct lyd_node *cfg, const char *ifname, char **options, + struct lyd_node *id, const char *val, const char *hex, + char *(*ip_cache_cb)(const char *, char *, size_t)) +{ + char opt[300]; + + if (!dhcp_compose_option(cfg, ifname, id, val, hex, opt, sizeof(opt), ip_cache_cb)) + return *options; + + if (*options) { + char *opts; + + opts = realloc(*options, strlen(*options) + strlen(opt) + 1); + if (!opts) { + ERROR("failed reallocating options: %s", strerror(errno)); + free(*options); + return NULL; + } + + *options = strcat(opts, opt); + } else + *options = strdup(opt); + + return *options; +} + +/* + * Default DHCP options for udhcpc, from networking/udhcp/common.c OPTION_REQ + */ +static void infer_options_v4(sr_session_ctx_t *session, const char *xpath) +{ + const char *opt[] = { + "netmask", + "broadcast", + "router", + "domain", + "hostname", /* server may use this to register our current name */ + "dns-server", + "ntp-server" /* will not be activated unless ietf-system also is */ + }; + size_t i; + + for (i = 0; i < NELEMS(opt); i++) + srx_set_item(session, NULL, 0, "%s/option[id='%s']", xpath, opt[i]); +} + +/* + * 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. + */ +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 */ + }; + size_t i; + + for (i = 0; i < NELEMS(opt); i++) + srx_set_item(session, NULL, 0, "%s/option[id='%s']", xpath, opt[i]); +} + +/* + * Called from ietf-interfaces.c ifchange_cand() to infer DHCP options + * for both DHCPv4 and DHCPv6 client configurations + */ +int ifchange_cand_infer_dhcp(sr_session_ctx_t *session, const char *xpath) +{ + sr_error_t err = SR_ERR_OK; + char *path, *ptr; + size_t cnt = 0; + + /* Extract path up to and including the dhcp container */ + path = strdup(xpath); + if (!path) + return SR_ERR_SYS; + + /* Find the dhcp container in the path */ + ptr = strstr(path, ":dhcp"); + if (!ptr) { + free(path); + return SR_ERR_OK; + } + + /* Move past ":dhcp" to find end of container name */ + ptr += 5; /* strlen(":dhcp") */ + + /* If there's more after dhcp (like /arping), truncate it */ + if (*ptr == '/') + *ptr = '\0'; + + /* Check if options already exist */ + err = srx_nitems(session, &cnt, "%s/option", path); + if (err || cnt) { + ERROR("%s(): no %s/options err %d cnt %zu", __func__, path, err, cnt); + goto out; + } + + /* Infer options based on IPv4 or IPv6 */ + if (strstr(path, ":ipv4/")) + infer_options_v4(session, path); + else if (strstr(path, ":ipv6/")) + infer_options_v6(session, path); + +out: + free(path); + return err; +} diff --git a/src/confd/src/infix-dhcp-common.h b/src/confd/src/infix-dhcp-common.h new file mode 100644 index 00000000..5eb36093 --- /dev/null +++ b/src/confd/src/infix-dhcp-common.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#ifndef INFIX_DHCP_COMMON_H_ +#define INFIX_DHCP_COMMON_H_ + +#include + +/* DHCP option lookup and composition helpers */ +int dhcp_option_lookup(const struct lyd_node *id); +char *dhcp_hostname(struct lyd_node *cfg, char *str, size_t len); +char *dhcp_fqdn(const char *val, char *str, size_t len); +char *dhcp_os_name_version(char *str, size_t len); +char *dhcp_compose_option(struct lyd_node *cfg, const char *ifname, struct lyd_node *id, + const char *val, const char *hex, char *option, size_t len, + char *(*ip_cache_cb)(const char *, char *, size_t)); +char *dhcp_compose_options(struct lyd_node *cfg, const char *ifname, char **options, + struct lyd_node *id, const char *val, const char *hex, + char *(*ip_cache_cb)(const char *, char *, size_t)); + +#endif /* INFIX_DHCP_COMMON_H_ */ diff --git a/src/confd/src/infix-dhcp-server.c b/src/confd/src/infix-dhcp-server.c index ffff1840..0e6bf373 100644 --- a/src/confd/src/infix-dhcp-server.c +++ b/src/confd/src/infix-dhcp-server.c @@ -2,13 +2,14 @@ #include #include +#include #include #include #include #include "core.h" -#include +#include "infix-dhcp-common.h" #define MODULE "infix-dhcp-server" #define ROOT_XPATH "/infix-dhcp-server:" diff --git a/src/confd/src/infix-dhcpv6-client.c b/src/confd/src/infix-dhcpv6-client.c new file mode 100644 index 00000000..a5c8807d --- /dev/null +++ b/src/confd/src/infix-dhcpv6-client.c @@ -0,0 +1,167 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "core.h" +#include "infix-dhcp-common.h" + +#define MODULE "infix-dhcpv6-client" +#define XPATH "/ietf-interfaces:interfaces/interface/ietf-ip:ipv6/infix-dhcpv6-client:dhcp" + +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 "; + + return strdup(defaults); +} + +static char *dhcp_options_v6(const char *ifname, struct lyd_node *cfg, bool *request_pd) +{ + struct lyd_node *option; + char *options = NULL; + + *request_pd = false; + + LYX_LIST_FOR_EACH(lyd_child(cfg), option, "option") { + struct lyd_node *id = lydx_get_child(option, "id"); + const char *val = lydx_get_cattr(option, "value"); + const char *hex = lydx_get_cattr(option, "hex"); + const char *name = lyd_get_value(id); + int num = dhcp_option_lookup(id); + + /* ia-pd option requires -d flag, not -O 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); + } + + return options ?: fallback_options_v6(ifname); +} + +static void add_v6(const char *ifname, struct lyd_node *cfg) +{ + const char *metric = lydx_get_cattr(cfg, "route-preference"); + 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 }; + bool request_pd = false; + FILE *fp; + + if (lydx_is_enabled(cfg, "information-only")) + strlcpy(info_only, "-l ", sizeof(info_only)); + + if (duid && duid[0]) { + size_t len = strlen(duid) + 32; + + client_duid = malloc(len); + if (!client_duid) + goto generr; + + snprintf(client_duid, len, "-x 1:%s ", duid); + } + + options = dhcp_options_v6(ifname, cfg, &request_pd); + if (request_pd) + strlcpy(prefix_del, "-d ", sizeof(prefix_del)); + + fp = fopenf("w", "/etc/finit.d/available/dhcpv6-client-%s.conf", ifname); + if (!fp) { + generr: + ERRNO("failed creating DHCPv6 client %s: %s", ifname, strerror(errno)); + goto err; + } + + 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" + " -- DHCPv6 client @%s\n", + ifname, ifname, ifname, + info_only, prefix_del, + options ? "-o " : "", options ?: "", + ifname, client_duid ?: "", ifname); + fclose(fp); + action = "enable"; +err: + systemf("initctl -bfqn %s dhcpv6-client-%s", action, ifname); + if (options) + free(options); + if (client_duid) + free(client_duid); +} + +static void del_v6(const char *ifname) +{ + systemf("initctl -bfq delete dhcpv6-client-%s", ifname); +} + +int infix_dhcpv6_client_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, + sr_event_t event, struct confd *confd) +{ + struct lyd_node *ifaces, *difaces, *iface, *diface, *ipv6, *dhcp, *ddhcp; + sr_error_t err = 0; + + if (event != SR_EV_DONE) + return SR_ERR_OK; + + ifaces = lydx_get_descendant(config, "interfaces", "interface", NULL); + difaces = lydx_get_descendant(diff, "interfaces", "interface", NULL); + + /* find the modified interfaces */ + LYX_LIST_FOR_EACH(difaces, diface, "interface") { + const char *ifname = lydx_get_cattr(diface, "name"); + struct lyd_node *dipv6; + + dipv6 = lydx_get_descendant(lyd_child(diface), "ipv6", NULL); + if (!dipv6) + continue; + + ddhcp = lydx_get_descendant(lyd_child(dipv6), "dhcp", NULL); + if (!ddhcp) + continue; + + /* Check if dhcp container was deleted */ + if (lydx_get_op(ddhcp) == LYDX_OP_DELETE) { + del_v6(ifname); + continue; + } + + /* Find corresponding interface in config to check if dhcp is present */ + LYX_LIST_FOR_EACH(ifaces, iface, "interface") { + if (strcmp(ifname, lydx_get_cattr(iface, "name"))) + continue; + + ipv6 = lydx_get_descendant(lyd_child(iface), "ipv6", NULL); + if (!ipv6) { + del_v6(ifname); + break; + } + + dhcp = lydx_get_descendant(lyd_child(ipv6), "dhcp", NULL); + if (!dhcp) + del_v6(ifname); + else + add_v6(ifname, dhcp); + break; + } + } + + return err; +} diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index ba92826e..09afa000 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -29,8 +29,9 @@ MODULES=( "infix-routing@2025-11-12.yang" "ieee802-dot1ab-lldp@2022-03-15.yang" "infix-lldp@2025-05-05.yang" - "infix-dhcp-common@2025-01-29.yang" - "infix-dhcp-client@2025-11-02.yang" + "infix-dhcp-common@2025-11-09.yang" + "infix-dhcp-client@2025-11-09.yang" + "infix-dhcpv6-client@2025-11-09.yang" "infix-dhcp-server@2025-10-28.yang" "infix-firewall@2025-04-26.yang" "infix-firewall-services@2025-04-26.yang" diff --git a/src/confd/yang/confd/infix-dhcp-client.yang b/src/confd/yang/confd/infix-dhcp-client.yang index 9abe21fa..1279d5e6 100644 --- a/src/confd/yang/confd/infix-dhcp-client.yang +++ b/src/confd/yang/confd/infix-dhcp-client.yang @@ -1,6 +1,6 @@ module infix-dhcp-client { yang-version 1.1; - namespace "urn:ietf:params:xml:ns:yang:dhcp-client"; + namespace "urn:infix:params:xml:ns:yang:dhcp-client"; prefix dhcp-client; import ietf-interfaces { @@ -16,6 +16,10 @@ module infix-dhcp-client { contact "kernelkit@googlegroups.com"; description "This module implements a DHCPv4 client"; + revision 2025-11-09 { + description "Fix namespace to use infix instead of ietf."; + reference "internal"; + } revision 2025-11-02 { description "Migrate DHCP client to ietf-ip augment. - Relocate from /dhcp-client to /interfaces/interface/ipv4/dhcp diff --git a/src/confd/yang/confd/infix-dhcp-client@2025-11-02.yang b/src/confd/yang/confd/infix-dhcp-client@2025-11-09.yang similarity index 100% rename from src/confd/yang/confd/infix-dhcp-client@2025-11-02.yang rename to src/confd/yang/confd/infix-dhcp-client@2025-11-09.yang diff --git a/src/confd/yang/confd/infix-dhcp-common.yang b/src/confd/yang/confd/infix-dhcp-common.yang index 839aac30..8a880e91 100644 --- a/src/confd/yang/confd/infix-dhcp-common.yang +++ b/src/confd/yang/confd/infix-dhcp-common.yang @@ -7,6 +7,10 @@ module infix-dhcp-common { contact "kernelkit@googlegroups.com"; description "Shared types between DHCP server and client."; + revision 2025-11-09 { + description "Add DHCPv6 options typedef."; + reference "internal"; + } revision 2025-01-29 { description "Initial revision."; reference "internal"; @@ -113,4 +117,73 @@ module infix-dhcp-common { pattern '([0-9a-fA-F]{2}:)*[0-9a-fA-F]{2}'; } } + + typedef duid { + description "DHCPv6 Unique Identifier (DUID), RFC 8415 section 11. + + A DUID consists of a two-octet type code and a variable length + identifier field. The minimum length is 4 octets (type + data). + + Common DUID types: + - DUID-LLT (type 1): Link-layer address plus time + - DUID-EN (type 2): Enterprise number + - DUID-LL (type 3): Link-layer address + - DUID-UUID (type 4): UUID + + Format: Colon-separated hex octets, minimum 4 octets. + Example: 00:03:00:01:08:00:27:fe:8c:2a (DUID-LL with Ethernet MAC)"; + type string { + pattern '([0-9a-fA-F]{2}:){3,}[0-9a-fA-F]{2}'; + /* Minimum 4 octets: ([0-9a-fA-F]{2}:){3,} matches at least 3 colons, + * plus final [0-9a-fA-F]{2} makes 4 octets total */ + } + } + + typedef options-v6 { + description "DHCPv6 options for client."; + type union { + type uint16 { + range "1..65535"; + } + type enumeration { + enum client-id { + value 1; + description "Client Identifier (DUID), RFC 8415. + Automatically generated by default."; + } + enum server-id { + value 2; + description "Server Identifier (DUID), RFC 8415."; + } + enum dns-server { + value 23; + description "DNS recursive name servers, RFC 3646."; + } + enum domain-search { + value 24; + description "Domain search list, RFC 3646."; + } + enum ia-pd { + value 25; + description "Identity Association for Prefix Delegation, RFC 8415."; + } + enum ntp-server { + value 56; + description "NTP time servers, RFC 5908."; + } + enum client-fqdn { + value 39; + description "Client Fully Qualified Domain Name, RFC 4704."; + } + enum information-refresh-time { + value 32; + description "Information refresh time for stateless DHCPv6, RFC 8415."; + } + enum sntp-server { + value 31; + description "Simple Network Time Protocol servers, RFC 4075."; + } + } + } + } } diff --git a/src/confd/yang/confd/infix-dhcp-common@2025-01-29.yang b/src/confd/yang/confd/infix-dhcp-common@2025-11-09.yang similarity index 100% rename from src/confd/yang/confd/infix-dhcp-common@2025-01-29.yang rename to src/confd/yang/confd/infix-dhcp-common@2025-11-09.yang diff --git a/src/confd/yang/confd/infix-dhcpv6-client.yang b/src/confd/yang/confd/infix-dhcpv6-client.yang new file mode 100644 index 00000000..690ecafb --- /dev/null +++ b/src/confd/yang/confd/infix-dhcpv6-client.yang @@ -0,0 +1,140 @@ +module infix-dhcpv6-client { + yang-version 1.1; + namespace "urn:infix:params:xml:ns:yang:dhcpv6-client"; + prefix dhcpv6-client; + + import ietf-interfaces { + prefix if; + } + import ietf-ip { + prefix ip; + } + import infix-dhcp-common { + prefix dhcp; + } + + organization "KernelKit"; + contact "kernelkit@googlegroups.com"; + description "This module implements a DHCPv6 client"; + + revision 2025-11-09 { + description "Initial revision: + - Augment for /interfaces/interface/ipv6/dhcp + - Support for DHCPv6 options, DUID, prefix delegation"; + reference "rfc8415"; + } + + /* + * Typedefs + */ + + typedef route-preference { + type uint32; + description "This type is used for selecting route preference (distance)."; + } + + /* + * DHCPv6 Client Configuration + */ + + augment "/if:interfaces/if:interface/ip:ipv6" { + container dhcp { + presence "Enable DHCPv6 client for this interface."; + description "DHCPv6 client configuration"; + + leaf duid { + type dhcp:duid; + description "Optional DHCPv6 Unique Identifier (DUID), option 1, RFC 8415. + + When omitted, the client automatically generates a DUID + based on link-layer address plus time (DUID-LLT) or link-layer + address only (DUID-LL), depending on the implementation. + + This setting allows manual configuration of the DUID in + hexadecimal format. The DUID type and content are encoded + as defined in RFC 8415 section 11. + + For full control of the DUID format, use the generic option + list instead, which supports entering raw HEX data. + + Example: 00:03:00:01:08:00:27:fe:8c:2a + (DUID-LL with Ethernet address)"; + } + + leaf information-only { + type boolean; + must "not(. = 'true') or ../../ip:autoconf/ip:create-global-addresses = 'true' " + + "or ../../ip:autoconf/ip:create-temporary-addresses = 'true'" { + error-message "IPv6 autoconf (SLAAC) must be enabled when information-only is true"; + error-app-tag "invalid-dhcpv6-config"; + } + description "Use stateless DHCPv6 (information-request mode). + + When enabled, the client requests configuration information + (like DNS servers) but does not request IPv6 addresses or + prefixes. This is used with SLAAC (Stateless Address + Autoconfiguration) when only additional configuration is + needed from DHCPv6."; + } + + list option { + key "id"; + description "List of DHCPv6 options to request (and accept). + + The default is an empty list, meaning all supported options. To + request specific options, add them here. For prefix delegation, + include the ia-pd option in the list."; + + must "not(id = 'client-fqdn' and hex)" { + error-message "Client FQDN option must use string format"; + error-app-tag "invalid-fqdn-format"; + } + + leaf id { + type dhcp:options-v6; + description "DHCPv6 option to request from, or inform server of."; + } + + choice value { + case value { + leaf value { + description "Optional string value to send to server. + + Example: + option:client-fqdn, value:host.example.com + + This will send option 39 with the specified FQDN, + requesting the server to update DNS records."; + type string; + } + } + case hex { + leaf hex { + description "Optional binary value in hexadecimal format. + + This allows full control over the option payload, + useful for vendor-specific options or custom data. + + Example: + option:client-id, hex:00:03:00:01:08:00:27:fe:8c:2a + + This would set a DUID-LL (type 3) with the specified + link-layer address."; + type dhcp:octet-string; + } + } + } + } + + leaf route-preference { + type route-preference; + default 5; + description + "The preference (administrative distance) that all DHCPv6 routes + are installed with. The default preference (5) is higher (less worth) + than static routes, but lower than those learned via dynamic routing + protocols, like OSPF."; + } + } + } +} diff --git a/src/confd/yang/confd/infix-dhcpv6-client@2025-11-09.yang b/src/confd/yang/confd/infix-dhcpv6-client@2025-11-09.yang new file mode 120000 index 00000000..12d2c890 --- /dev/null +++ b/src/confd/yang/confd/infix-dhcpv6-client@2025-11-09.yang @@ -0,0 +1 @@ +infix-dhcpv6-client.yang \ No newline at end of file