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);
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);
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)
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,
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)
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",
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 } })
#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;
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);
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) &&
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(¶ms, 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];
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)
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);
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);
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;
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;
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;
}
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(
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)
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;
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;
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);
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,
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);
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"
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