summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLuca Boccassi <bluca@debian.org>2024-01-17 17:20:35 +0100
committerGitHub <noreply@github.com>2024-01-17 17:20:35 +0100
commit0a9735eac2823ab96121852c94c8b3f0efa1bdf7 (patch)
tree275d9383e5a13fa5f4fc91915f9c2fa15e8751c4
parentRestart the DHCPv4 client when max REQUEST attempts is reached (diff)
parenttest-resolve: add basic test for SVCB/HTTPS RRs (diff)
downloadsystemd-0a9735eac2823ab96121852c94c8b3f0efa1bdf7.tar.xz
systemd-0a9735eac2823ab96121852c94c8b3f0efa1bdf7.zip
Merge pull request #30661 from rpigott/resolved-https-record
resolved: support RFC 9460 SVCB and HTTPS records
-rw-r--r--src/basic/escape.c27
-rw-r--r--src/basic/escape.h1
-rw-r--r--src/resolve/resolved-dns-packet.c127
-rw-r--r--src/resolve/resolved-dns-packet.h19
-rw-r--r--src/resolve/resolved-dns-rr.c238
-rw-r--r--src/resolve/resolved-dns-rr.h23
-rw-r--r--src/shared/varlink-io.systemd.Resolve.Monitor.c4
-rw-r--r--src/test/test-escape.c18
-rw-r--r--test/knot-data/zones/test.zone3
-rwxr-xr-xtest/units/testsuite-75.sh6
10 files changed, 462 insertions, 4 deletions
diff --git a/src/basic/escape.c b/src/basic/escape.c
index 75a1d68e96..d95f35e798 100644
--- a/src/basic/escape.c
+++ b/src/basic/escape.c
@@ -471,6 +471,33 @@ char* octescape(const char *s, size_t len) {
return buf;
}
+char* decescape(const char *s, const char *bad, size_t len) {
+ char *buf, *t;
+
+ /* Escapes all chars in bad, in addition to \ and " chars, in \nnn decimal style escaping. */
+
+ assert(s || len == 0);
+
+ t = buf = new(char, len * 4 + 1);
+ if (!buf)
+ return NULL;
+
+ for (size_t i = 0; i < len; i++) {
+ uint8_t u = (uint8_t) s[i];
+
+ if (u < ' ' || u >= 127 || IN_SET(u, '\\', '"') || strchr(bad, u)) {
+ *(t++) = '\\';
+ *(t++) = '0' + (u / 100);
+ *(t++) = '0' + ((u / 10) % 10);
+ *(t++) = '0' + (u % 10);
+ } else
+ *(t++) = u;
+ }
+
+ *t = 0;
+ return buf;
+}
+
static char* strcpy_backslash_escaped(char *t, const char *s, const char *bad) {
assert(bad);
assert(t);
diff --git a/src/basic/escape.h b/src/basic/escape.h
index 318da6f220..65caf0dbcf 100644
--- a/src/basic/escape.h
+++ b/src/basic/escape.h
@@ -65,6 +65,7 @@ static inline char* xescape(const char *s, const char *bad) {
return xescape_full(s, bad, SIZE_MAX, 0);
}
char* octescape(const char *s, size_t len);
+char* decescape(const char *s, const char *bad, size_t len);
char* escape_non_printable_full(const char *str, size_t console_width, XEscapeFlags flags);
char* shell_escape(const char *s, const char *bad);
diff --git a/src/resolve/resolved-dns-packet.c b/src/resolve/resolved-dns-packet.c
index 44e1e4faab..a7d04449b2 100644
--- a/src/resolve/resolved-dns-packet.c
+++ b/src/resolve/resolved-dns-packet.c
@@ -1183,6 +1183,31 @@ int dns_packet_append_rr(DnsPacket *p, const DnsResourceRecord *rr, const DnsAns
r = dns_packet_append_blob(p, rr->tlsa.data, rr->tlsa.data_size, NULL);
break;
+ case DNS_TYPE_SVCB:
+ case DNS_TYPE_HTTPS:
+ r = dns_packet_append_uint16(p, rr->svcb.priority, NULL);
+ if (r < 0)
+ goto fail;
+
+ r = dns_packet_append_name(p, rr->svcb.target_name, false, false, NULL);
+ if (r < 0)
+ goto fail;
+
+ LIST_FOREACH(params, i, rr->svcb.params) {
+ r = dns_packet_append_uint16(p, i->key, NULL);
+ if (r < 0)
+ goto fail;
+
+ r = dns_packet_append_uint16(p, i->length, NULL);
+ if (r < 0)
+ goto fail;
+
+ r = dns_packet_append_blob(p, i->value, i->length, NULL);
+ if (r < 0)
+ goto fail;
+ }
+ break;
+
case DNS_TYPE_CAA:
r = dns_packet_append_uint8(p, rr->caa.flags, NULL);
if (r < 0)
@@ -1689,6 +1714,41 @@ static bool loc_size_ok(uint8_t size) {
return m <= 9 && e <= 9 && (m > 0 || e == 0);
}
+static bool dns_svc_param_is_valid(DnsSvcParam *i) {
+ if (!i)
+ return false;
+
+ switch (i->key) {
+ /* RFC 9460, section 7.1.1: alpn-ids must exactly fill SvcParamValue */
+ case DNS_SVC_PARAM_KEY_ALPN: {
+ size_t sz = 0;
+ if (i->length <= 0)
+ return false;
+ while (sz < i->length)
+ sz += 1 + i->value[sz]; /* N.B. will not overflow */
+ return sz == i->length;
+ }
+
+ /* RFC 9460, section 7.1.1: value must be empty */
+ case DNS_SVC_PARAM_KEY_NO_DEFAULT_ALPN:
+ return i->length == 0;
+
+ /* RFC 9460, section 7.2 */
+ case DNS_SVC_PARAM_KEY_PORT:
+ return i->length == 2;
+
+ /* RFC 9460, section 7.3: addrs must exactly fill SvcParamValue */
+ case DNS_SVC_PARAM_KEY_IPV4HINT:
+ return i->length % (sizeof (struct in_addr)) == 0;
+ case DNS_SVC_PARAM_KEY_IPV6HINT:
+ return i->length % (sizeof (struct in6_addr)) == 0;
+
+ /* Otherwise, permit any value */
+ default:
+ return true;
+ }
+}
+
int dns_packet_read_rr(
DnsPacket *p,
DnsResourceRecord **ret,
@@ -2123,6 +2183,52 @@ int dns_packet_read_rr(
break;
+ case DNS_TYPE_SVCB:
+ case DNS_TYPE_HTTPS:
+ r = dns_packet_read_uint16(p, &rr->svcb.priority, NULL);
+ if (r < 0)
+ return r;
+
+ r = dns_packet_read_name(p, &rr->svcb.target_name, false /* uncompressed */, NULL);
+ if (r < 0)
+ return r;
+
+ DnsSvcParam *last = NULL;
+ while (p->rindex - offset < rdlength) {
+ _cleanup_free_ DnsSvcParam *i = NULL;
+ uint16_t svc_param_key;
+ uint16_t sz;
+
+ r = dns_packet_read_uint16(p, &svc_param_key, NULL);
+ if (r < 0)
+ return r;
+ /* RFC 9460, section 2.2 says we must consider an RR malformed if SvcParamKeys are
+ * not in strictly increasing order */
+ if (last && last->key >= svc_param_key)
+ return -EBADMSG;
+
+ r = dns_packet_read_uint16(p, &sz, NULL);
+ if (r < 0)
+ return r;
+
+ i = malloc0(offsetof(DnsSvcParam, value) + sz);
+ if (!i)
+ return -ENOMEM;
+
+ i->key = svc_param_key;
+ i->length = sz;
+ r = dns_packet_read_blob(p, &i->value, sz, NULL);
+ if (r < 0)
+ return r;
+ if (!dns_svc_param_is_valid(i))
+ return -EBADMSG;
+
+ LIST_INSERT_AFTER(params, rr->svcb.params, last, i);
+ last = TAKE_PTR(i);
+ }
+
+ break;
+
case DNS_TYPE_CAA:
r = dns_packet_read_uint8(p, &rr->caa.flags, NULL);
if (r < 0)
@@ -2801,6 +2907,27 @@ const char *format_dns_ede_rcode(int i, char buf[static DECIMAL_STR_MAX(int)]) {
return snprintf_ok(buf, DECIMAL_STR_MAX(int), "%i", i);
}
+static const char* const dns_svc_param_key_table[_DNS_SVC_PARAM_KEY_MAX_DEFINED] = {
+ [DNS_SVC_PARAM_KEY_MANDATORY] = "mandatory",
+ [DNS_SVC_PARAM_KEY_ALPN] = "alpn",
+ [DNS_SVC_PARAM_KEY_NO_DEFAULT_ALPN] = "no-default-alpn",
+ [DNS_SVC_PARAM_KEY_PORT] = "port",
+ [DNS_SVC_PARAM_KEY_IPV4HINT] = "ipv4hint",
+ [DNS_SVC_PARAM_KEY_ECH] = "ech",
+ [DNS_SVC_PARAM_KEY_IPV6HINT] = "ipv6hint",
+ [DNS_SVC_PARAM_KEY_DOHPATH] = "dohpath",
+ [DNS_SVC_PARAM_KEY_OHTTP] = "ohttp",
+};
+DEFINE_STRING_TABLE_LOOKUP_TO_STRING(dns_svc_param_key, int);
+
+const char *format_dns_svc_param_key(uint16_t i, char buf[static DECIMAL_STR_MAX(uint16_t)+3]) {
+ const char *p = dns_svc_param_key_to_string(i);
+ if (p)
+ return p;
+
+ return snprintf_ok(buf, DECIMAL_STR_MAX(uint16_t)+3, "key%i", i);
+}
+
static const char* const dns_protocol_table[_DNS_PROTOCOL_MAX] = {
[DNS_PROTOCOL_DNS] = "dns",
[DNS_PROTOCOL_MDNS] = "mdns",
diff --git a/src/resolve/resolved-dns-packet.h b/src/resolve/resolved-dns-packet.h
index 705fc511d6..393b7b2364 100644
--- a/src/resolve/resolved-dns-packet.h
+++ b/src/resolve/resolved-dns-packet.h
@@ -361,6 +361,25 @@ const char *format_dns_ede_rcode(int i, char buf[static DECIMAL_STR_MAX(int)]);
const char* dns_protocol_to_string(DnsProtocol p) _const_;
DnsProtocol dns_protocol_from_string(const char *s) _pure_;
+/* https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml#dns-svcparamkeys */
+enum {
+ DNS_SVC_PARAM_KEY_MANDATORY = 0, /* RFC 9460 section 8 */
+ DNS_SVC_PARAM_KEY_ALPN = 1, /* RFC 9460 section 7.1 */
+ DNS_SVC_PARAM_KEY_NO_DEFAULT_ALPN = 2, /* RFC 9460 Section 7.1 */
+ DNS_SVC_PARAM_KEY_PORT = 3, /* RFC 9460 section 7.2 */
+ DNS_SVC_PARAM_KEY_IPV4HINT = 4, /* RFC 9460 section 7.3 */
+ DNS_SVC_PARAM_KEY_ECH = 5, /* RFC 9460 */
+ DNS_SVC_PARAM_KEY_IPV6HINT = 6, /* RFC 9460 section 7.3 */
+ DNS_SVC_PARAM_KEY_DOHPATH = 7, /* RFC 9461 */
+ DNS_SVC_PARAM_KEY_OHTTP = 8,
+ _DNS_SVC_PARAM_KEY_MAX_DEFINED,
+ DNS_SVC_PARAM_KEY_INVALID = 65535 /* RFC 9460 */
+};
+
+const char* dns_svc_param_key_to_string(int i) _const_;
+const char *format_dns_svc_param_key(uint16_t i, char buf[static DECIMAL_STR_MAX(uint16_t)+3]);
+#define FORMAT_DNS_SVC_PARAM_KEY(i) format_dns_svc_param_key(i, (char [DECIMAL_STR_MAX(uint16_t)+3]) {})
+
#define LLMNR_MULTICAST_IPV4_ADDRESS ((struct in_addr) { .s_addr = htobe32(224U << 24 | 252U) })
#define LLMNR_MULTICAST_IPV6_ADDRESS ((struct in6_addr) { .s6_addr = { 0xFF, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03 } })
diff --git a/src/resolve/resolved-dns-rr.c b/src/resolve/resolved-dns-rr.c
index 2bdcc7c1dc..7d824ee806 100644
--- a/src/resolve/resolved-dns-rr.c
+++ b/src/resolve/resolved-dns-rr.c
@@ -15,6 +15,7 @@
#include "string-util.h"
#include "strv.h"
#include "terminal-util.h"
+#include "unaligned.h"
DnsResourceKey* dns_resource_key_new(uint16_t class, uint16_t type, const char *name) {
DnsResourceKey *k;
@@ -469,6 +470,12 @@ static DnsResourceRecord* dns_resource_record_free(DnsResourceRecord *rr) {
free(rr->tlsa.data);
break;
+ case DNS_TYPE_SVCB:
+ case DNS_TYPE_HTTPS:
+ free(rr->svcb.target_name);
+ dns_svc_param_free_all(rr->svcb.params);
+ break;
+
case DNS_TYPE_CAA:
free(rr->caa.tag);
free(rr->caa.value);
@@ -676,6 +683,12 @@ int dns_resource_record_payload_equal(const DnsResourceRecord *a, const DnsResou
a->tlsa.matching_type == b->tlsa.matching_type &&
FIELD_EQUAL(a->tlsa, b->tlsa, data);
+ case DNS_TYPE_SVCB:
+ case DNS_TYPE_HTTPS:
+ return a->svcb.priority == b->svcb.priority &&
+ dns_name_equal(a->svcb.target_name, b->svcb.target_name) &&
+ dns_svc_params_equal(a->svcb.params, b->svcb.params);
+
case DNS_TYPE_CAA:
return a->caa.flags == b->caa.flags &&
streq(a->caa.tag, b->caa.tag) &&
@@ -814,6 +827,107 @@ static char *format_txt(DnsTxtItem *first) {
return s;
}
+static char *format_svc_param_value(DnsSvcParam *i) {
+ _cleanup_free_ char *value = NULL;
+
+ assert(i);
+
+ switch (i->key) {
+ case DNS_SVC_PARAM_KEY_ALPN: {
+ size_t offset = 0;
+ _cleanup_strv_free_ char **values_strv = NULL;
+ while (offset < i->length) {
+ size_t sz = (uint8_t) i->value[offset++];
+
+ char *alpn = cescape_length((char *)&i->value[offset], sz);
+ if (!alpn)
+ return NULL;
+
+ if (strv_push(&values_strv, alpn) < 0)
+ return NULL;
+
+ offset += sz;
+ }
+ value = strv_join(values_strv, ",");
+ if (!value)
+ return NULL;
+ break;
+
+ }
+ case DNS_SVC_PARAM_KEY_PORT: {
+ uint16_t port = unaligned_read_be16(i->value);
+ if (asprintf(&value, "%" PRIu16, port) < 0)
+ return NULL;
+ return TAKE_PTR(value);
+ }
+ case DNS_SVC_PARAM_KEY_IPV4HINT: {
+ const struct in_addr *addrs = i->value_in_addr;
+ _cleanup_strv_free_ char **values_strv = NULL;
+ for (size_t n = 0; n < i->length / sizeof (struct in_addr); n++) {
+ char *addr;
+ if (in_addr_to_string(AF_INET, (const union in_addr_union*) &addrs[n], &addr) < 0)
+ return NULL;
+ if (strv_push(&values_strv, addr) < 0)
+ return NULL;
+ }
+ return strv_join(values_strv, ",");
+ }
+ case DNS_SVC_PARAM_KEY_IPV6HINT: {
+ const struct in6_addr *addrs = i->value_in6_addr;
+ _cleanup_strv_free_ char **values_strv = NULL;
+ for (size_t n = 0; n < i->length / sizeof (struct in6_addr); n++) {
+ char *addr;
+ if (in_addr_to_string(AF_INET6, (const union in_addr_union*) &addrs[n], &addr) < 0)
+ return NULL;
+ if (strv_push(&values_strv, addr) < 0)
+ return NULL;
+ }
+ return strv_join(values_strv, ",");
+ }
+ default: {
+ value = decescape((char *)&i->value, " ,", i->length);
+ if (!value)
+ return NULL;
+ break;
+ }
+ }
+
+ char *qvalue;
+ if (asprintf(&qvalue, "\"%s\"", value) < 0)
+ return NULL;
+ return qvalue;
+}
+
+static char *format_svc_param(DnsSvcParam *i) {
+ const char *key = FORMAT_DNS_SVC_PARAM_KEY(i->key);
+ _cleanup_free_ char *value = NULL;
+
+ assert(i);
+
+ if (i->length == 0)
+ return strdup(key);
+
+ value = format_svc_param_value(i);
+ if (!value)
+ return NULL;
+
+ return strjoin(key, "=", value);
+}
+
+static char *format_svc_params(DnsSvcParam *first) {
+ _cleanup_strv_free_ char **params = NULL;
+
+ LIST_FOREACH(params, i, first) {
+ char *param = format_svc_param(i);
+ if (!param)
+ return NULL;
+ if (strv_push(&params, param) < 0)
+ return NULL;
+ }
+
+ return strv_join(params, " ");
+}
+
const char *dns_resource_record_to_string(DnsResourceRecord *rr) {
_cleanup_free_ char *s = NULL, *t = NULL;
char k[DNS_RESOURCE_KEY_STRING_MAX];
@@ -1124,6 +1238,19 @@ const char *dns_resource_record_to_string(DnsResourceRecord *rr) {
break;
+ case DNS_TYPE_SVCB:
+ case DNS_TYPE_HTTPS:
+ t = format_svc_params(rr->svcb.params);
+ if (!t)
+ return NULL;
+ r = asprintf(&s, "%s %d %s %s", k, rr->svcb.priority,
+ isempty(rr->svcb.target_name) ? "." : rr->svcb.target_name,
+ t);
+ if (r < 0)
+ return NULL;
+
+ break;
+
case DNS_TYPE_OPENPGPKEY:
r = asprintf(&s, "%s", k);
if (r < 0)
@@ -1445,6 +1572,16 @@ void dns_resource_record_hash_func(const DnsResourceRecord *rr, struct siphash *
siphash24_compress_safe(rr->tlsa.data, rr->tlsa.data_size, state);
break;
+ case DNS_TYPE_SVCB:
+ case DNS_TYPE_HTTPS:
+ dns_name_hash_func(rr->svcb.target_name, state);
+ siphash24_compress_typesafe(rr->svcb.priority, state);
+ LIST_FOREACH(params, j, rr->svcb.params) {
+ siphash24_compress_typesafe(j->key, state);
+ siphash24_compress_safe(j->value, j->length, state);
+ }
+ break;
+
case DNS_TYPE_CAA:
siphash24_compress_typesafe(rr->caa.flags, state);
string_hash_func(rr->caa.tag, state);
@@ -1658,6 +1795,17 @@ DnsResourceRecord *dns_resource_record_copy(DnsResourceRecord *rr) {
copy->caa.value_size = rr->caa.value_size;
break;
+ case DNS_TYPE_SVCB:
+ case DNS_TYPE_HTTPS:
+ copy->svcb.priority = rr->svcb.priority;
+ copy->svcb.target_name = strdup(rr->svcb.target_name);
+ if (!copy->svcb.target_name)
+ return NULL;
+ copy->svcb.params = dns_svc_params_copy(rr->svcb.params);
+ if (rr->svcb.params && !copy->svcb.params)
+ return NULL;
+ break;
+
case DNS_TYPE_OPT:
default:
copy->generic.data = memdup(rr->generic.data, rr->generic.data_size);
@@ -1772,6 +1920,13 @@ DnsTxtItem *dns_txt_item_free_all(DnsTxtItem *first) {
return NULL;
}
+DnsSvcParam *dns_svc_param_free_all(DnsSvcParam *first) {
+ LIST_FOREACH(params, i, first)
+ free(i);
+
+ return NULL;
+}
+
bool dns_txt_item_equal(DnsTxtItem *a, DnsTxtItem *b) {
DnsTxtItem *bb = b;
@@ -1808,6 +1963,45 @@ DnsTxtItem *dns_txt_item_copy(DnsTxtItem *first) {
return copy;
}
+bool dns_svc_params_equal(DnsSvcParam *a, DnsSvcParam *b) {
+ DnsSvcParam *bb = b;
+
+ if (a == b)
+ return true;
+
+ LIST_FOREACH(params, aa, a) {
+ if (!bb)
+ return false;
+
+ if (aa->key != bb->key)
+ return false;
+
+ if (memcmp_nn(aa->value, aa->length, bb->value, bb->length) != 0)
+ return false;
+
+ bb = bb->params_next;
+ }
+
+ return !bb;
+}
+
+DnsSvcParam *dns_svc_params_copy(DnsSvcParam *first) {
+ DnsSvcParam *copy = NULL, *end = NULL;
+
+ LIST_FOREACH(params, i, first) {
+ DnsSvcParam *j;
+
+ j = memdup(i, offsetof(DnsSvcParam, value) + i->length);
+ if (!j)
+ return dns_svc_param_free_all(copy);
+
+ LIST_INSERT_AFTER(params, copy, end, j);
+ end = j;
+ }
+
+ return copy;
+}
+
int dns_txt_item_new_empty(DnsTxtItem **ret) {
DnsTxtItem *i;
@@ -1930,10 +2124,33 @@ static int txt_to_json(DnsTxtItem *items, JsonVariant **ret) {
r = json_variant_new_array(ret, elements, n);
finalize:
- for (size_t i = 0; i < n; i++)
- json_variant_unref(elements[i]);
+ json_variant_unref_many(elements, n);
+ return r;
+}
+
+static int svc_params_to_json(DnsSvcParam *params, JsonVariant **ret) {
+ JsonVariant **elements = NULL;
+ size_t n = 0;
+ int r;
+
+ assert(ret);
+
+ LIST_FOREACH(params, i, params) {
+ if (!GREEDY_REALLOC(elements, n + 1)) {
+ r = -ENOMEM;
+ goto finalize;
+ }
+
+ r = json_variant_new_base64(elements + n, i->value, i->length);
+ if (r < 0)
+ goto finalize;
+
+ n++;
+ }
- free(elements);
+ r = json_variant_new_array(ret, elements, n);
+finalize:
+ json_variant_unref_many(elements, n);
return r;
}
@@ -2112,6 +2329,21 @@ int dns_resource_record_to_json(DnsResourceRecord *rr, JsonVariant **ret) {
JSON_BUILD_PAIR("matchingType", JSON_BUILD_UNSIGNED(rr->tlsa.matching_type)),
JSON_BUILD_PAIR("data", JSON_BUILD_HEX(rr->tlsa.data, rr->tlsa.data_size))));
+ case DNS_TYPE_SVCB:
+ case DNS_TYPE_HTTPS: {
+ _cleanup_(json_variant_unrefp) JsonVariant *p = NULL;
+ r = svc_params_to_json(rr->svcb.params, &p);
+ if (r < 0)
+ return r;
+
+ return json_build(ret,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)),
+ JSON_BUILD_PAIR("priority", JSON_BUILD_UNSIGNED(rr->svcb.priority)),
+ JSON_BUILD_PAIR("target", JSON_BUILD_STRING(rr->svcb.target_name)),
+ JSON_BUILD_PAIR("params", JSON_BUILD_VARIANT(p))));
+ }
+
case DNS_TYPE_CAA:
return json_build(ret,
JSON_BUILD_OBJECT(
diff --git a/src/resolve/resolved-dns-rr.h b/src/resolve/resolved-dns-rr.h
index fd15cc343d..961d3c7852 100644
--- a/src/resolve/resolved-dns-rr.h
+++ b/src/resolve/resolved-dns-rr.h
@@ -16,6 +16,7 @@
typedef struct DnsResourceKey DnsResourceKey;
typedef struct DnsResourceRecord DnsResourceRecord;
typedef struct DnsTxtItem DnsTxtItem;
+typedef struct DnsSvcParam DnsSvcParam;
/* DNSKEY RR flags */
#define DNSKEY_FLAG_SEP (UINT16_C(1) << 0)
@@ -90,6 +91,17 @@ struct DnsTxtItem {
uint8_t data[];
};
+struct DnsSvcParam {
+ uint16_t key;
+ size_t length;
+ LIST_FIELDS(DnsSvcParam, params);
+ union {
+ DECLARE_FLEX_ARRAY(uint8_t, value);
+ DECLARE_FLEX_ARRAY(struct in_addr, value_in_addr);
+ DECLARE_FLEX_ARRAY(struct in6_addr, value_in6_addr);
+ };
+};
+
struct DnsResourceRecord {
unsigned n_ref;
uint32_t ttl;
@@ -243,6 +255,13 @@ struct DnsResourceRecord {
uint8_t matching_type;
} tlsa;
+ /* https://tools.ietf.org/html/rfc9460 */
+ struct {
+ uint16_t priority;
+ char *target_name;
+ DnsSvcParam *params;
+ } svcb, https;
+
/* https://tools.ietf.org/html/rfc6844 */
struct {
char *tag;
@@ -368,6 +387,10 @@ bool dns_txt_item_equal(DnsTxtItem *a, DnsTxtItem *b);
DnsTxtItem *dns_txt_item_copy(DnsTxtItem *i);
int dns_txt_item_new_empty(DnsTxtItem **ret);
+DnsSvcParam *dns_svc_param_free_all(DnsSvcParam *i);
+bool dns_svc_params_equal(DnsSvcParam *a, DnsSvcParam *b);
+DnsSvcParam *dns_svc_params_copy(DnsSvcParam *first);
+
int dns_resource_record_new_from_raw(DnsResourceRecord **ret, const void *data, size_t size);
int dns_resource_key_to_json(DnsResourceKey *key, JsonVariant **ret);
diff --git a/src/shared/varlink-io.systemd.Resolve.Monitor.c b/src/shared/varlink-io.systemd.Resolve.Monitor.c
index 96a58ca768..e46d1975d7 100644
--- a/src/shared/varlink-io.systemd.Resolve.Monitor.c
+++ b/src/shared/varlink-io.systemd.Resolve.Monitor.c
@@ -59,7 +59,9 @@ VARLINK_DEFINE_STRUCT_TYPE(
VARLINK_DEFINE_FIELD(matchingType, VARLINK_INT, VARLINK_NULLABLE),
VARLINK_DEFINE_FIELD(data, VARLINK_STRING, VARLINK_NULLABLE),
VARLINK_DEFINE_FIELD(tag, VARLINK_STRING, VARLINK_NULLABLE),
- VARLINK_DEFINE_FIELD(value, VARLINK_STRING, VARLINK_NULLABLE));
+ VARLINK_DEFINE_FIELD(value, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(target, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(params, VARLINK_STRING, VARLINK_NULLABLE|VARLINK_ARRAY));
VARLINK_DEFINE_STRUCT_TYPE(
ResourceRecordArray,
diff --git a/src/test/test-escape.c b/src/test/test-escape.c
index 21786ae72a..364e0f3956 100644
--- a/src/test/test-escape.c
+++ b/src/test/test-escape.c
@@ -239,4 +239,22 @@ TEST(octescape) {
test_octescape_one("\123\213\222", "\123\\213\\222");
}
+static void test_decescape_one(const char *s, const char *bad, const char *expected) {
+ _cleanup_free_ char *ret = NULL;
+
+ assert_se(ret = decescape(s, bad, strlen_ptr(s)));
+ log_debug("decescape(\"%s\") → \"%s\" (expected: \"%s\")", strnull(s), ret, expected);
+ assert_se(streq(ret, expected));
+}
+
+TEST(decescape) {
+ test_decescape_one(NULL, "bad", "");
+ test_decescape_one("foo", "", "foo");
+ test_decescape_one("foo", "f", "\\102oo");
+ test_decescape_one("foo", "o", "f\\111\\111");
+ test_decescape_one("go\"bb\\ledyg\x03ook\r\n", "", "go\\034bb\\092ledyg\\003ook\\013\\010");
+ test_decescape_one("\\xff\xff" "f", "f", "\\092x\\102\\102\\255\\102");
+ test_decescape_one("all", "all", "\\097\\108\\108");
+}
+
DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/test/knot-data/zones/test.zone b/test/knot-data/zones/test.zone
index ba5fcebc2d..065ff7e2a0 100644
--- a/test/knot-data/zones/test.zone
+++ b/test/knot-data/zones/test.zone
@@ -19,3 +19,6 @@ ns1.unsigned AAAA fd00:dead:beef:cafe::1
onlinesign NS ns1.unsigned
signed NS ns1.unsigned
unsigned NS ns1.unsigned
+
+svcb SVCB 1 . alpn=dot ipv4hint=10.0.0.1 ipv6hint=fd00:dead:beef:cafe::1
+https HTTPS 1 . alpn="h2,h3"
diff --git a/test/units/testsuite-75.sh b/test/units/testsuite-75.sh
index f1fb5d943a..92f6b8e030 100755
--- a/test/units/testsuite-75.sh
+++ b/test/units/testsuite-75.sh
@@ -371,6 +371,12 @@ run dig +noall +authority +comments SRV .
grep -qF "status: NOERROR" "$RUN_OUT"
grep -qE "IN\s+SOA\s+ns1\.unsigned\.test\." "$RUN_OUT"
+run resolvectl query -t SVCB svcb.test
+grep -qF 'alpn="dot"' "$RUN_OUT"
+grep -qF "ipv4hint=10.0.0.1" "$RUN_OUT"
+
+run resolvectl query -t HTTPS https.test
+grep -qF 'alpn="h2,h3"' "$RUN_OUT"
: "--- ZONE: unsigned.test. ---"
run dig @ns1.unsigned.test +short unsigned.test A unsigned.test AAAA