resolved: add "static RRs" concept (#41213)

split out of #40980
This commit is contained in:
Lennart Poettering
2026-03-24 22:45:22 +01:00
committed by GitHub
22 changed files with 921 additions and 7 deletions
+62 -1
View File
@@ -709,6 +709,10 @@ nsec_t statx_timestamp_load_nsec(const struct statx_timestamp *ts) {
void inode_hash_func(const struct stat *q, struct siphash *state) {
siphash24_compress_typesafe(q->st_dev, state);
siphash24_compress_typesafe(q->st_ino, state);
/* Also include inode type, to mirror stat_inode_same() */
mode_t type = q->st_mode & S_IFMT;
siphash24_compress_typesafe(type, state);
}
int inode_compare_func(const struct stat *a, const struct stat *b) {
@@ -718,11 +722,68 @@ int inode_compare_func(const struct stat *a, const struct stat *b) {
if (r != 0)
return r;
return CMP(a->st_ino, b->st_ino);
r = CMP(a->st_ino, b->st_ino);
if (r != 0)
return r;
return CMP(a->st_mode & S_IFMT, b->st_mode & S_IFMT);
}
DEFINE_HASH_OPS_WITH_KEY_DESTRUCTOR(inode_hash_ops, struct stat, inode_hash_func, inode_compare_func, free);
void inode_unmodified_hash_func(const struct stat *q, struct siphash *state) {
inode_hash_func(q, state);
siphash24_compress_typesafe(q->st_mtim.tv_sec, state);
siphash24_compress_typesafe(q->st_mtim.tv_nsec, state);
if (S_ISREG(q->st_mode))
siphash24_compress_typesafe(q->st_size, state);
else {
uint64_t invalid = UINT64_MAX;
siphash24_compress_typesafe(invalid, state);
}
if (S_ISCHR(q->st_mode) || S_ISBLK(q->st_mode))
siphash24_compress_typesafe(q->st_rdev, state);
else {
dev_t invalid = (dev_t) -1;
siphash24_compress_typesafe(invalid, state);
}
}
int inode_unmodified_compare_func(const struct stat *a, const struct stat *b) {
int r;
r = inode_compare_func(a, b);
if (r != 0)
return r;
r = CMP(a->st_mtim.tv_sec, b->st_mtim.tv_sec);
if (r != 0)
return r;
r = CMP(a->st_mtim.tv_nsec, b->st_mtim.tv_nsec);
if (r != 0)
return r;
if (S_ISREG(a->st_mode)) {
r = CMP(a->st_size, b->st_size);
if (r != 0)
return r;
}
if (S_ISCHR(a->st_mode) || S_ISBLK(a->st_mode)) {
r = CMP(a->st_rdev, b->st_rdev);
if (r != 0)
return r;
}
return 0;
}
DEFINE_HASH_OPS_WITH_KEY_DESTRUCTOR(inode_unmodified_hash_ops, struct stat, inode_unmodified_hash_func, inode_unmodified_compare_func, free);
const char* inode_type_to_string(mode_t m) {
/* Returns a short string for the inode type. We use the same name as the underlying macros for each
+7
View File
@@ -122,10 +122,17 @@ int xstatfsat(int dir_fd, const char *path, struct statfs *ret);
usec_t statx_timestamp_load(const struct statx_timestamp *ts) _pure_;
nsec_t statx_timestamp_load_nsec(const struct statx_timestamp *ts) _pure_;
/* This compares inode number, backing device and inode type, but not modification info */
void inode_hash_func(const struct stat *q, struct siphash *state);
int inode_compare_func(const struct stat *a, const struct stat *b);
extern const struct hash_ops inode_hash_ops;
/* This is a more thorough version of the above, and also checks the mtimes, the size, and the rdev. It does
* not check "external" attributes such as access mode or ownership. */
void inode_unmodified_hash_func(const struct stat *q, struct siphash *state);
int inode_unmodified_compare_func(const struct stat *a, const struct stat *b);
extern const struct hash_ops inode_unmodified_hash_ops;
DECLARE_STRING_TABLE_LOOKUP(inode_type, mode_t);
/* Macros that check whether the stat/statx structures have been initialized already. For "struct stat" we
+49 -1
View File
@@ -9,6 +9,7 @@
#include "errno-util.h"
#include "fd-util.h"
#include "glyph-util.h"
#include "in-addr-util.h"
#include "iovec-util.h"
#include "json-util.h"
#include "log.h"
@@ -189,12 +190,59 @@ int json_dispatch_in_addr(const char *name, sd_json_variant *variant, sd_json_di
return 0;
}
/* We support a more human readable string based encoding, and an array based encoding */
if (sd_json_variant_is_string(variant)) {
union in_addr_union a;
r = in_addr_from_string(AF_INET, sd_json_variant_string(variant), &a);
if (r < 0)
return json_log(variant, flags, r,
"JSON field '%s' is not a valid IPv4 address string: %s", strna(name), sd_json_variant_string(variant));
*address = a.in;
return 0;
}
r = json_dispatch_byte_array_iovec(name, variant, flags, &iov);
if (r < 0)
return r;
if (iov.iov_len != sizeof(struct in_addr))
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is array of unexpected size.", strna(name));
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL),
"Expected JSON field '%s' to be an array of %zu bytes.", strna(name), sizeof(struct in_addr));
memcpy(address, iov.iov_base, iov.iov_len);
return 0;
}
int json_dispatch_in6_addr(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
struct in6_addr *address = ASSERT_PTR(userdata);
_cleanup_(iovec_done) struct iovec iov = {};
int r;
if (sd_json_variant_is_null(variant)) {
*address = (struct in6_addr) {};
return 0;
}
/* We support both a more human readable string based encoding and an array based encoding */
if (sd_json_variant_is_string(variant)) {
union in_addr_union a;
r = in_addr_from_string(AF_INET6, sd_json_variant_string(variant), &a);
if (r < 0)
return json_log(variant, flags, r,
"JSON field '%s' is not a valid IPv6 address string: %s", strna(name), sd_json_variant_string(variant));
*address = a.in6;
return 0;
}
r = json_dispatch_byte_array_iovec(name, variant, flags, &iov);
if (r < 0)
return r;
if (iov.iov_len != sizeof(struct in6_addr))
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL),
"Expected JSON field '%s' to be an array of %zu bytes.", strna(name), sizeof(struct in6_addr));
memcpy(address, iov.iov_base, iov.iov_len);
return 0;
+1
View File
@@ -115,6 +115,7 @@ int json_dispatch_user_group_name(const char *name, sd_json_variant *variant, sd
int json_dispatch_const_user_group_name(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_const_unit_name(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_in_addr(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_in6_addr(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_path(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_const_path(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_strv_path(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
+1
View File
@@ -36,6 +36,7 @@ systemd_resolved_extract_sources = files(
'resolved-mdns.c',
'resolved-resolv-conf.c',
'resolved-socket-graveyard.c',
'resolved-static-records.c',
'resolved-util.c',
'resolved-varlink.c',
)
+36
View File
@@ -21,6 +21,7 @@
#include "resolved-etc-hosts.h"
#include "resolved-hook.h"
#include "resolved-manager.h"
#include "resolved-static-records.h"
#include "resolved-timeouts.h"
#include "set.h"
#include "string-util.h"
@@ -910,6 +911,33 @@ static int dns_query_try_etc_hosts(DnsQuery *q) {
return 1;
}
static int dns_query_try_static_records(DnsQuery *q) {
int r;
assert(q);
if (FLAGS_SET(q->flags, SD_RESOLVED_NO_SYNTHESIZE))
return 0;
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
r = manager_static_records_lookup(
q->manager,
q->question_bypass ? q->question_bypass->question : q->question_utf8,
&answer);
if (r <= 0)
return r;
dns_query_reset_answer(q);
q->answer = TAKE_PTR(answer);
q->answer_rcode = DNS_RCODE_SUCCESS;
q->answer_protocol = dns_synthesize_protocol(q->flags);
q->answer_family = dns_synthesize_family(q->flags);
q->answer_query_flags = SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL|SD_RESOLVED_SYNTHETIC;
return 1;
}
static int dns_query_go_scopes(DnsQuery *q) {
int r;
@@ -1038,6 +1066,14 @@ int dns_query_go(DnsQuery *q) {
q->state != DNS_TRANSACTION_NULL)
return 0;
r = dns_query_try_static_records(q);
if (r < 0)
return r;
if (r > 0) {
dns_query_complete(q, DNS_TRANSACTION_SUCCESS);
return 1;
}
r = dns_query_try_etc_hosts(q);
if (r < 0)
return r;
+1
View File
@@ -72,6 +72,7 @@ void etc_hosts_clear(EtcHosts *hosts) {
void manager_etc_hosts_flush(Manager *m) {
etc_hosts_clear(&m->etc_hosts);
m->etc_hosts_stat = (struct stat) {};
m->etc_hosts_last = USEC_INFINITY;
}
static int parse_line(EtcHosts *hosts, unsigned nr, const char *line) {
+1
View File
@@ -31,6 +31,7 @@ Resolve.DNSOverTLS, config_parse_dns_over_tls_mode, 0,
Resolve.Cache, config_parse_dns_cache_mode, DNS_CACHE_MODE_YES, offsetof(Manager, enable_cache)
Resolve.DNSStubListener, config_parse_dns_stub_listener_mode, 0, offsetof(Manager, dns_stub_listener_mode)
Resolve.ReadEtcHosts, config_parse_bool, 0, offsetof(Manager, read_etc_hosts)
Resolve.ReadStaticRecords, config_parse_bool, 0, offsetof(Manager, read_static_records)
Resolve.ResolveUnicastSingleLabel, config_parse_bool, 0, offsetof(Manager, resolve_unicast_single_label)
Resolve.DNSStubListenerExtra, config_parse_dns_stub_listener_extra, 0, offsetof(Manager, dns_extra_stub_listeners)
Resolve.CacheFromLocalhost, config_parse_bool, 0, offsetof(Manager, cache_from_localhost)
+6
View File
@@ -49,6 +49,7 @@
#include "resolved-mdns.h"
#include "resolved-resolv-conf.h"
#include "resolved-socket-graveyard.h"
#include "resolved-static-records.h"
#include "resolved-util.h"
#include "resolved-varlink.h"
#include "set.h"
@@ -637,6 +638,7 @@ static void manager_set_defaults(Manager *m) {
m->enable_cache = DNS_CACHE_MODE_YES;
m->dns_stub_listener_mode = DNS_STUB_LISTENER_YES;
m->read_etc_hosts = true;
m->read_static_records = true;
m->resolve_unicast_single_label = false;
m->cache_from_localhost = false;
m->stale_retention_usec = 0;
@@ -659,6 +661,8 @@ static int manager_dispatch_reload_signal(sd_event_source *s, const struct signa
m->unicast_scope = dns_scope_free(m->unicast_scope);
m->delegates = hashmap_free(m->delegates);
dns_trust_anchor_flush(&m->trust_anchor);
manager_etc_hosts_flush(m);
manager_static_records_flush(m);
manager_set_defaults(m);
@@ -729,6 +733,7 @@ int manager_new(Manager **ret) {
.read_resolv_conf = true,
.need_builtin_fallbacks = true,
.etc_hosts_last = USEC_INFINITY,
.static_records_last = USEC_INFINITY,
.sigrtmin18_info.memory_pressure_handler = manager_memory_pressure,
.sigrtmin18_info.memory_pressure_userdata = m,
@@ -917,6 +922,7 @@ Manager* manager_free(Manager *m) {
dns_trust_anchor_flush(&m->trust_anchor);
manager_etc_hosts_flush(m);
manager_static_records_flush(m);
while ((sb = hashmap_first(m->dns_service_browsers)))
dns_service_browser_free(sb);
+6
View File
@@ -123,6 +123,12 @@ typedef struct Manager {
struct stat etc_hosts_stat;
bool read_etc_hosts;
/* Data from {/etc,/run,/usr/local/lib,/usr/lib}/systemd/resolve/static.d/ */
Hashmap *static_records;
usec_t static_records_last;
Set *static_records_stat;
bool read_static_records;
/* List of refused DNS Record Types */
Set *refuse_record_types;
+226
View File
@@ -0,0 +1,226 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-json.h"
#include "alloc-util.h"
#include "conf-files.h"
#include "constants.h"
#include "dns-answer.h"
#include "dns-domain.h"
#include "dns-question.h"
#include "dns-rr.h"
#include "errno-util.h"
#include "fd-util.h"
#include "fileio.h"
#include "hashmap.h"
#include "json-util.h"
#include "log.h"
#include "resolved-manager.h"
#include "resolved-static-records.h"
#include "set.h"
#include "stat-util.h"
/* This implements a mechanism to extend what systemd-resolved resolves locally, via .rr drop-ins in
* {/etc,/run,/usr/local/lib,/usr/lib}/systemd/resolve/static.d/. These files are in JSON format, and are RR
* serializations, that match the usual way we serialize RRs to JSON.
*
* Note that this deliberately doesn't use the (probably more user-friendly) classic DNS zone file format,
* to keep things a bit simpler, and symmetric to the places we currently already generate JSON
* serializations of DNS RRs. Also note the semantics are different from DNS zone file format, for example
* regarding delegation (i.e. the RRs defined here have no effect on subdomains), which is probably nicer for
* one-off mappings of domains to specific resources. Or in other words, this is supposed to be a drop-in
* based alternative to /etc/hosts, not a one to DNS zone files. (The JSON format is also a lot more
* extensible to us, for example we could teach it to map certain lookups to specific DNS errors, or extend
* it so that subdomains always get NXDOMAIN or similar).
*
* (That said, if there's a good reason, we can also support *.zone files too one day).
*/
/* Recheck static records at most once every 2s */
#define STATIC_RECORDS_RECHECK_USEC (2*USEC_PER_SEC)
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
answer_by_name_hash_ops,
char,
dns_name_hash_func,
dns_name_compare_func,
DnsAnswer,
dns_answer_unref);
static int load_static_record_file_item(sd_json_variant *rj, Hashmap **records) {
int r;
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
r = dns_resource_record_from_json(rj, &rr);
if (r < 0)
return log_error_errno(r, "Failed to parse DNS record from JSON: %m");
_cleanup_(dns_answer_unrefp) DnsAnswer *a =
hashmap_remove(*records, dns_resource_key_name(rr->key));
r = dns_answer_add_extend_full(&a, rr, /* ifindex= */ 0, DNS_ANSWER_AUTHENTICATED, /* rrsig= */ NULL, /* until= */ USEC_INFINITY);
if (r < 0)
return log_error_errno(r, "Failed to append RR to DNS answer: %m");
DnsAnswerItem *item = ASSERT_PTR(ordered_set_first(a->items));
r = hashmap_ensure_put(records, &answer_by_name_hash_ops, dns_resource_key_name(item->rr->key), a);
if (r < 0)
return log_error_errno(r, "Failed to add RR to static record set: %m");
TAKE_PTR(a);
log_debug("Added static resource record: %s", dns_resource_record_to_string(rr));
return 1;
}
static int load_static_record_file(const ConfFile *cf, Hashmap **records, Set **stats) {
int r;
assert(cf);
assert(records);
assert(stats);
/* Have we seen this file before? Then we might as well skip loading it again, it wouldn't have any
* additional effect anyway. (Note: masking/overriding has already been applied before we reach this
* point, here everything is purely additive.) */
if (set_contains(*stats, &cf->st))
return 0;
_cleanup_free_ struct stat *st_copy = memdup(&cf->st, sizeof(cf->st));
if (!st_copy)
return log_oom();
if (set_ensure_consume(stats, &inode_unmodified_hash_ops, TAKE_PTR(st_copy)) < 0)
return log_oom();
_cleanup_fclose_ FILE *f = NULL;
r = xfopenat(cf->fd, /* path= */ NULL, "re", /* open_flags= */ 0, &f);
if (r < 0) {
log_warning_errno(r, "Failed to open '%s', skipping: %m", cf->result);
return 0;
}
_cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL;
unsigned line = 0, column = 0;
r = sd_json_parse_file(f, cf->result, /* flags= */ 0, &j, &line, &column);
if (r < 0) {
if (line > 0)
log_syntax(/* unit= */ NULL, LOG_WARNING, cf->result, line, r, "Failed to parse JSON, skipping: %m");
else
log_warning_errno(r, "Failed to parse JSON file '%s', skipping: %m", cf->result);
return 0;
}
if (sd_json_variant_is_array(j)) {
sd_json_variant *i;
int ret = 0;
JSON_VARIANT_ARRAY_FOREACH(i, j)
RET_GATHER(ret, load_static_record_file_item(i, records));
if (ret < 0)
return ret;
} else if (sd_json_variant_is_object(j)) {
r = load_static_record_file_item(j, records);
if (r < 0)
return r;
} else {
log_warning("JSON file '%s' contains neither array nor object, skipping.", cf->result);
return 0;
}
return 1;
}
static int manager_static_records_read(Manager *m) {
int r;
usec_t ts;
assert_se(sd_event_now(m->event, CLOCK_BOOTTIME, &ts) >= 0);
/* See if we checked the static records db recently already */
if (m->static_records_last != USEC_INFINITY && usec_add(m->static_records_last, STATIC_RECORDS_RECHECK_USEC) > ts)
return 0;
m->static_records_last = ts;
ConfFile **files = NULL;
size_t n_files = 0;
CLEANUP_ARRAY(files, n_files, conf_file_free_many);
r = conf_files_list_nulstr_full(
".rr",
/* root= */ NULL,
CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN,
CONF_PATHS_NULSTR("systemd/resolve/static.d/"),
&files,
&n_files);
if (r < 0)
return log_error_errno(r, "Failed to enumerate static record drop-ins: %m");
/* Let's suppress reloads if nothing changed. For that keep the set of inodes from the previous
* reload around, and see if there are any changes on them. */
bool reload;
if (set_size(m->static_records_stat) != n_files)
reload = true;
else {
reload = false;
FOREACH_ARRAY(f, files, n_files)
if (!set_contains(m->static_records_stat, &(*f)->st)) {
reload = true;
break;
}
}
if (!reload) {
log_debug("No static record files changed, not re-reading.");
return 0;
}
_cleanup_(hashmap_freep) Hashmap *records = NULL;
_cleanup_(set_freep) Set *stats = NULL;
FOREACH_ARRAY(f, files, n_files)
(void) load_static_record_file(*f, &records, &stats);
hashmap_free(m->static_records);
m->static_records = TAKE_PTR(records);
set_free(m->static_records_stat);
m->static_records_stat = TAKE_PTR(stats);
return 0;
}
int manager_static_records_lookup(Manager *m, DnsQuestion *q, DnsAnswer **answer) {
int r;
assert(m);
assert(q);
assert(answer);
if (!m->read_static_records)
return 0;
(void) manager_static_records_read(m);
const char *n = dns_question_first_name(q);
if (!n)
return 0;
DnsAnswer *f = hashmap_get(m->static_records, n);
if (!f)
return 0;
r = dns_answer_extend(answer, f);
if (r < 0)
return r;
return 1;
}
void manager_static_records_flush(Manager *m) {
assert(m);
m->static_records = hashmap_free(m->static_records);
m->static_records_stat = set_free(m->static_records_stat);
m->static_records_last = USEC_INFINITY;
}
+7
View File
@@ -0,0 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "resolved-forward.h"
void manager_static_records_flush(Manager *m);
int manager_static_records_lookup(Manager *m, DnsQuestion* q, DnsAnswer **answer);
+1
View File
@@ -39,6 +39,7 @@
#DNSStubListener=yes
#DNSStubListenerExtra=
#ReadEtcHosts=yes
#ReadStaticRecords=yes
#ResolveUnicastSingleLabel=no
#StaleRetentionSec=0
#RefuseRecordTypes=
+41 -2
View File
@@ -7,6 +7,16 @@
#include "dns-type.h"
#include "tests.h"
static void test_to_json_from_json(DnsResourceRecord *rr) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL;
ASSERT_OK(dns_resource_record_to_json(rr, &j));
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr2 = NULL;
ASSERT_OK(dns_resource_record_from_json(j, &rr2));
ASSERT_TRUE(dns_resource_record_equal(rr, rr2));
}
/* ================================================================
* DNS_RESOURCE_RECORD_RDATA()
* ================================================================ */
@@ -802,6 +812,8 @@ TEST(dns_resource_record_new_address_ipv4) {
ASSERT_EQ(rr->key->type, DNS_TYPE_A);
ASSERT_STREQ(dns_resource_key_name(rr->key), "www.example.com");
ASSERT_EQ(rr->a.in_addr.s_addr, addr.in.s_addr);
test_to_json_from_json(rr);
}
TEST(dns_resource_record_new_address_ipv6) {
@@ -818,6 +830,8 @@ TEST(dns_resource_record_new_address_ipv6) {
ASSERT_EQ(rr->key->type, DNS_TYPE_AAAA);
ASSERT_STREQ(dns_resource_key_name(rr->key), "www.example.com");
ASSERT_EQ(memcmp(&rr->aaaa.in6_addr, &addr.in6, sizeof(struct in6_addr)), 0);
test_to_json_from_json(rr);
}
/* ================================================================
@@ -1003,11 +1017,13 @@ TEST(dns_resource_record_equal_cname_copy) {
a = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_CNAME, "www.example.com");
ASSERT_NOT_NULL(a);
a->cname.name = strdup("example.com");
a->cname.name = ASSERT_PTR(strdup("example.com"));
b = dns_resource_record_copy(a);
ASSERT_NOT_NULL(b);
ASSERT_TRUE(dns_resource_record_equal(a, b));
test_to_json_from_json(a);
}
TEST(dns_resource_record_equal_cname_fail) {
@@ -1220,11 +1236,13 @@ TEST(dns_resource_record_equal_ptr_copy) {
a = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_PTR, "127.1.168.192.in-addr-arpa");
ASSERT_NOT_NULL(a);
a->ptr.name = strdup("example.com");
a->ptr.name = ASSERT_PTR(strdup("example.com"));
b = dns_resource_record_copy(a);
ASSERT_NOT_NULL(b);
ASSERT_TRUE(dns_resource_record_equal(a, b));
test_to_json_from_json(a);
}
TEST(dns_resource_record_equal_ptr_fail) {
@@ -2461,4 +2479,25 @@ TEST(dns_resource_record_clamp_ttl_copy) {
ASSERT_EQ(orig->ttl, 3600u);
}
static void test_from_json(const char *text, int expected) {
log_notice("Trying to parse as JSON RR: %s", text);
_cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL;
ASSERT_OK(sd_json_parse(text, /* flags= */ 0, &j, /* reterr_line= */ NULL, /* reterr_column= */ NULL));
ASSERT_EQ(dns_resource_record_from_json(j, NULL), expected);
}
TEST(from_bad_json) {
test_from_json("{}", -EBADMSG);
test_from_json("{\"key\":{}}", -ENXIO);
test_from_json("{\"key\":{\"name\":\"foobar\",\"type\":9}}", -EOPNOTSUPP);
test_from_json("{\"key\":{\"name\":\"foobar\"}}", -ENXIO);
test_from_json("{\"key\":{\"type\":9}}", -ENXIO);
test_from_json("{\"key\":{\"name\":\"foobar\",\"type\":1}}", -ENXIO);
test_from_json("{\"key\":{\"name\":\"foobar\",\"type\":1},\"address\":[1,2,3,4]}", 0);
test_from_json("{\"key\":{\"name\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"type\":1},\"address\":[1,2,3,4]}", 0);
test_from_json("{\"key\":{\"name\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"type\":1},\"address\":[1,2,3,4]}", -EBADMSG);
test_from_json("{\"key\":{\"name\":\"a.a\",\"type\":1},\"address\":[1,2,3,4]}", 0);
test_from_json("{\"key\":{\"name\":\"a..a\",\"type\":1},\"address\":[1,2,3,4]}", -EBADMSG);
}
DEFINE_TEST_MAIN(LOG_DEBUG);
+99 -2
View File
@@ -2215,6 +2215,12 @@ int dns_resource_key_from_json(sd_json_variant *v, DnsResourceKey **ret) {
if (r < 0)
return r;
r = dns_name_is_valid(p.name);
if (r < 0)
return r;
if (r == 0)
return -EBADMSG;
key = dns_resource_key_new(p.class, p.type, p.name);
if (!key)
return -ENOMEM;
@@ -2302,7 +2308,6 @@ int dns_resource_record_to_json(DnsResourceRecord *rr, sd_json_variant **ret) {
int r;
assert(rr);
assert(ret);
r = dns_resource_key_to_json(rr->key, &k);
if (r < 0)
@@ -2508,11 +2513,103 @@ int dns_resource_record_to_json(DnsResourceRecord *rr, sd_json_variant **ret) {
default:
/* Can't provide broken-down format */
*ret = NULL;
if (ret)
*ret = NULL;
return 0;
}
}
int dns_resource_record_from_json(sd_json_variant *v, DnsResourceRecord **ret) {
int r;
assert(v);
sd_json_variant *k = sd_json_variant_by_key(v, "key");
if (!k)
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Resource record entry lacks key field, refusing.");
_cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
r = dns_resource_key_from_json(k, &key);
if (r < 0)
return r;
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
rr = dns_resource_record_new(key);
if (!rr)
return log_oom_debug();
/* Note, for now we only support the most common subset of RRs for decoding here. Please send patches for more. */
switch (key->type) {
case DNS_TYPE_PTR:
case DNS_TYPE_NS:
case DNS_TYPE_CNAME:
case DNS_TYPE_DNAME: {
_cleanup_free_ char *name = NULL;
static const struct sd_json_dispatch_field table[] = {
{ "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, 0, SD_JSON_MANDATORY },
{ "key", SD_JSON_VARIANT_OBJECT, NULL, 0, SD_JSON_MANDATORY },
{}
};
r = sd_json_dispatch(v, table, /* flags= */ 0, &name);
if (r < 0)
return r;
r = dns_name_is_valid(name);
if (r < 0)
return r;
if (r == 0)
return -EBADMSG;
rr->ptr.name = TAKE_PTR(name);
break;
}
case DNS_TYPE_A: {
struct in_addr addr = {};
static const struct sd_json_dispatch_field table[] = {
{ "address", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in_addr, 0, SD_JSON_MANDATORY },
{ "key", SD_JSON_VARIANT_OBJECT, NULL, 0, SD_JSON_MANDATORY },
{}
};
r = sd_json_dispatch(v, table, /* flags= */ 0, &addr);
if (r < 0)
return r;
rr->a.in_addr = addr;
break;
}
case DNS_TYPE_AAAA: {
struct in6_addr addr = {};
static const struct sd_json_dispatch_field table[] = {
{ "address", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in6_addr, 0, SD_JSON_MANDATORY },
{ "key", SD_JSON_VARIANT_OBJECT, NULL, 0, SD_JSON_MANDATORY },
{}
};
r = sd_json_dispatch(v, table, /* flags= */ 0, &addr);
if (r < 0)
return r;
rr->aaaa.in6_addr = addr;
break;
}
default:
return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Decoding DNS type %s is currently not supported.", dns_type_to_string(key->type));
}
if (ret)
*ret = TAKE_PTR(rr);
return 0;
}
static const char* const dnssec_algorithm_table[_DNSSEC_ALGORITHM_MAX_DEFINED] = {
/* Mnemonics as listed on https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml */
[DNSSEC_ALGORITHM_RSAMD5] = "RSAMD5",
+1
View File
@@ -419,6 +419,7 @@ int dns_resource_record_new_from_raw(DnsResourceRecord **ret, const void *data,
int dns_resource_key_to_json(DnsResourceKey *key, sd_json_variant **ret);
int dns_resource_key_from_json(sd_json_variant *v, DnsResourceKey **ret);
int dns_resource_record_to_json(DnsResourceRecord *rr, sd_json_variant **ret);
int dns_resource_record_from_json(sd_json_variant *v, DnsResourceRecord **ret);
void dns_resource_key_hash_func(const DnsResourceKey *k, struct siphash *state);
int dns_resource_key_compare_func(const DnsResourceKey *x, const DnsResourceKey *y);
+197
View File
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <float.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/sysmacros.h>
#include <unistd.h>
@@ -1630,4 +1631,200 @@ TEST(must_be) {
ASSERT_OK(sd_json_parse("[]", SD_JSON_PARSE_MUST_BE_OBJECT|SD_JSON_PARSE_MUST_BE_ARRAY, /* ret= */ NULL, /* reterr_line= */ NULL, /* reterr_column= */ NULL));
}
TEST(json_dispatch_in_addr) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL;
/* 192.168.1.1 = { 192, 168, 1, 1 } */
ASSERT_OK(sd_json_build(&j, SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR("addr", JSON_BUILD_IN4_ADDR(&(const struct in_addr) { .s_addr = htobe32(0xC0A80101U) })),
SD_JSON_BUILD_PAIR("null_addr", SD_JSON_BUILD_NULL))));
struct {
struct in_addr addr;
struct in_addr null_addr;
} data = {};
ASSERT_OK(sd_json_dispatch(j,
(const sd_json_dispatch_field[]) {
{ "addr", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in_addr, offsetof(typeof(data), addr) },
{ "null_addr", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in_addr, offsetof(typeof(data), null_addr) },
{},
},
/* flags= */ 0,
&data));
ASSERT_EQ(be32toh(data.addr.s_addr), 0xC0A80101U);
ASSERT_EQ(data.null_addr.s_addr, 0U);
struct in_addr dummy = {};
/* Too few bytes (3 instead of 4) */
j = sd_json_variant_unref(j);
ASSERT_OK(sd_json_build(&j, SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR("addr", SD_JSON_BUILD_ARRAY(SD_JSON_BUILD_UNSIGNED(192), SD_JSON_BUILD_UNSIGNED(168), SD_JSON_BUILD_UNSIGNED(1))))));
ASSERT_ERROR(sd_json_dispatch(j,
(const sd_json_dispatch_field[]) {
{ "addr", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in_addr, 0 },
{},
},
/* flags= */ 0,
&dummy), EINVAL);
/* Too many bytes (5 instead of 4) */
j = sd_json_variant_unref(j);
ASSERT_OK(sd_json_build(&j, SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR("addr", SD_JSON_BUILD_ARRAY(SD_JSON_BUILD_UNSIGNED(192), SD_JSON_BUILD_UNSIGNED(168), SD_JSON_BUILD_UNSIGNED(1), SD_JSON_BUILD_UNSIGNED(1), SD_JSON_BUILD_UNSIGNED(0))))));
ASSERT_ERROR(sd_json_dispatch(j,
(const sd_json_dispatch_field[]) {
{ "addr", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in_addr, 0 },
{},
},
/* flags= */ 0,
&dummy), EINVAL);
/* Not an array or string */
j = sd_json_variant_unref(j);
ASSERT_OK(sd_json_build(&j, SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR("addr", SD_JSON_BUILD_BOOLEAN(true)))));
ASSERT_ERROR(sd_json_dispatch(j,
(const sd_json_dispatch_field[]) {
{ "addr", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in_addr, 0 },
{},
},
/* flags= */ 0,
&dummy), EINVAL);
/* A string */
j = sd_json_variant_unref(j);
ASSERT_OK(sd_json_build(&j, SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR("addr", JSON_BUILD_CONST_STRING("192.168.1.1")))));
zero(data);
ASSERT_OK(sd_json_dispatch(j,
(const sd_json_dispatch_field[]) {
{ "addr", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in_addr, 0 },
{},
},
/* flags= */ 0,
&data));
ASSERT_EQ(be32toh(data.addr.s_addr), 0xC0A80101U);
/* Byte value out of range (> 255) */
j = sd_json_variant_unref(j);
ASSERT_OK(sd_json_build(&j, SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR("addr", SD_JSON_BUILD_ARRAY(SD_JSON_BUILD_UNSIGNED(256), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(1))))));
ASSERT_ERROR(sd_json_dispatch(j,
(const sd_json_dispatch_field[]) {
{ "addr", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in_addr, 0 },
{},
},
/* flags= */ 0,
&dummy), EINVAL);
/* Negative element */
j = sd_json_variant_unref(j);
ASSERT_OK(sd_json_build(&j, SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR("addr", SD_JSON_BUILD_ARRAY(SD_JSON_BUILD_INTEGER(-1), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(1))))));
ASSERT_ERROR(sd_json_dispatch(j,
(const sd_json_dispatch_field[]) {
{ "addr", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in_addr, 0 },
{},
},
/* flags= */ 0,
&dummy), EINVAL);
}
TEST(json_dispatch_in6_addr) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL;
/* ::1 */
ASSERT_OK(sd_json_build(&j, SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR("addr", JSON_BUILD_IN6_ADDR(&(const struct in6_addr) { .s6_addr = { [15] = 1 } })),
SD_JSON_BUILD_PAIR("null_addr", SD_JSON_BUILD_NULL))));
struct {
struct in6_addr addr;
struct in6_addr null_addr;
} data = {};
ASSERT_OK(sd_json_dispatch(j,
(const sd_json_dispatch_field[]) {
{ "addr", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in6_addr, offsetof(typeof(data), addr) },
{ "null_addr", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in6_addr, offsetof(typeof(data), null_addr) },
{},
},
/* flags= */ 0,
&data));
ASSERT_EQ(data.addr.s6_addr[15], 1);
for (size_t i = 0; i < 15; i++)
ASSERT_EQ(data.addr.s6_addr[i], 0);
for (size_t i = 0; i < 16; i++)
ASSERT_EQ(data.null_addr.s6_addr[i], 0);
struct in6_addr dummy = {};
/* Too few bytes (15 instead of 16) */
j = sd_json_variant_unref(j);
ASSERT_OK(sd_json_build(&j, SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR("addr", SD_JSON_BUILD_ARRAY(
SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0),
SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0),
SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0),
SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(1))))));
ASSERT_ERROR(sd_json_dispatch(j,
(const sd_json_dispatch_field[]) {
{ "addr", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in6_addr, 0 },
{},
},
/* flags= */ 0,
&dummy), EINVAL);
/* Too many bytes (17 instead of 16) */
j = sd_json_variant_unref(j);
ASSERT_OK(sd_json_build(&j, SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR("addr", SD_JSON_BUILD_ARRAY(
SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0),
SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0),
SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0),
SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(0), SD_JSON_BUILD_UNSIGNED(1),
SD_JSON_BUILD_UNSIGNED(0))))));
ASSERT_ERROR(sd_json_dispatch(j,
(const sd_json_dispatch_field[]) {
{ "addr", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in6_addr, 0 },
{},
},
/* flags= */ 0,
&dummy), EINVAL);
/* Not an array */
j = sd_json_variant_unref(j);
ASSERT_OK(sd_json_build(&j, SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR("addr", SD_JSON_BUILD_BOOLEAN(true)))));
ASSERT_ERROR(sd_json_dispatch(j,
(const sd_json_dispatch_field[]) {
{ "addr", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in6_addr, 0 },
{},
},
/* flags= */ 0,
&dummy), EINVAL);
/* A string */
j = sd_json_variant_unref(j);
ASSERT_OK(sd_json_build(&j, SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR("addr", JSON_BUILD_CONST_STRING("::1")))));
zero(data);
ASSERT_OK(sd_json_dispatch(j,
(const sd_json_dispatch_field[]) {
{ "addr", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_in6_addr, 0 },
{},
},
/* flags= */ 0,
&data));
ASSERT_EQ(data.addr.s6_addr[15], 1);
for (size_t i = 0; i < 15; i++)
ASSERT_EQ(data.addr.s6_addr[i], 0);
}
DEFINE_TEST_MAIN(LOG_DEBUG);