Merge pull request #30661 from rpigott/resolved-https-record

resolved: support RFC 9460 SVCB and HTTPS records
This commit is contained in:
Luca Boccassi 2024-01-17 16:20:35 +00:00 committed by GitHub
commit 0a9735eac2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 462 additions and 4 deletions

View file

@ -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);

View file

@ -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);

View file

@ -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",

View file

@ -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 } })

View file

@ -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;
}
free(elements);
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++;
}
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(

View file

@ -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);

View file

@ -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,

View file

@ -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);

View file

@ -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"

View file

@ -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