confd: refactor dhcp client/server modeling of client-id

Instead of encoding hex data in regular strings, using an 'id:' prefix,
or guessing based on pattern in C code, we have decided for a breaking
change in the DHCP client model.

This commit introduces string and hex values for client-id in the models
for both DHCP client and server.  The *breaking* part of the changes are
strictly related to how a user pass binary data as colon-separated hex
digits.  The dedicated leaf node `client-id` is now reserved for string
values and the generic `list option` can be used to input user defined
string or hex values.  Even non-conformant client-id options can now be
encoded.  All described in detail in infix-dhcp-client.yang

The server model has been updated to be able to send generic options in
hex format, and the static host lease matcher for client-id now require
a 'str' or 'hex' modifier.

Note: dnsmasq is RFC compliant and will not match non-conforming option
      61 (client-id) even though the client can send such payload.

To increase test coverage, the server_host test has been updated to
check both string and hex client-id.  Matching on hostname is already
covered by other test(s).

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2025-01-31 13:56:40 +01:00
parent af8a90d651
commit 340330b75b
7 changed files with 163 additions and 83 deletions
+19 -39
View File
@@ -71,9 +71,9 @@ static char *hostname(struct lyd_node *cfg, char *str, size_t len)
return str;
}
static char *fqdn(const char *value, char *str, size_t len)
static char *fqdn(const char *val, char *str, size_t len)
{
snprintf(str, len, "-F \"%s\" ", value);
snprintf(str, len, "-F \"%s\" ", val);
return str;
}
@@ -104,26 +104,8 @@ static char *os_name_version(char *str, size_t len)
return str;
}
static bool is_hex(const char *s)
{
while (s[0] && s[1]) {
if (!isxdigit(s[0]) || !isxdigit(s[1]))
return false;
s += 2;
if (*s == '\0')
return true;
if (*s != ':')
return false;
s++;
}
return false;
}
static char *compose_option(struct lyd_node *cfg, const char *ifname, struct lyd_node *id,
const char *value, char *option, size_t len)
const char *val, const char *hex, char *option, size_t len)
{
const char *name = lyd_get_value(id);
int num = dhcp_option_lookup(id);
@@ -133,22 +115,24 @@ static char *compose_option(struct lyd_node *cfg, const char *ifname, struct lyd
return NULL;
}
if (value) {
if (val || hex) {
switch (num) {
case 81: /* fqdn */
return fqdn(value, option, len);
if (!val)
return NULL;
return fqdn(val, option, len);
case 12: /* hostname */
if (!strcmp(value, "auto"))
if (val && !strcmp(val, "auto"))
return hostname(cfg, option, len);
/* fallthrough */
default:
if (is_hex(value)) {
if (hex) {
snprintf(option, len, "-x %d:", num);
strlcat(option, value, len);
strlcat(option, hex, len);
strlcat(option, " ", len);
} else {
/* string value */
snprintf(option, len, "-x %d:'\"%s\"' ", num, value);
snprintf(option, len, "-x %d:'\"%s\"' ", num, val);
}
break;
}
@@ -175,11 +159,11 @@ static char *compose_option(struct lyd_node *cfg, const char *ifname, struct lyd
}
static char *compose_options(struct lyd_node *cfg, const char *ifname, char **options,
struct lyd_node *id, const char *value)
struct lyd_node *id, const char *val, const char *hex)
{
char opt[300];
if (!compose_option(cfg, ifname, id, value, opt, sizeof(opt)))
if (!compose_option(cfg, ifname, id, val, hex, opt, sizeof(opt)))
return *options;
if (*options) {
@@ -220,8 +204,9 @@ static char *dhcp_options(const char *ifname, struct lyd_node *cfg)
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");
options = compose_options(cfg, ifname, &options, id, val);
options = compose_options(cfg, ifname, &options, id, val, hex);
}
return options ?: fallback_options(ifname);
@@ -249,16 +234,11 @@ static void add(const char *ifname, struct lyd_node *cfg)
goto generr;
strlcpy(cid, "-C -x 61:00", len);
if (is_hex(client_id)) {
strlcat(cid, ":", len);
strlcat(cid, client_id, len);
} else {
for (size_t i = 0; client_id[i]; i++) {
char hex[5];
for (size_t i = 0; client_id[i]; i++) {
char hex[5];
snprintf(hex, sizeof(hex), ":%02x", client_id[i]);
strlcat(cid, hex, len);
}
snprintf(hex, sizeof(hex), ":%02x", client_id[i]);
strlcat(cid, hex, len);
}
}
+15 -3
View File
@@ -84,6 +84,7 @@ static int configure_options(FILE *fp, struct lyd_node *cfg, const char *tag)
*/
val = lydx_get_cattr(option, "name")
?: lydx_get_cattr(option, "string")
?: lydx_get_cattr(option, "hex")
?: NULL;
if (!val) {
val = lydx_get_cattr(option, "address");
@@ -118,7 +119,7 @@ static const char *host_match(struct lyd_node *match, const char **id)
{
struct {
const char *key;
const char *prefix;
const char *prefix; /* dnsmasq prefix */
} choice[] = {
{ "mac-address", NULL },
{ "hostname", NULL },
@@ -129,14 +130,25 @@ static const char *host_match(struct lyd_node *match, const char **id)
return NULL;
for (size_t i = 0; i < NELEMS(choice); i++) {
struct lyd_node *node;
struct lyd_node *node, *sub;
const char *value;
node = lydx_get_child(match, choice[i].key);
if (!node)
continue;
*id = choice[i].prefix;
return lyd_get_value(node);
value = lyd_get_value(node);
if (value)
return value;
/* The client-id setting is has a qualifier */
sub = lydx_get_child(node, "str");
if (sub)
return lyd_get_value(sub);
sub = lydx_get_child(node, "hex");
if (sub)
return lyd_get_value(sub);
}
return NULL;
+81 -16
View File
@@ -52,55 +52,120 @@ module infix-dhcp-client {
*/
container dhcp-client {
description
"DHCPv4 client configuration";
description "DHCPv4 client configuration";
leaf enabled {
type boolean;
default "true";
description "Globally enables the DHCP client function.";
}
list client-if {
key "if-name";
description "List of interfaces requesting DHCPv4 configuration.";
key "if-name";
leaf if-name {
type if:interface-ref;
mandatory true;
description "Name of the interface.";
}
leaf enabled {
type boolean;
default "true";
description "Enable DHCP client for this interface.";
}
leaf client-id {
type string;
description "Optional Client ID, option 61, default: MAC address.";
description "Optional Client ID, option 61, RFC 2132.
When omitted the client sends its MAC address as client-id.
RFC 2132 defines client-id as [ OPT | LEN | TYPE | DATA ],
where TYPE is 0x01 for MAC address and 0x00 for anything
else. Servers usually just do a binary match of the data
using the length field. This particular setting encodes
the value as an ASCII string without trailing zero.
For full control of both type and data fields, use generic
option list instead, which supports entering raw HEX data.";
}
leaf arping {
type boolean;
default "true";
description "ARP for lease to check for IP address collisions (slow).";
}
list option {
key "id";
description
"List of DHCP options to request (and accept). The default is an
empty list, meaning all supported options. To restrict the
client to only get IP address and default route, set this to:
'subnet router'";
description "List of DHCP options to request (and accept).
The default is an empty list, meaning all supported options. To
restrict the client to only get IP address and default route, set
this to: 'subnet router'";
must "not(id = 'fqdn' and 'value/hex')" {
error-message "FQDN option must use string format";
error-app-tag "invalid-fqdn-format";
}
leaf id {
type dhcp:options;
description "DHCP option to request from, or inform server of.";
}
leaf value {
type string;
description "Optional value, only used for non-flag request options.
Example: option:hostname, value:xyzzy
option:clientid, value:01:02:03:04:05:06:07:08:09:0a
option:0x51, value:xyzzy.example.com";
must "../id != 'hostname' or re-match(., '[a-zA-Z0-9\\-_]{1,64}')";
choice value {
case value {
leaf value {
description "Optional value, to inform server, e.g., hostname.
Example 1)
option:12, value:xyzzy
Example 2)
option:hostname, value:xyzzy
Will be sent as [12 | 05 | 78 | 79 | 80 | 80 | 79],
which the server can use for static host matching.
For the 'hostname' option the 'auto' keyword can be
used to send the hostname part from IETF system.";
type string;
must "../id != 'hostname' or re-match(., '[a-zA-Z0-9\\-_]{1,64}')";
}
}
case hex {
leaf hex {
description "Optional binary value, to inform server, e.g., client-id.
This is a raw payload option allowing full control of
what is sent in an option. Often used with options 61
and option 43 (vendor-specific).
Example 1)
option:client-id, hex:00:c0:ff:ee
Will be sent as : [61 | 03 | 00 | c0 | ff | ee], which
is the RFC conformant formatting of option 61.
Example 2)
option:client-id, hex:c0:ff:ee
Will be sent as : [61 | 03 | c0 | ff | ee] meaning the
'htype' will be set to 'c0', while the RFC only defines
'00' (string) and and '01' (mac) for this field, some
users may want full control over this option.";
type dhcp:octet-string;
}
}
}
}
leaf route-preference {
type route-preference;
default 5;
+7
View File
@@ -106,4 +106,11 @@ module infix-dhcp-common {
}
}
}
typedef octet-string {
description "A generic string or a hex values.";
type string {
pattern '([0-9a-fA-F]{2}:)*[0-9a-fA-F]{2}';
}
}
}
+30 -16
View File
@@ -25,16 +25,6 @@ module infix-dhcp-server {
reference "internal";
}
typedef octet-string {
description "A generic string or a hex value if prefixed with 'id:'.";
type union {
type string {
pattern 'id:([0-9a-fA-F]{2}:)*[0-9a-fA-F]{2}';
}
type string;
}
}
typedef dhcp-lease-time {
description "The lease time in seconds, with a minimum of 2 minutes (120 seconds).
For static host entries, the value 'infinite' may be used.";
@@ -104,6 +94,15 @@ module infix-dhcp-server {
}
}
case hex-opt {
leaf hex {
description "Binary data as colon-separated pairs of hex digits.
Example: c0:ff:ee";
type dhcp:octet-string;
}
}
case default-opt {
leaf string {
description "Generic string value.";
@@ -214,15 +213,30 @@ module infix-dhcp-server {
}
case client-id {
leaf client-id {
description "Match on client-id (string 'abc' or hex 'id:c0:ff:ee', DHCP option 61.";
type union {
type yang:mac-address;
type octet-string;
container client-id {
description "Match on client-id, DHCP option 61.";
choice id {
description "Format of the client-id value";
default str;
case str {
leaf str {
description "String value for text-based client-id.
Example: xyzzy";
type string;
}
}
case hex {
leaf hex {
description "Binary data as colon-separated pairs of hex digits.
Example: c0:ff:ee";
type dhcp:octet-string;
}
}
}
}
}
/* case option82 here ... */
}
/* More advanced match rules, e.g., option82 + client-id are not supported. */
@@ -1,8 +1,8 @@
=== DHCP Server Static Host
==== Description
Verify DHCP server can hand out static host leases based on
a very long client-id, ensuring no pool address is handed
out instead.
Verify DHCP server can hand out static host leases based on client-id,
both hexadecimal and a very long string, ensuring no pool address is
handed out instead.
==== Topology
ifdef::topdoc[]
+8 -6
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
"""DHCP Server Static Host
Verify DHCP server can hand out static host leases based on
a very long client-id, ensuring no pool address is handed
out instead.
Verify DHCP server can hand out static host leases based on client-id,
both hexadecimal and a very long string, ensuring no pool address is
handed out instead.
"""
import infamy
import infamy.iface as iface
@@ -19,6 +20,7 @@ with infamy.Test() as test:
ADDRESS2 = '192.168.2.22'
GW2 = '192.168.2.1'
HOSTNM1 = 'foo'
HOSTCID1 = '00:c0:ff:ee' # Infix DHCP server is RFC compliant
HOSTNM11 = 'client1'
HOSTCID2 = 'xyzzydissiegillespiefoobarterrawinklesouponastick'
HOSTNM2 = 'bar'
@@ -70,7 +72,7 @@ with infamy.Test() as test:
"host": [{
"address": ADDRESS1,
"match": {
"hostname": HOSTNM1
"client-id": {"hex": HOSTCID1}
},
"option": [
{
@@ -94,7 +96,7 @@ with infamy.Test() as test:
"host": [{
"address": ADDRESS2,
"match": {
"client-id": HOSTCID2
"client-id": {"str": HOSTCID2}
},
"option": [
{
@@ -120,7 +122,7 @@ with infamy.Test() as test:
"if-name": client1["link"],
"option": [
{"id": "router"},
{"id": "hostname", "value": "auto"},
{"id": "client-id", "hex": HOSTCID1},
{"id": 121}
]
}]