dhcp-message: introduce dhcp_message_get_option_dnr()

This is for DHCP option 162 (DNR).
This commit is contained in:
Yu Watanabe
2026-05-13 09:41:24 +09:00
parent 64846d9d8a
commit 7e612d5c15
3 changed files with 354 additions and 0 deletions
+159
View File
@@ -9,6 +9,7 @@
#include "dhcp-route.h"
#include "dns-def.h"
#include "dns-domain.h"
#include "dns-resolver-internal.h"
#include "errno-util.h"
#include "ether-addr-util.h"
#include "hostname-util.h"
@@ -20,6 +21,7 @@
#include "set.h"
#include "sort-util.h"
#include "string-util.h"
#include "unaligned.h"
static sd_dhcp_message* dhcp_message_free(sd_dhcp_message *message) {
if (!message)
@@ -1164,6 +1166,163 @@ int dhcp_message_get_option_length_prefixed_data(
return 0;
}
static int parse_dnr_one(const struct iovec *iov, sd_dns_resolver *ret) {
int r;
assert(iovec_is_set(iov));
assert(ret);
_cleanup_(sd_dns_resolver_done) sd_dns_resolver resolver = {};
struct iovec i = *iov;
/* service priority */
if (i.iov_len < sizeof(be16_t))
return -EBADMSG;
resolver.priority = unaligned_read_be16(i.iov_base);
iovec_inc(&i, sizeof(be16_t));
/* RFC 9460 section 2.4.1:
* When SvcPriority is 0, the SVCB record is in AliasMode.
*
* We do not support the alias mode. But the entry itself is not invalid. */
if (resolver.priority == 0) {
*ret = (sd_dns_resolver) {};
return 0;
}
/* authentication domain name */
if (!iovec_is_set(&i))
return -EBADMSG;
size_t name_len = *(uint8_t*) i.iov_base;
iovec_inc(&i, 1);
if (i.iov_len < name_len)
return -EBADMSG;
const uint8_t *name_buf = i.iov_base;
iovec_inc(&i, name_len);
r = dns_name_from_wire_format(&name_buf, &name_len, &resolver.auth_name);
if (r < 0)
return r;
if (r == 0 || name_len != 0)
return -EBADMSG;
r = dns_name_is_valid_ldh(resolver.auth_name);
if (r < 0)
return r;
if (r == 0)
return -EBADMSG;
if (dns_name_is_root(resolver.auth_name))
return -EBADMSG;
/* RFC9463 section 3.1.6: In ADN-only mode, server omits everything after the ADN.
*
* We don't support these, but they are not invalid. */
if (!iovec_is_set(&i)) {
*ret = (sd_dns_resolver) {};
return 0;
}
/* IPv4 addresses */
size_t n = *(uint8_t*) i.iov_base;
iovec_inc(&i, 1);
if (n % sizeof(struct in_addr) != 0)
return -EBADMSG;
n /= sizeof(struct in_addr);
/* RFC9463 section 3.1.8: option MUST include at least one valid IP addr */
if (n == 0)
return -EBADMSG;
resolver.family = AF_INET;
resolver.n_addrs = n;
resolver.addrs = new(union in_addr_union, n);
if (!resolver.addrs)
return -ENOMEM;
for (size_t j = 0; j < n; j++) {
if (i.iov_len < sizeof(struct in_addr))
return -EBADMSG;
struct in_addr a;
memcpy(&a, i.iov_base, sizeof(struct in_addr));
iovec_inc(&i, sizeof(struct in_addr));
/* RFC9463 section 5.2: client MUST discard multicast and host loopback addresses */
if (in4_addr_is_multicast(&a) || in4_addr_is_localhost(&a))
return -EBADMSG;
resolver.addrs[j] = (union in_addr_union) { .in = a };
}
/* service params */
r = dnr_parse_svc_params(i.iov_base, i.iov_len, &resolver);
if (r < 0)
return r;
if (r == 0) {
/* We can't use this record, but it is not invalid. */
*ret = (sd_dns_resolver) {};
return 0;
}
*ret = TAKE_STRUCT(resolver);
return 1;
}
int dhcp_message_get_option_dnr(sd_dhcp_message *message, size_t *ret_n_resolvers, sd_dns_resolver **ret_resolvers) {
int r;
assert(message);
assert(ret_n_resolvers || !ret_resolvers);
/* See RFC 9463 section 5.1 */
_cleanup_(iovw_done_free) struct iovec_wrapper iovw = {};
r = dhcp_message_get_option_length_prefixed_data(message, SD_DHCP_OPTION_V4_DNR, /* length_size= */ 2, &iovw);
if (r < 0)
return r;
sd_dns_resolver *resolvers = NULL;
size_t n_resolvers = 0;
CLEANUP_ARRAY(resolvers, n_resolvers, dns_resolver_free_array);
FOREACH_ARRAY(i, iovw.iovec, iovw.count) {
_cleanup_(sd_dns_resolver_done) sd_dns_resolver dnr = {};
r = parse_dnr_one(i, &dnr);
if (r < 0)
return r;
if (r == 0)
continue;
if (!ret_resolvers) {
n_resolvers++;
continue;
}
if (!GREEDY_REALLOC(resolvers, n_resolvers + 1))
return -ENOMEM;
resolvers[n_resolvers++] = TAKE_STRUCT(dnr);
}
if (n_resolvers == 0) /* no supported resolver */
return -ENODATA;
if (ret_resolvers) {
/* Sort the resolvers with their priorities. */
typesafe_qsort(resolvers, n_resolvers, dns_resolver_prio_compare);
*ret_resolvers = TAKE_PTR(resolvers);
}
if (ret_n_resolvers)
*ret_n_resolvers = n_resolvers;
return 0;
}
static int dhcp_message_verify_header(
const struct iovec *iov,
uint8_t op,
+1
View File
@@ -79,6 +79,7 @@ int dhcp_message_get_option_hostname(sd_dhcp_message *message, char **ret);
int dhcp_message_get_option_domains(sd_dhcp_message *message, uint8_t code, char ***ret);
int dhcp_message_get_option_sub_tlv(sd_dhcp_message *message, uint8_t code, TLVFlag flags, TLV **ret);
int dhcp_message_get_option_length_prefixed_data(sd_dhcp_message *message, uint8_t code, size_t length_size, struct iovec_wrapper *ret);
int dhcp_message_get_option_dnr(sd_dhcp_message *message, size_t *ret_n_resolvers, sd_dns_resolver **ret_resolvers);
int dhcp_message_parse(
const struct iovec *iov,
+194
View File
@@ -7,6 +7,8 @@
#include "dhcp-message.h"
#include "dhcp-protocol.h"
#include "dhcp-route.h"
#include "dns-packet.h"
#include "dns-resolver-internal.h"
#include "ether-addr-util.h"
#include "iovec-util.h"
#include "iovec-wrapper.h"
@@ -575,4 +577,196 @@ TEST(domains) {
test_domains_fail(ELEMENTSOF(truncated), truncated);
}
TEST(dnr) {
_cleanup_(sd_dhcp_message_unrefp) sd_dhcp_message *m = NULL;
ASSERT_OK(dhcp_message_new(&m));
sd_dns_resolver *resolvers = NULL;
size_t n_resolvers = 0;
CLEANUP_ARRAY(resolvers, n_resolvers, dns_resolver_free_array);
static uint8_t data[] = {
/* Instance 1 */
/* length */
0, 78,
/* priority */
0, 1,
/* authentication domain name */
22,
8, 'r', 'e', 's', 'o', 'l', 'v', 'e', 'r',
7, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
3, 'c', 'o', 'm',
0,
/* addresses */
8,
192, 0, 2, 1,
192, 0, 2, 2,
/* service parameters */
/* ALPN */
0, DNS_SVC_PARAM_KEY_ALPN,
0, 14,
2, 'h', '2',
2, 'h', '3',
3, 'd', 'o', 't',
3, 'd', 'o', 'q',
/* port */
0, DNS_SVC_PARAM_KEY_PORT,
0, 2,
0, 42,
/* DoH path*/
0, DNS_SVC_PARAM_KEY_DOHPATH,
0, 16,
'/', 'd', 'n', 's', '-', 'q', 'u', 'e', 'r', 'y', '{', '?', 'd', 'n', 's', '}',
/* Instance 2 */
/* length */
0, 44,
/* priority */
0, 2,
/* authentication domain name */
22,
8, 'h', 'o', 'g', 'e', 'h', 'o', 'g', 'e',
7, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
3, 'c', 'o', 'm',
0,
/* addresses */
4,
192, 0, 2, 3,
/* service parameters */
/* ALPN */
0, DNS_SVC_PARAM_KEY_ALPN,
0, 4,
3, 'd', 'o', 't',
/* port */
0, DNS_SVC_PARAM_KEY_PORT,
0, 2,
0, 33,
/* Instance 3 (no address, ignored) */
/* length */
0, 20,
/* priority */
0, 3,
/* authentication domain name */
17,
3, 'f', 'o', 'o',
7, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
3, 'c', 'o', 'm',
0,
/* Instance 4 (unknown alpn, ignored) */
/* length */
0, 37,
/* priority */
0, 4,
/* authentication domain name */
20,
6, 'b', 'a', 'r', 'b', 'a', 'z',
7, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
3, 'c', 'o', 'm',
0,
/* addresses */
4,
192, 0, 2, 4,
/* service parameters */
/* ALPN */
0, DNS_SVC_PARAM_KEY_ALPN,
0, 5,
4, 'h', 'o', 'g', 'e',
};
ASSERT_OK(dhcp_message_append_option(m, SD_DHCP_OPTION_V4_DNR, ELEMENTSOF(data), data));
ASSERT_OK(dhcp_message_get_option_dnr(m, &n_resolvers, &resolvers));
ASSERT_EQ(n_resolvers, 2u);
ASSERT_EQ(resolvers[0].priority, 1u);
ASSERT_STREQ(resolvers[0].auth_name, "resolver.example.com");
ASSERT_EQ(resolvers[0].family, AF_INET);
ASSERT_EQ(resolvers[0].n_addrs, 2u);
ASSERT_STREQ(IN_ADDR_TO_STRING(resolvers[0].family, &resolvers[0].addrs[0]), "192.0.2.1");
ASSERT_STREQ(IN_ADDR_TO_STRING(resolvers[0].family, &resolvers[0].addrs[1]), "192.0.2.2");
ASSERT_EQ(resolvers[0].transports, SD_DNS_ALPN_HTTP_2_TLS | SD_DNS_ALPN_HTTP_3 | SD_DNS_ALPN_DOT | SD_DNS_ALPN_DOQ);
ASSERT_EQ(resolvers[0].port, 42u);
ASSERT_STREQ(resolvers[0].dohpath, "/dns-query{?dns}");
ASSERT_EQ(resolvers[1].priority, 2u);
ASSERT_STREQ(resolvers[1].auth_name, "hogehoge.example.com");
ASSERT_EQ(resolvers[1].family, AF_INET);
ASSERT_EQ(resolvers[1].n_addrs, 1u);
ASSERT_STREQ(IN_ADDR_TO_STRING(resolvers[1].family, &resolvers[1].addrs[0]), "192.0.2.3");
ASSERT_EQ(resolvers[1].transports, SD_DNS_ALPN_DOT);
ASSERT_EQ(resolvers[1].port, 33u);
ASSERT_NULL(resolvers[1].dohpath);
/* missing DoH path */
static uint8_t invalid[] = {
/* length */
0, 35,
/* priority */
0, 5,
/* authentication domain name */
20,
6, 'b', 'a', 'r', 'b', 'a', 'z',
7, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
3, 'c', 'o', 'm',
0,
/* addresses */
4,
192, 0, 2, 5,
/* service parameters */
/* ALPN */
0, DNS_SVC_PARAM_KEY_ALPN,
0, 3,
2, 'h', '2',
};
ASSERT_OK(dhcp_message_append_option(m, SD_DHCP_OPTION_V4_DNR, ELEMENTSOF(invalid), invalid));
ASSERT_ERROR(dhcp_message_get_option_dnr(m, NULL, NULL), EBADMSG);
dhcp_message_remove_option(m, SD_DHCP_OPTION_V4_DNR);
/* missing ALPN */
static uint8_t invalid2[] = {
/* length */
0, 28,
/* priority */
0, 6,
/* authentication domain name */
20,
6, 'b', 'a', 'r', 'b', 'a', 'z',
7, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
3, 'c', 'o', 'm',
0,
/* addresses */
4,
192, 0, 2, 6,
};
ASSERT_OK(dhcp_message_append_option(m, SD_DHCP_OPTION_V4_DNR, ELEMENTSOF(invalid2), invalid2));
ASSERT_ERROR(dhcp_message_get_option_dnr(m, NULL, NULL), EBADMSG);
dhcp_message_remove_option(m, SD_DHCP_OPTION_V4_DNR);
/* truncated domain name */
static uint8_t invalid3[] = {
/* length */
0, 34,
/* priority */
0, 7,
/* authentication domain name */
18,
6, 'b', 'a', 'r', 'b', 'a', 'z',
7, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
3, 'c', 'o',
/* addresses */
4,
192, 0, 2, 7,
/* service parameters */
/* ALPN */
0, DNS_SVC_PARAM_KEY_ALPN,
0, 4,
3, 'd', 'o', 't',
};
ASSERT_OK(dhcp_message_append_option(m, SD_DHCP_OPTION_V4_DNR, ELEMENTSOF(invalid3), invalid3));
ASSERT_ERROR(dhcp_message_get_option_dnr(m, NULL, NULL), EMSGSIZE);
}
DEFINE_TEST_MAIN(LOG_DEBUG);